@hed-hog/operations 0.0.322 → 0.0.325

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 (689) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +9 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +25 -0
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/controllers/operations-project-costs.controller.d.ts +422 -0
  6. package/dist/controllers/operations-project-costs.controller.d.ts.map +1 -0
  7. package/dist/controllers/operations-project-costs.controller.js +250 -0
  8. package/dist/controllers/operations-project-costs.controller.js.map +1 -0
  9. package/dist/controllers/operations-reports.controller.d.ts +9 -0
  10. package/dist/controllers/operations-reports.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-tasks.controller.d.ts +42 -0
  12. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  13. package/dist/controllers/operations-tasks.controller.js +48 -0
  14. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  15. package/dist/controllers/operations-timesheets.controller.d.ts +1 -0
  16. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  17. package/dist/dto/create-collaborator-project-assignment.dto.d.ts +5 -0
  18. package/dist/dto/create-collaborator-project-assignment.dto.d.ts.map +1 -0
  19. package/dist/dto/create-collaborator-project-assignment.dto.js +30 -0
  20. package/dist/dto/create-collaborator-project-assignment.dto.js.map +1 -0
  21. package/dist/dto/create-project-cost-category.dto.d.ts +10 -0
  22. package/dist/dto/create-project-cost-category.dto.d.ts.map +1 -0
  23. package/dist/dto/create-project-cost-category.dto.js +59 -0
  24. package/dist/dto/create-project-cost-category.dto.js.map +1 -0
  25. package/dist/dto/create-project-cost-type.dto.d.ts +14 -0
  26. package/dist/dto/create-project-cost-type.dto.d.ts.map +1 -0
  27. package/dist/dto/create-project-cost-type.dto.js +87 -0
  28. package/dist/dto/create-project-cost-type.dto.js.map +1 -0
  29. package/dist/dto/create-project-cost.dto.d.ts +22 -0
  30. package/dist/dto/create-project-cost.dto.d.ts.map +1 -0
  31. package/dist/dto/create-project-cost.dto.js +135 -0
  32. package/dist/dto/create-project-cost.dto.js.map +1 -0
  33. package/dist/dto/get-project-cost-report.dto.d.ts +10 -0
  34. package/dist/dto/get-project-cost-report.dto.d.ts.map +1 -0
  35. package/dist/dto/get-project-cost-report.dto.js +65 -0
  36. package/dist/dto/get-project-cost-report.dto.js.map +1 -0
  37. package/dist/dto/list-project-cost-categories.dto.d.ts +6 -0
  38. package/dist/dto/list-project-cost-categories.dto.d.ts.map +1 -0
  39. package/dist/dto/list-project-cost-categories.dto.js +34 -0
  40. package/dist/dto/list-project-cost-categories.dto.js.map +1 -0
  41. package/dist/dto/list-project-cost-types.dto.d.ts +8 -0
  42. package/dist/dto/list-project-cost-types.dto.d.ts.map +1 -0
  43. package/dist/dto/list-project-cost-types.dto.js +45 -0
  44. package/dist/dto/list-project-cost-types.dto.js.map +1 -0
  45. package/dist/dto/list-project-costs.dto.d.ts +14 -0
  46. package/dist/dto/list-project-costs.dto.d.ts.map +1 -0
  47. package/dist/dto/list-project-costs.dto.js +81 -0
  48. package/dist/dto/list-project-costs.dto.js.map +1 -0
  49. package/dist/dto/list-tasks.dto.d.ts +1 -0
  50. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  51. package/dist/dto/list-tasks.dto.js +6 -0
  52. package/dist/dto/list-tasks.dto.js.map +1 -1
  53. package/dist/dto/list-timesheets.dto.d.ts +1 -0
  54. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  55. package/dist/dto/list-timesheets.dto.js +7 -0
  56. package/dist/dto/list-timesheets.dto.js.map +1 -1
  57. package/dist/dto/update-collaborator-project-assignment.dto.d.ts +11 -0
  58. package/dist/dto/update-collaborator-project-assignment.dto.d.ts.map +1 -0
  59. package/dist/dto/update-collaborator-project-assignment.dto.js +65 -0
  60. package/dist/dto/update-collaborator-project-assignment.dto.js.map +1 -0
  61. package/dist/dto/update-project-cost-category.dto.d.ts +6 -0
  62. package/dist/dto/update-project-cost-category.dto.d.ts.map +1 -0
  63. package/dist/dto/update-project-cost-category.dto.js +9 -0
  64. package/dist/dto/update-project-cost-category.dto.js.map +1 -0
  65. package/dist/dto/update-project-cost-type.dto.d.ts +6 -0
  66. package/dist/dto/update-project-cost-type.dto.d.ts.map +1 -0
  67. package/dist/dto/update-project-cost-type.dto.js +9 -0
  68. package/dist/dto/update-project-cost-type.dto.js.map +1 -0
  69. package/dist/dto/update-project-cost.dto.d.ts +6 -0
  70. package/dist/dto/update-project-cost.dto.d.ts.map +1 -0
  71. package/dist/dto/update-project-cost.dto.js +9 -0
  72. package/dist/dto/update-project-cost.dto.js.map +1 -0
  73. package/dist/operations.module.d.ts.map +1 -1
  74. package/dist/operations.module.js +2 -0
  75. package/dist/operations.module.js.map +1 -1
  76. package/dist/operations.service.d.ts +562 -0
  77. package/dist/operations.service.d.ts.map +1 -1
  78. package/dist/operations.service.js +1657 -47
  79. package/dist/operations.service.js.map +1 -1
  80. package/hedhog/data/menu.yaml +52 -0
  81. package/hedhog/data/operations_project_cost_category.yaml +80 -0
  82. package/hedhog/data/operations_project_cost_type.yaml +503 -0
  83. package/hedhog/data/route.yaml +274 -0
  84. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +2 -18
  85. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +185 -276
  86. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -0
  87. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +242 -0
  88. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +167 -59
  89. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -853
  90. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +450 -0
  91. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +602 -0
  92. package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +1401 -0
  93. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2003 -1846
  94. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +56 -11
  95. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +297 -2
  96. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +530 -0
  97. package/hedhog/frontend/app/_lib/api.ts.ejs +247 -0
  98. package/hedhog/frontend/app/_lib/types.ts.ejs +196 -7
  99. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +9 -3
  100. package/hedhog/frontend/app/collaborators/page.tsx.ejs +18 -7
  101. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +219 -122
  102. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +674 -0
  103. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +845 -0
  104. package/hedhog/frontend/app/projects/[id]/costs-report/page.tsx.ejs +10 -0
  105. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +20 -349
  106. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +192 -484
  107. package/hedhog/frontend/messages/en.json +279 -10
  108. package/hedhog/frontend/messages/en.json.ejs +2043 -0
  109. package/hedhog/frontend/messages/operations/en.json +2068 -0
  110. package/hedhog/frontend/messages/operations/operations/en.json +2102 -0
  111. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  112. package/hedhog/frontend/messages/operations/pt.json +2072 -0
  113. package/hedhog/frontend/messages/pt.json +284 -13
  114. package/hedhog/frontend/messages/pt.json.ejs +2056 -0
  115. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts +29 -0
  116. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts.map +1 -0
  117. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js +95 -0
  118. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js.map +1 -0
  119. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.tsx +233 -0
  120. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts +10 -0
  121. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  122. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js +577 -0
  123. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js.map +1 -0
  124. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.tsx +868 -0
  125. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts +4 -0
  126. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  127. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js +337 -0
  128. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js.map +1 -0
  129. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.tsx +476 -0
  130. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts +9 -0
  131. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  132. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js +1348 -0
  133. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js.map +1 -0
  134. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.tsx +2233 -0
  135. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts +12 -0
  136. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  137. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js +162 -0
  138. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js.map +1 -0
  139. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.tsx +261 -0
  140. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts +18 -0
  141. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts.map +1 -0
  142. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js +145 -0
  143. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js.map +1 -0
  144. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.tsx +258 -0
  145. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts +4 -0
  146. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts.map +1 -0
  147. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js +223 -0
  148. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js.map +1 -0
  149. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.tsx +342 -0
  150. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts +58 -0
  151. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts.map +1 -0
  152. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js +438 -0
  153. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js.map +1 -0
  154. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.tsx +698 -0
  155. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts +20 -0
  156. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts.map +1 -0
  157. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js +233 -0
  158. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js.map +1 -0
  159. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.tsx +392 -0
  160. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts +4 -0
  161. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  162. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js +814 -0
  163. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js.map +1 -0
  164. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.tsx +1288 -0
  165. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts +21 -0
  166. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts.map +1 -0
  167. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js +174 -0
  168. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js.map +1 -0
  169. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.tsx +306 -0
  170. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts +10 -0
  171. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts.map +1 -0
  172. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js +12 -0
  173. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js.map +1 -0
  174. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.tsx +29 -0
  175. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts +15 -0
  176. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts.map +1 -0
  177. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js +501 -0
  178. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js.map +1 -0
  179. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.tsx +853 -0
  180. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts +6 -0
  181. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts.map +1 -0
  182. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js +847 -0
  183. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js.map +1 -0
  184. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.tsx +1340 -0
  185. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts +4 -0
  186. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts.map +1 -0
  187. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js +2930 -0
  188. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js.map +1 -0
  189. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.tsx +4378 -0
  190. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts +9 -0
  191. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts.map +1 -0
  192. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js +1013 -0
  193. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js.map +1 -0
  194. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.tsx +1745 -0
  195. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts +13 -0
  196. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts.map +1 -0
  197. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js +38 -0
  198. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js.map +1 -0
  199. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.tsx +74 -0
  200. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts +7 -0
  201. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts.map +1 -0
  202. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js +11 -0
  203. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js.map +1 -0
  204. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.tsx +15 -0
  205. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts +18 -0
  206. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  207. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js +406 -0
  208. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js.map +1 -0
  209. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.tsx +660 -0
  210. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts +26 -0
  211. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts.map +1 -0
  212. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js +332 -0
  213. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js.map +1 -0
  214. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.tsx +518 -0
  215. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts +6 -0
  216. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts.map +1 -0
  217. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js +255 -0
  218. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js.map +1 -0
  219. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.tsx +388 -0
  220. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  221. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  222. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js +131 -0
  223. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  224. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  225. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts +108 -0
  226. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts.map +1 -0
  227. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js +162 -0
  228. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js.map +1 -0
  229. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.ts +428 -0
  230. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  231. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  232. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js +36 -0
  233. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js.map +1 -0
  234. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.ts +44 -0
  235. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +836 -0
  236. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -0
  237. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js +3 -0
  238. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js.map +1 -0
  239. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +860 -0
  240. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts +16 -0
  241. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts.map +1 -0
  242. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js +182 -0
  243. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js.map +1 -0
  244. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.ts +250 -0
  245. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts +4 -0
  246. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts.map +1 -0
  247. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js +51 -0
  248. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js.map +1 -0
  249. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.ts +61 -0
  250. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts +2 -0
  251. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts.map +1 -0
  252. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js +954 -0
  253. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js.map +1 -0
  254. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.tsx +1277 -0
  255. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts +2 -0
  256. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts.map +1 -0
  257. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js +488 -0
  258. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js.map +1 -0
  259. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.tsx +805 -0
  260. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts +6 -0
  261. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  262. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js +9 -0
  263. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js.map +1 -0
  264. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.tsx +11 -0
  265. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts +6 -0
  266. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts.map +1 -0
  267. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js +9 -0
  268. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js.map +1 -0
  269. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.tsx +11 -0
  270. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts +2 -0
  271. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts.map +1 -0
  272. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js +8 -0
  273. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js.map +1 -0
  274. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.tsx +5 -0
  275. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts +2 -0
  276. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts.map +1 -0
  277. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js +612 -0
  278. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js.map +1 -0
  279. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.tsx +939 -0
  280. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts +6 -0
  281. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  282. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js +9 -0
  283. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js.map +1 -0
  284. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.tsx +11 -0
  285. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts +6 -0
  286. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts.map +1 -0
  287. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js +9 -0
  288. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js.map +1 -0
  289. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.tsx +11 -0
  290. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts +6 -0
  291. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts.map +1 -0
  292. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js +9 -0
  293. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js.map +1 -0
  294. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.tsx +17 -0
  295. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts +2 -0
  296. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts.map +1 -0
  297. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js +348 -0
  298. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js.map +1 -0
  299. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.tsx +536 -0
  300. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts +2 -0
  301. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts.map +1 -0
  302. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js +401 -0
  303. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js.map +1 -0
  304. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.tsx +607 -0
  305. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts +5 -0
  306. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts.map +1 -0
  307. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js +7 -0
  308. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js.map +1 -0
  309. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.tsx +9 -0
  310. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts +6 -0
  311. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts.map +1 -0
  312. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js +9 -0
  313. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js.map +1 -0
  314. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.tsx +11 -0
  315. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts +2 -0
  316. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts.map +1 -0
  317. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js +321 -0
  318. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js.map +1 -0
  319. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.tsx +440 -0
  320. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts +2 -0
  321. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts.map +1 -0
  322. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js +939 -0
  323. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js.map +1 -0
  324. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.tsx +1499 -0
  325. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts +29 -0
  326. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts.map +1 -0
  327. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js +95 -0
  328. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js.map +1 -0
  329. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.tsx +233 -0
  330. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts +10 -0
  331. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  332. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js +577 -0
  333. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js.map +1 -0
  334. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.tsx +868 -0
  335. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts +4 -0
  336. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  337. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js +337 -0
  338. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js.map +1 -0
  339. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.tsx +476 -0
  340. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts +9 -0
  341. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  342. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js +1348 -0
  343. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js.map +1 -0
  344. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.tsx +2233 -0
  345. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts +12 -0
  346. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  347. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js +162 -0
  348. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js.map +1 -0
  349. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.tsx +261 -0
  350. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts +18 -0
  351. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts.map +1 -0
  352. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js +145 -0
  353. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js.map +1 -0
  354. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.tsx +258 -0
  355. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts +4 -0
  356. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts.map +1 -0
  357. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js +223 -0
  358. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js.map +1 -0
  359. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.tsx +342 -0
  360. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts +58 -0
  361. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts.map +1 -0
  362. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js +438 -0
  363. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js.map +1 -0
  364. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.tsx +698 -0
  365. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts +20 -0
  366. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts.map +1 -0
  367. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js +233 -0
  368. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js.map +1 -0
  369. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.tsx +392 -0
  370. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts +4 -0
  371. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  372. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js +814 -0
  373. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js.map +1 -0
  374. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.tsx +1288 -0
  375. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts +21 -0
  376. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts.map +1 -0
  377. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js +174 -0
  378. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js.map +1 -0
  379. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.tsx +306 -0
  380. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts +10 -0
  381. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts.map +1 -0
  382. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js +12 -0
  383. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js.map +1 -0
  384. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.tsx +29 -0
  385. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts +15 -0
  386. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts.map +1 -0
  387. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js +501 -0
  388. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js.map +1 -0
  389. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.tsx +853 -0
  390. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts +6 -0
  391. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts.map +1 -0
  392. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js +459 -0
  393. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js.map +1 -0
  394. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.tsx +598 -0
  395. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts +6 -0
  396. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts.map +1 -0
  397. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js +876 -0
  398. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js.map +1 -0
  399. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.tsx +1368 -0
  400. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts +4 -0
  401. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts.map +1 -0
  402. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js +2930 -0
  403. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js.map +1 -0
  404. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.tsx +4378 -0
  405. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts +9 -0
  406. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts.map +1 -0
  407. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js +1013 -0
  408. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js.map +1 -0
  409. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.tsx +1745 -0
  410. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts +13 -0
  411. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts.map +1 -0
  412. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js +38 -0
  413. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js.map +1 -0
  414. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.tsx +74 -0
  415. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts +7 -0
  416. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts.map +1 -0
  417. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js +11 -0
  418. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js.map +1 -0
  419. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.tsx +15 -0
  420. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts +18 -0
  421. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  422. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js +406 -0
  423. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js.map +1 -0
  424. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.tsx +660 -0
  425. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts +26 -0
  426. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts.map +1 -0
  427. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js +332 -0
  428. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js.map +1 -0
  429. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.tsx +518 -0
  430. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts +6 -0
  431. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts.map +1 -0
  432. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js +255 -0
  433. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js.map +1 -0
  434. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.tsx +388 -0
  435. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  436. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  437. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js +131 -0
  438. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  439. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  440. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts +108 -0
  441. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts.map +1 -0
  442. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js +162 -0
  443. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js.map +1 -0
  444. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.ts +428 -0
  445. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  446. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  447. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js +36 -0
  448. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js.map +1 -0
  449. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.ts +44 -0
  450. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +836 -0
  451. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -0
  452. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js +3 -0
  453. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js.map +1 -0
  454. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +860 -0
  455. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts +16 -0
  456. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts.map +1 -0
  457. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js +182 -0
  458. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js.map +1 -0
  459. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.ts +250 -0
  460. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts +4 -0
  461. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts.map +1 -0
  462. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js +51 -0
  463. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js.map +1 -0
  464. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.ts +61 -0
  465. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts +2 -0
  466. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts.map +1 -0
  467. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js +954 -0
  468. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js.map +1 -0
  469. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.tsx +1277 -0
  470. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts +2 -0
  471. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts.map +1 -0
  472. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js +488 -0
  473. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js.map +1 -0
  474. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.tsx +805 -0
  475. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts +6 -0
  476. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  477. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js +9 -0
  478. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js.map +1 -0
  479. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.tsx +11 -0
  480. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts +6 -0
  481. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts.map +1 -0
  482. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js +9 -0
  483. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js.map +1 -0
  484. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.tsx +11 -0
  485. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts +2 -0
  486. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts.map +1 -0
  487. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js +8 -0
  488. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js.map +1 -0
  489. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.tsx +5 -0
  490. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts +2 -0
  491. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts.map +1 -0
  492. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js +612 -0
  493. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js.map +1 -0
  494. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.tsx +939 -0
  495. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts +6 -0
  496. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  497. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js +9 -0
  498. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js.map +1 -0
  499. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.tsx +11 -0
  500. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts +6 -0
  501. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts.map +1 -0
  502. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js +9 -0
  503. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js.map +1 -0
  504. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.tsx +11 -0
  505. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts +6 -0
  506. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts.map +1 -0
  507. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js +9 -0
  508. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js.map +1 -0
  509. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.tsx +17 -0
  510. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts +2 -0
  511. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts.map +1 -0
  512. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js +348 -0
  513. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js.map +1 -0
  514. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.tsx +536 -0
  515. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts +2 -0
  516. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts.map +1 -0
  517. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js +401 -0
  518. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js.map +1 -0
  519. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.tsx +607 -0
  520. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts +5 -0
  521. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts.map +1 -0
  522. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js +7 -0
  523. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js.map +1 -0
  524. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.tsx +9 -0
  525. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts +6 -0
  526. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts.map +1 -0
  527. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js +9 -0
  528. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js.map +1 -0
  529. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.tsx +11 -0
  530. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts +2 -0
  531. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts.map +1 -0
  532. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js +321 -0
  533. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js.map +1 -0
  534. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.tsx +440 -0
  535. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts +2 -0
  536. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts.map +1 -0
  537. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js +939 -0
  538. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js.map +1 -0
  539. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.tsx +1499 -0
  540. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts +2 -0
  541. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts.map +1 -0
  542. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js +8 -0
  543. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js.map +1 -0
  544. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.tsx +5 -0
  545. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts +2 -0
  546. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts.map +1 -0
  547. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js +436 -0
  548. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js.map +1 -0
  549. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.tsx +675 -0
  550. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts +2 -0
  551. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts.map +1 -0
  552. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js +563 -0
  553. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js.map +1 -0
  554. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.tsx +846 -0
  555. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts +6 -0
  556. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts.map +1 -0
  557. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js +9 -0
  558. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js.map +1 -0
  559. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.tsx +10 -0
  560. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts +6 -0
  561. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts.map +1 -0
  562. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js +9 -0
  563. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js.map +1 -0
  564. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.tsx +11 -0
  565. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts +6 -0
  566. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts.map +1 -0
  567. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js +9 -0
  568. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js.map +1 -0
  569. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.tsx +11 -0
  570. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts +2 -0
  571. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts.map +1 -0
  572. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js +8 -0
  573. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js.map +1 -0
  574. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.tsx +5 -0
  575. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts +2 -0
  576. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts.map +1 -0
  577. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js +492 -0
  578. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js.map +1 -0
  579. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.tsx +757 -0
  580. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts +2 -0
  581. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts.map +1 -0
  582. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js +342 -0
  583. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js.map +1 -0
  584. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.tsx +430 -0
  585. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts +2 -0
  586. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts.map +1 -0
  587. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js +338 -0
  588. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js.map +1 -0
  589. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.tsx +428 -0
  590. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts +2 -0
  591. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts.map +1 -0
  592. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js +660 -0
  593. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js.map +1 -0
  594. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.tsx +992 -0
  595. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts +2 -0
  596. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts.map +1 -0
  597. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js +515 -0
  598. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js.map +1 -0
  599. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.tsx +707 -0
  600. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts +2 -0
  601. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts.map +1 -0
  602. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js +1141 -0
  603. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js.map +1 -0
  604. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.tsx +1705 -0
  605. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts +2 -0
  606. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts.map +1 -0
  607. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js +8 -0
  608. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js.map +1 -0
  609. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.tsx +5 -0
  610. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts +2 -0
  611. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts.map +1 -0
  612. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js +436 -0
  613. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js.map +1 -0
  614. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.tsx +675 -0
  615. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts +2 -0
  616. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts.map +1 -0
  617. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js +563 -0
  618. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js.map +1 -0
  619. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.tsx +846 -0
  620. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts +6 -0
  621. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts.map +1 -0
  622. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js +9 -0
  623. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js.map +1 -0
  624. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.tsx +11 -0
  625. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts +6 -0
  626. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts.map +1 -0
  627. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js +9 -0
  628. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js.map +1 -0
  629. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.tsx +11 -0
  630. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts +2 -0
  631. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts.map +1 -0
  632. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js +8 -0
  633. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js.map +1 -0
  634. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.tsx +5 -0
  635. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts +2 -0
  636. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts.map +1 -0
  637. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js +492 -0
  638. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js.map +1 -0
  639. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.tsx +757 -0
  640. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts +2 -0
  641. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts.map +1 -0
  642. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js +342 -0
  643. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js.map +1 -0
  644. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.tsx +430 -0
  645. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts +2 -0
  646. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts.map +1 -0
  647. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js +338 -0
  648. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js.map +1 -0
  649. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.tsx +428 -0
  650. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts +2 -0
  651. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts.map +1 -0
  652. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js +660 -0
  653. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js.map +1 -0
  654. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.tsx +992 -0
  655. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts +2 -0
  656. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts.map +1 -0
  657. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js +515 -0
  658. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js.map +1 -0
  659. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.tsx +707 -0
  660. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts +2 -0
  661. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts.map +1 -0
  662. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js +1141 -0
  663. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js.map +1 -0
  664. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.tsx +1705 -0
  665. package/hedhog/table/operations_project_assignment.yaml +1 -0
  666. package/hedhog/table/operations_project_cost.yaml +93 -0
  667. package/hedhog/table/operations_project_cost_category.yaml +37 -0
  668. package/hedhog/table/operations_project_cost_type.yaml +55 -0
  669. package/hedhog/table/operations_task_comment.yaml +26 -0
  670. package/package.json +6 -6
  671. package/src/controllers/operations-collaborators.controller.ts +26 -0
  672. package/src/controllers/operations-project-costs.controller.ts +249 -0
  673. package/src/controllers/operations-tasks.controller.ts +49 -0
  674. package/src/dto/create-collaborator-project-assignment.dto.ts +14 -0
  675. package/src/dto/create-project-cost-category.dto.ts +37 -0
  676. package/src/dto/create-project-cost-type.dto.ts +64 -0
  677. package/src/dto/create-project-cost.dto.ts +126 -0
  678. package/src/dto/get-project-cost-report.dto.ts +46 -0
  679. package/src/dto/list-project-cost-categories.dto.ts +17 -0
  680. package/src/dto/list-project-cost-types.dto.ts +28 -0
  681. package/src/dto/list-project-costs.dto.ts +59 -0
  682. package/src/dto/list-tasks.dto.ts +7 -0
  683. package/src/dto/list-timesheets.dto.ts +7 -1
  684. package/src/dto/update-collaborator-project-assignment.dto.ts +58 -0
  685. package/src/dto/update-project-cost-category.dto.ts +4 -0
  686. package/src/dto/update-project-cost-type.dto.ts +4 -0
  687. package/src/dto/update-project-cost.dto.ts +4 -0
  688. package/src/operations.module.ts +2 -0
  689. package/src/operations.service.ts +2274 -39
@@ -9,6 +9,7 @@ import {
9
9
  } from '@hed-hog/core';
10
10
  import {
11
11
  BadRequestException,
12
+ ConflictException,
12
13
  ForbiddenException,
13
14
  Inject,
14
15
  Injectable,
@@ -544,6 +545,18 @@ type TaskPayload = {
544
545
  archived?: boolean;
545
546
  };
546
547
 
548
+ type TaskCommentRecord = {
549
+ id: number;
550
+ taskId: number;
551
+ content: string;
552
+ actorCollaboratorId: number | null;
553
+ actorName: string | null;
554
+ actorUserPhotoId: number | null;
555
+ actorPersonAvatarId: number | null;
556
+ createdAt: string;
557
+ updatedAt: string | null;
558
+ };
559
+
547
560
  type QuickTimesheetEntryPayload = {
548
561
  projectId?: number | null;
549
562
  projectAssignmentId?: number | null;
@@ -2149,6 +2162,95 @@ export class OperationsService {
2149
2162
  return this.getCollaboratorByIdForUser(userId, collaboratorId);
2150
2163
  }
2151
2164
 
2165
+ async updateCollaboratorProjectAssignment(
2166
+ collaboratorId: number,
2167
+ projectId: number,
2168
+ data: {
2169
+ projectRoleId?: number | null;
2170
+ roleLabel?: string | null;
2171
+ allocationPercent?: number | null;
2172
+ weeklyHours?: number | null;
2173
+ startDate?: string | null;
2174
+ endDate?: string | null;
2175
+ status?: string;
2176
+ }
2177
+ ) {
2178
+ const sets: string[] = [];
2179
+ const params: unknown[] = [collaboratorId, projectId];
2180
+ let idx = 3;
2181
+
2182
+ if ('projectRoleId' in data) {
2183
+ sets.push(`project_role_id = $${idx++}`);
2184
+ params.push(data.projectRoleId ?? null);
2185
+ }
2186
+ if ('roleLabel' in data) {
2187
+ sets.push(`role_label = $${idx++}`);
2188
+ params.push(data.roleLabel ?? null);
2189
+ }
2190
+ if ('allocationPercent' in data) {
2191
+ sets.push(`allocation_percent = $${idx++}`);
2192
+ params.push(data.allocationPercent ?? null);
2193
+ }
2194
+ if ('weeklyHours' in data) {
2195
+ sets.push(`weekly_hours = $${idx++}`);
2196
+ params.push(data.weeklyHours ?? null);
2197
+ }
2198
+ if ('startDate' in data) {
2199
+ sets.push(`start_date = $${idx++}::date`);
2200
+ params.push(data.startDate ?? null);
2201
+ }
2202
+ if ('endDate' in data) {
2203
+ sets.push(`end_date = $${idx++}::date`);
2204
+ params.push(data.endDate ?? null);
2205
+ }
2206
+ if ('status' in data) {
2207
+ sets.push(
2208
+ `status = $${idx++}::operations_project_assignment_status_155b459bbf_enum`
2209
+ );
2210
+ params.push(data.status);
2211
+ }
2212
+
2213
+ if (!sets.length) return { updated: false };
2214
+
2215
+ sets.push(`updated_at = NOW()`);
2216
+
2217
+ await this.prisma.$executeRawUnsafe(
2218
+ `UPDATE operations_project_assignment
2219
+ SET ${sets.join(', ')}
2220
+ WHERE collaborator_id = $1
2221
+ AND project_id = $2
2222
+ AND deleted_at IS NULL`,
2223
+ ...params
2224
+ );
2225
+
2226
+ return { updated: true };
2227
+ }
2228
+
2229
+ async addCollaboratorProjectAssignment(
2230
+ collaboratorId: number,
2231
+ data: { projectId: number; roleLabel?: string }
2232
+ ) {
2233
+ const existing = await this.querySingle<{ id: number }>(
2234
+ `SELECT id FROM operations_project_assignment
2235
+ WHERE collaborator_id = $1 AND project_id = $2 AND deleted_at IS NULL`,
2236
+ [collaboratorId, data.projectId]
2237
+ );
2238
+
2239
+ if (existing) {
2240
+ return { id: existing.id, created: false };
2241
+ }
2242
+
2243
+ const row = await this.querySingle<{ id: number }>(
2244
+ `INSERT INTO operations_project_assignment
2245
+ (collaborator_id, project_id, role_label, status)
2246
+ VALUES ($1, $2, $3, 'active')
2247
+ RETURNING id`,
2248
+ [collaboratorId, data.projectId, data.roleLabel ?? '']
2249
+ );
2250
+
2251
+ return { id: row!.id, created: true };
2252
+ }
2253
+
2152
2254
  async getCollaboratorCompensationHistory(
2153
2255
  userId: number,
2154
2256
  collaboratorId: number
@@ -2755,6 +2857,7 @@ export class OperationsService {
2755
2857
  status?: string;
2756
2858
  myOnly?: boolean;
2757
2859
  archived?: boolean;
2860
+ collaboratorId?: number;
2758
2861
  }
2759
2862
  ) {
2760
2863
  const actor = await this.getActorContext(userId);
@@ -2828,6 +2931,13 @@ export class OperationsService {
2828
2931
  filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
2829
2932
  }
2830
2933
 
2934
+ if (paginationParams.collaboratorId) {
2935
+ const colId = paginationParams.collaboratorId;
2936
+ filters.push(
2937
+ `(pa.collaborator_id = ${this.param(params, colId)} OR t.assignee_collaborator_id = ${this.param(params, colId)})`
2938
+ );
2939
+ }
2940
+
2831
2941
  const whereClause = filters.join(' AND ');
2832
2942
  const totalRow = await this.querySingle<{ total: string }>(
2833
2943
  `SELECT COUNT(*)::text AS total
@@ -2868,6 +2978,8 @@ export class OperationsService {
2868
2978
  assigneeName: string | null;
2869
2979
  assigneeUserPhotoId: number | null;
2870
2980
  assigneePersonAvatarId: number | null;
2981
+ commentCount: number;
2982
+ fileCount: number;
2871
2983
  createdAt: string;
2872
2984
  deletedAt: string | null;
2873
2985
  }>(
@@ -2886,6 +2998,8 @@ export class OperationsService {
2886
2998
  ac.display_name AS "assigneeName",
2887
2999
  au.photo_id AS "assigneeUserPhotoId",
2888
3000
  ap.avatar_id AS "assigneePersonAvatarId",
3001
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
3002
+ COALESCE(task_files.count, 0)::int AS "fileCount",
2889
3003
  t.created_at AS "createdAt",
2890
3004
  t.deleted_at AS "deletedAt"
2891
3005
  FROM operations_task t
@@ -2898,6 +3012,16 @@ export class OperationsService {
2898
3012
  ON au.id = ac.user_id
2899
3013
  LEFT JOIN person ap
2900
3014
  ON ap.id = ac.person_id
3015
+ LEFT JOIN LATERAL (
3016
+ SELECT COUNT(*) AS count
3017
+ FROM operations_task_comment tc
3018
+ WHERE tc.task_id = t.id
3019
+ ) task_comments ON TRUE
3020
+ LEFT JOIN LATERAL (
3021
+ SELECT COUNT(*) AS count
3022
+ FROM operations_task_file tf
3023
+ WHERE tf.operations_task_id = t.id
3024
+ ) task_files ON TRUE
2901
3025
  JOIN operations_project p
2902
3026
  ON p.id = COALESCE(t.project_id, pa.project_id)
2903
3027
  WHERE ${whereClause}
@@ -3262,6 +3386,183 @@ export class OperationsService {
3262
3386
  return { success: true };
3263
3387
  }
3264
3388
 
3389
+ async listTaskComments(userId: number, taskId: number) {
3390
+ const actor = await this.getActorContext(userId);
3391
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3392
+ throw new ForbiddenException(
3393
+ 'Operations collaborator access is required.'
3394
+ );
3395
+ }
3396
+
3397
+ const current = await this.getTaskRecordForActor(
3398
+ this.prisma,
3399
+ actor,
3400
+ taskId
3401
+ );
3402
+ await this.assertProjectAccess(actor, current.projectId);
3403
+
3404
+ return this.queryRows<TaskCommentRecord>(
3405
+ `SELECT tc.id,
3406
+ tc.task_id AS "taskId",
3407
+ tc.content,
3408
+ tc.actor_collaborator_id AS "actorCollaboratorId",
3409
+ actor.display_name AS "actorName",
3410
+ actor_user.photo_id AS "actorUserPhotoId",
3411
+ actor_person.avatar_id AS "actorPersonAvatarId",
3412
+ tc.created_at AS "createdAt",
3413
+ tc.updated_at AS "updatedAt"
3414
+ FROM operations_task_comment tc
3415
+ LEFT JOIN operations_collaborator actor
3416
+ ON actor.id = tc.actor_collaborator_id
3417
+ AND actor.deleted_at IS NULL
3418
+ LEFT JOIN "user" actor_user
3419
+ ON actor_user.id = actor.user_id
3420
+ LEFT JOIN person actor_person
3421
+ ON actor_person.id = actor.person_id
3422
+ WHERE tc.task_id = $1
3423
+ ORDER BY tc.created_at ASC, tc.id ASC`,
3424
+ [taskId]
3425
+ );
3426
+ }
3427
+
3428
+ async addTaskComment(userId: number, taskId: number, content: string) {
3429
+ const actor = await this.getActorContext(userId);
3430
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3431
+ throw new ForbiddenException(
3432
+ 'Operations collaborator access is required.'
3433
+ );
3434
+ }
3435
+
3436
+ const current = await this.getTaskRecordForActor(
3437
+ this.prisma,
3438
+ actor,
3439
+ taskId
3440
+ );
3441
+ await this.assertProjectAccess(actor, current.projectId);
3442
+
3443
+ const normalizedContent = this.normalizeOptionalText(content);
3444
+ if (!normalizedContent) {
3445
+ throw new BadRequestException('Comment content is required.');
3446
+ }
3447
+
3448
+ const inserted = await this.queryRows<{ id: number }>(
3449
+ `INSERT INTO operations_task_comment (
3450
+ task_id,
3451
+ actor_collaborator_id,
3452
+ content,
3453
+ created_at,
3454
+ updated_at
3455
+ ) VALUES ($1, $2, $3, NOW(), NOW())
3456
+ RETURNING id`,
3457
+ [taskId, actor.collaboratorId ?? null, normalizedContent]
3458
+ );
3459
+
3460
+ const commentId = inserted[0]?.id;
3461
+ const comments = await this.listTaskComments(userId, taskId);
3462
+ const createdComment = comments.find((comment) => comment.id === commentId);
3463
+
3464
+ if (!createdComment) {
3465
+ throw new NotFoundException('Task comment could not be loaded.');
3466
+ }
3467
+
3468
+ return createdComment;
3469
+ }
3470
+
3471
+ async updateTaskComment(
3472
+ userId: number,
3473
+ taskId: number,
3474
+ commentId: number,
3475
+ content: string
3476
+ ) {
3477
+ const actor = await this.getActorContext(userId);
3478
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3479
+ throw new ForbiddenException(
3480
+ 'Operations collaborator access is required.'
3481
+ );
3482
+ }
3483
+
3484
+ const current = await this.getTaskRecordForActor(
3485
+ this.prisma,
3486
+ actor,
3487
+ taskId
3488
+ );
3489
+ await this.assertProjectAccess(actor, current.projectId);
3490
+
3491
+ const normalizedContent = this.normalizeOptionalText(content);
3492
+ if (!normalizedContent) {
3493
+ throw new BadRequestException('Comment content is required.');
3494
+ }
3495
+
3496
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
3497
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId"
3498
+ FROM operations_task_comment
3499
+ WHERE id = $1 AND task_id = $2`,
3500
+ [commentId, taskId]
3501
+ );
3502
+
3503
+ const row = rows[0];
3504
+ if (!row) {
3505
+ throw new NotFoundException('Comment not found.');
3506
+ }
3507
+
3508
+ if (row.actorCollaboratorId !== actor.collaboratorId) {
3509
+ throw new ForbiddenException('You can only edit your own comments.');
3510
+ }
3511
+
3512
+ await this.queryRows(
3513
+ `UPDATE operations_task_comment
3514
+ SET content = $1, updated_at = NOW()
3515
+ WHERE id = $2`,
3516
+ [normalizedContent, commentId]
3517
+ );
3518
+
3519
+ const comments = await this.listTaskComments(userId, taskId);
3520
+ return comments.find((c) => c.id === commentId) ?? null;
3521
+ }
3522
+
3523
+ async removeTaskComment(
3524
+ userId: number,
3525
+ taskId: number,
3526
+ commentId: number
3527
+ ) {
3528
+ const actor = await this.getActorContext(userId);
3529
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3530
+ throw new ForbiddenException(
3531
+ 'Operations collaborator access is required.'
3532
+ );
3533
+ }
3534
+
3535
+ const current = await this.getTaskRecordForActor(
3536
+ this.prisma,
3537
+ actor,
3538
+ taskId
3539
+ );
3540
+ await this.assertProjectAccess(actor, current.projectId);
3541
+
3542
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
3543
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId"
3544
+ FROM operations_task_comment
3545
+ WHERE id = $1 AND task_id = $2`,
3546
+ [commentId, taskId]
3547
+ );
3548
+
3549
+ const row = rows[0];
3550
+ if (!row) {
3551
+ throw new NotFoundException('Comment not found.');
3552
+ }
3553
+
3554
+ if (row.actorCollaboratorId !== actor.collaboratorId) {
3555
+ throw new ForbiddenException('You can only delete your own comments.');
3556
+ }
3557
+
3558
+ await this.queryRows(
3559
+ `DELETE FROM operations_task_comment WHERE id = $1`,
3560
+ [commentId]
3561
+ );
3562
+
3563
+ return { success: true };
3564
+ }
3565
+
3265
3566
  async listTimesheetEntries(
3266
3567
  userId: number,
3267
3568
  paginationParams: {
@@ -5099,6 +5400,7 @@ export class OperationsService {
5099
5400
  status?: string;
5100
5401
  dateFrom?: string;
5101
5402
  dateTo?: string;
5403
+ collaboratorId?: number;
5102
5404
  } = {}
5103
5405
  ) {
5104
5406
  const actor = await this.getActorContext(userId);
@@ -5125,6 +5427,10 @@ export class OperationsService {
5125
5427
  where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
5126
5428
  }
5127
5429
 
5430
+ if (filters.collaboratorId) {
5431
+ where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
5432
+ }
5433
+
5128
5434
  if (pagination?.search) {
5129
5435
  const searchPlaceholder = this.param(params, `%${pagination.search}%`);
5130
5436
  where.push(`(
@@ -5164,6 +5470,7 @@ export class OperationsService {
5164
5470
  reviewedAt: string | null;
5165
5471
  notes: string | null;
5166
5472
  decisionNote: string | null;
5473
+ approvalId: number | null;
5167
5474
  }>(
5168
5475
  `SELECT t.id,
5169
5476
  t.collaborator_id AS "collaboratorId",
@@ -5177,7 +5484,8 @@ export class OperationsService {
5177
5484
  t.submitted_at AS "submittedAt",
5178
5485
  t.reviewed_at AS "reviewedAt",
5179
5486
  t.notes,
5180
- approval.decision_note AS "decisionNote"
5487
+ approval.decision_note AS "decisionNote",
5488
+ approval.id AS "approvalId"
5181
5489
  FROM operations_timesheet t
5182
5490
  JOIN operations_collaborator c ON c.id = t.collaborator_id
5183
5491
  LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
@@ -8026,6 +8334,7 @@ export class OperationsService {
8026
8334
  assigneeUserPhotoId: number | null;
8027
8335
  assigneePersonAvatarId: number | null;
8028
8336
  projectAssignmentId: number | null;
8337
+ commentCount: number;
8029
8338
  createdAt: string;
8030
8339
  }>(
8031
8340
  `SELECT t.id,
@@ -8042,6 +8351,7 @@ export class OperationsService {
8042
8351
  au.photo_id AS "assigneeUserPhotoId",
8043
8352
  ap.avatar_id AS "assigneePersonAvatarId",
8044
8353
  t.project_assignment_id AS "projectAssignmentId",
8354
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
8045
8355
  t.created_at AS "createdAt"
8046
8356
  FROM operations_task t
8047
8357
  LEFT JOIN operations_collaborator ac
@@ -8050,6 +8360,11 @@ export class OperationsService {
8050
8360
  ON au.id = ac.user_id
8051
8361
  LEFT JOIN person ap
8052
8362
  ON ap.id = ac.person_id
8363
+ LEFT JOIN LATERAL (
8364
+ SELECT COUNT(*) AS count
8365
+ FROM operations_task_comment tc
8366
+ WHERE tc.task_id = t.id
8367
+ ) task_comments ON TRUE
8053
8368
  WHERE COALESCE(t.project_id, (
8054
8369
  SELECT pa.project_id FROM operations_project_assignment pa
8055
8370
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -8080,6 +8395,7 @@ export class OperationsService {
8080
8395
  assigneePersonAvatarId: number | null;
8081
8396
  projectAssignmentId: number | null;
8082
8397
  projectId: number | null;
8398
+ commentCount: number;
8083
8399
  createdAt: string;
8084
8400
  deletedAt: string | null;
8085
8401
  }>(
@@ -8098,6 +8414,7 @@ export class OperationsService {
8098
8414
  ap.avatar_id AS "assigneePersonAvatarId",
8099
8415
  t.project_assignment_id AS "projectAssignmentId",
8100
8416
  COALESCE(t.project_id, pa.project_id) AS "projectId",
8417
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
8101
8418
  t.created_at AS "createdAt",
8102
8419
  t.deleted_at AS "deletedAt"
8103
8420
  FROM operations_task t
@@ -8107,6 +8424,11 @@ export class OperationsService {
8107
8424
  LEFT JOIN person ap ON ap.id = ac.person_id
8108
8425
  LEFT JOIN operations_project_assignment pa
8109
8426
  ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
8427
+ LEFT JOIN LATERAL (
8428
+ SELECT COUNT(*) AS count
8429
+ FROM operations_task_comment tc
8430
+ WHERE tc.task_id = t.id
8431
+ ) task_comments ON TRUE
8110
8432
  WHERE t.id = $1`,
8111
8433
  [taskId]
8112
8434
  );
@@ -11052,12 +11374,18 @@ export class OperationsService {
11052
11374
  au.photo_id AS "assigneeUserPhotoId",
11053
11375
  ap.avatar_id AS "assigneePersonAvatarId",
11054
11376
  t.project_assignment_id AS "projectAssignmentId",
11377
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
11055
11378
  t.created_at AS "createdAt"
11056
11379
  FROM operations_task t
11057
11380
  LEFT JOIN operations_collaborator ac
11058
11381
  ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
11059
11382
  LEFT JOIN "user" au ON au.id = ac.user_id
11060
11383
  LEFT JOIN person ap ON ap.id = ac.person_id
11384
+ LEFT JOIN LATERAL (
11385
+ SELECT COUNT(*) AS count
11386
+ FROM operations_task_comment tc
11387
+ WHERE tc.task_id = t.id
11388
+ ) task_comments ON TRUE
11061
11389
  WHERE COALESCE(t.project_id, (
11062
11390
  SELECT pa.project_id FROM operations_project_assignment pa
11063
11391
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -11121,7 +11449,9 @@ export class OperationsService {
11121
11449
  ];
11122
11450
 
11123
11451
  if (actor.collaboratorId) {
11124
- filters.push(`pa.collaborator_id = ${this.param(params, actor.collaboratorId)}`);
11452
+ const p1 = this.param(params, actor.collaboratorId);
11453
+ const p2 = this.param(params, actor.collaboratorId);
11454
+ filters.push(`(pa.collaborator_id = ${p1} OR t.assignee_collaborator_id = ${p2})`);
11125
11455
  }
11126
11456
 
11127
11457
  if (pagination.search) {
@@ -11178,6 +11508,7 @@ export class OperationsService {
11178
11508
  assigneeName: string | null;
11179
11509
  assigneeUserPhotoId: number | null;
11180
11510
  assigneePersonAvatarId: number | null;
11511
+ commentCount: number;
11181
11512
  createdAt: string;
11182
11513
  deletedAt: string | null;
11183
11514
  }>(
@@ -11196,6 +11527,7 @@ export class OperationsService {
11196
11527
  ac.display_name AS "assigneeName",
11197
11528
  au.photo_id AS "assigneeUserPhotoId",
11198
11529
  ap.avatar_id AS "assigneePersonAvatarId",
11530
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
11199
11531
  t.created_at AS "createdAt",
11200
11532
  t.deleted_at AS "deletedAt"
11201
11533
  FROM operations_task t
@@ -11208,6 +11540,11 @@ export class OperationsService {
11208
11540
  ON au.id = ac.user_id
11209
11541
  LEFT JOIN person ap
11210
11542
  ON ap.id = ac.person_id
11543
+ LEFT JOIN LATERAL (
11544
+ SELECT COUNT(*) AS count
11545
+ FROM operations_task_comment tc
11546
+ WHERE tc.task_id = t.id
11547
+ ) task_comments ON TRUE
11211
11548
  JOIN operations_project p
11212
11549
  ON p.id = COALESCE(t.project_id, pa.project_id)
11213
11550
  WHERE ${whereClause}
@@ -11442,7 +11779,16 @@ export class OperationsService {
11442
11779
  ? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
11443
11780
  : { revenue: 1, cost: 1, backlog: 1 };
11444
11781
 
11445
- const params: unknown[] = [from, to];
11782
+ const fromDate = new Date(`${from}T00:00:00`);
11783
+ const toDate = new Date(`${to}T00:00:00`);
11784
+ const periodDays = Math.max(
11785
+ 1,
11786
+ Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
11787
+ );
11788
+ const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
11789
+ const periodMonths = periodDays / 30.4375;
11790
+
11791
+ const params: unknown[] = [from, to, periodMonths, periodWeeks];
11446
11792
  const where = [
11447
11793
  'p.deleted_at IS NULL',
11448
11794
  '(p.end_date IS NULL OR p.end_date >= $1::date)',
@@ -11469,6 +11815,8 @@ export class OperationsService {
11469
11815
  weeklyHours: string | null;
11470
11816
  actualHours: string | null;
11471
11817
  billableHours: string | null;
11818
+ realizedCost: string | null;
11819
+ allocatedCost: string | null;
11472
11820
  openTasks: string | null;
11473
11821
  backlogHours: string | null;
11474
11822
  futureDeliveries: string | null;
@@ -11487,6 +11835,8 @@ export class OperationsService {
11487
11835
  COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
11488
11836
  COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
11489
11837
  COALESCE(time_stats.billable_hours, 0)::text AS "billableHours",
11838
+ COALESCE(cost_stats.realized_cost, 0)::text AS "realizedCost",
11839
+ COALESCE(alloc_cost_stats.allocated_cost, 0)::text AS "allocatedCost",
11490
11840
  COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
11491
11841
  COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
11492
11842
  COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
@@ -11515,6 +11865,146 @@ export class OperationsService {
11515
11865
  AND entry.deleted_at IS NULL
11516
11866
  AND entry.work_date BETWEEN $1::date AND $2::date
11517
11867
  ) time_stats ON TRUE
11868
+ LEFT JOIN LATERAL (
11869
+ SELECT COALESCE(
11870
+ SUM(
11871
+ entry.hours
11872
+ * (
11873
+ (
11874
+ COALESCE(collaborator_costs.salary_cost, 0)
11875
+ + COALESCE(collaborator_costs.benefits_cost, 0)
11876
+ + COALESCE(collaborator_costs.taxes_cost, 0)
11877
+ + COALESCE(collaborator_costs.tools_cost, 0)
11878
+ )
11879
+ * $3::numeric
11880
+ / GREATEST(
11881
+ COALESCE(collaborator_record.weekly_capacity_hours, 40)::numeric * $4::numeric,
11882
+ COALESCE(collaborator_hours.total_hours, 0),
11883
+ 1
11884
+ )
11885
+ )
11886
+ ),
11887
+ 0
11888
+ ) AS realized_cost
11889
+ FROM operations_timesheet_entry entry
11890
+ JOIN operations_project_assignment pa
11891
+ ON pa.id = entry.project_assignment_id
11892
+ AND pa.deleted_at IS NULL
11893
+ JOIN operations_collaborator collaborator_record
11894
+ ON collaborator_record.id = pa.collaborator_id
11895
+ AND collaborator_record.deleted_at IS NULL
11896
+ LEFT JOIN LATERAL (
11897
+ SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
11898
+ cost_totals.benefits_cost,
11899
+ cost_totals.taxes_cost,
11900
+ cost_totals.tools_cost
11901
+ FROM (
11902
+ SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
11903
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
11904
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
11905
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
11906
+ FROM operations_collaborator_cost cost
11907
+ LEFT JOIN operations_cost_type cost_type
11908
+ ON cost_type.id = cost.cost_type_id
11909
+ WHERE cost.collaborator_id = collaborator_record.id
11910
+ AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
11911
+ AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
11912
+ ) cost_totals
11913
+ LEFT JOIN LATERAL (
11914
+ SELECT h.amount
11915
+ FROM operations_collaborator_compensation_history h
11916
+ WHERE h.collaborator_id = collaborator_record.id
11917
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
11918
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
11919
+ LIMIT 1
11920
+ ) compensation_history ON TRUE
11921
+ LEFT JOIN LATERAL (
11922
+ SELECT oc.budget_amount
11923
+ FROM operations_contract oc
11924
+ WHERE oc.related_collaborator_id = collaborator_record.id
11925
+ AND oc.deleted_at IS NULL
11926
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
11927
+ oc.created_at DESC
11928
+ LIMIT 1
11929
+ ) hiring_contract ON TRUE
11930
+ ) collaborator_costs ON TRUE
11931
+ LEFT JOIN LATERAL (
11932
+ SELECT COALESCE(SUM(entry2.hours), 0) AS total_hours
11933
+ FROM operations_timesheet_entry entry2
11934
+ JOIN operations_project_assignment pa2
11935
+ ON pa2.id = entry2.project_assignment_id
11936
+ AND pa2.deleted_at IS NULL
11937
+ WHERE pa2.collaborator_id = collaborator_record.id
11938
+ AND entry2.deleted_at IS NULL
11939
+ AND entry2.work_date BETWEEN $1::date AND $2::date
11940
+ ) collaborator_hours ON TRUE
11941
+ WHERE pa.project_id = p.id
11942
+ AND entry.deleted_at IS NULL
11943
+ AND entry.work_date BETWEEN $1::date AND $2::date
11944
+ ) cost_stats ON TRUE
11945
+ LEFT JOIN LATERAL (
11946
+ SELECT COALESCE(
11947
+ SUM(
11948
+ pa.weekly_hours
11949
+ * (
11950
+ (
11951
+ COALESCE(alloc_costs.salary_cost, 0)
11952
+ + COALESCE(alloc_costs.benefits_cost, 0)
11953
+ + COALESCE(alloc_costs.taxes_cost, 0)
11954
+ + COALESCE(alloc_costs.tools_cost, 0)
11955
+ )
11956
+ * $3::numeric
11957
+ / GREATEST(
11958
+ COALESCE(alloc_col.weekly_capacity_hours, 40)::numeric,
11959
+ 1
11960
+ )
11961
+ )
11962
+ ),
11963
+ 0
11964
+ ) AS allocated_cost
11965
+ FROM operations_project_assignment pa
11966
+ JOIN operations_collaborator alloc_col
11967
+ ON alloc_col.id = pa.collaborator_id
11968
+ AND alloc_col.deleted_at IS NULL
11969
+ LEFT JOIN LATERAL (
11970
+ SELECT COALESCE(NULLIF(ct.salary_cost, 0), ch.amount, hc.budget_amount, 0) AS salary_cost,
11971
+ ct.benefits_cost,
11972
+ ct.taxes_cost,
11973
+ ct.tools_cost
11974
+ FROM (
11975
+ SELECT COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
11976
+ COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
11977
+ COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
11978
+ COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
11979
+ FROM operations_collaborator_cost c
11980
+ LEFT JOIN operations_cost_type ct2
11981
+ ON ct2.id = c.cost_type_id
11982
+ WHERE c.collaborator_id = alloc_col.id
11983
+ AND (c.start_date IS NULL OR c.start_date <= $2::date)
11984
+ AND (c.end_date IS NULL OR c.end_date >= $1::date)
11985
+ ) ct
11986
+ LEFT JOIN LATERAL (
11987
+ SELECT h.amount
11988
+ FROM operations_collaborator_compensation_history h
11989
+ WHERE h.collaborator_id = alloc_col.id
11990
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
11991
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
11992
+ LIMIT 1
11993
+ ) ch ON TRUE
11994
+ LEFT JOIN LATERAL (
11995
+ SELECT oc.budget_amount
11996
+ FROM operations_contract oc
11997
+ WHERE oc.related_collaborator_id = alloc_col.id
11998
+ AND oc.deleted_at IS NULL
11999
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
12000
+ oc.created_at DESC
12001
+ LIMIT 1
12002
+ ) hc ON TRUE
12003
+ ) alloc_costs ON TRUE
12004
+ WHERE pa.project_id = p.id
12005
+ AND pa.deleted_at IS NULL
12006
+ AND pa.status IN ('planned', 'active')
12007
+ ) alloc_cost_stats ON TRUE
11518
12008
  LEFT JOIN LATERAL (
11519
12009
  SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
11520
12010
  COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
@@ -11528,12 +12018,6 @@ export class OperationsService {
11528
12018
  params
11529
12019
  );
11530
12020
 
11531
- const fromDate = new Date(`${from}T00:00:00`);
11532
- const toDate = new Date(`${to}T00:00:00`);
11533
- const periodWeeks = Math.max(
11534
- 1,
11535
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11536
- );
11537
12021
  const rows = dbRows
11538
12022
  .map((row) => {
11539
12023
  const progress = Number(row.progressPercent ?? 0);
@@ -11541,7 +12025,12 @@ export class OperationsService {
11541
12025
  const recognizedRevenue = contractedRevenue * (progress / 100);
11542
12026
  const actualHours = Number(row.actualHours ?? 0);
11543
12027
  const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
11544
- const realizedCost = 0;
12028
+ const realizedCost = Number(row.realizedCost ?? 0);
12029
+ const allocatedCost = Number(row.allocatedCost ?? 0);
12030
+ const consumedHoursCost = realizedCost;
12031
+ const idlenessHours = Math.max(plannedHours - actualHours, 0);
12032
+ const idlenessRate = plannedHours > 0 ? (idlenessHours / plannedHours) * 100 : 0;
12033
+ const idlenessCost = Math.max(allocatedCost - consumedHoursCost, 0);
11545
12034
  const reportStatus =
11546
12035
  row.status === 'paused'
11547
12036
  ? 'paused'
@@ -11576,7 +12065,7 @@ export class OperationsService {
11576
12065
  contractedRevenue,
11577
12066
  recognizedRevenue,
11578
12067
  realizedCost,
11579
- forecastCost: realizedCost,
12068
+ forecastCost: realizedCost * multiplier.cost,
11580
12069
  teamCost: realizedCost,
11581
12070
  infraCost: 0,
11582
12071
  licenseCost: 0,
@@ -11592,6 +12081,10 @@ export class OperationsService {
11592
12081
  financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
11593
12082
  backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
11594
12083
  futureDeliveries: Number(row.futureDeliveries ?? 0),
12084
+ allocatedCost,
12085
+ consumedHoursCost,
12086
+ idlenessRate,
12087
+ idlenessCost,
11595
12088
  risk,
11596
12089
  recommendation:
11597
12090
  risk === 'alto'
@@ -11617,6 +12110,9 @@ export class OperationsService {
11617
12110
  acc.avgDeadline += row.physicalProgress;
11618
12111
  acc.avgAllocation += row.allocatedCapacity;
11619
12112
  acc.atRisk += row.risk === 'alto' ? 1 : 0;
12113
+ acc.allocatedCost += row.allocatedCost;
12114
+ acc.consumedHoursCost += row.consumedHoursCost;
12115
+ acc.idlenessCost += row.idlenessCost;
11620
12116
  return acc;
11621
12117
  },
11622
12118
  {
@@ -11635,6 +12131,11 @@ export class OperationsService {
11635
12131
  avgAllocation: 0,
11636
12132
  atRisk: 0,
11637
12133
  burnRate: 0,
12134
+ allocatedCost: 0,
12135
+ consumedHoursCost: 0,
12136
+ idlenessCost: 0,
12137
+ idlenessRate: 0,
12138
+ plannedProfit: 0,
11638
12139
  }
11639
12140
  );
11640
12141
  summary.profit = summary.recognizedRevenue - summary.realizedCost;
@@ -11642,6 +12143,10 @@ export class OperationsService {
11642
12143
  summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
11643
12144
  summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
11644
12145
  summary.burnRate = summary.plannedHours ? (summary.actualHours / summary.plannedHours) * 100 : 0;
12146
+ summary.plannedProfit = summary.contractedRevenue - summary.allocatedCost;
12147
+ summary.idlenessRate = summary.plannedHours > 0
12148
+ ? Math.max(0, (summary.plannedHours - summary.actualHours) / summary.plannedHours * 100)
12149
+ : 0;
11645
12150
 
11646
12151
  const forecast = Array.from({ length: 12 }, (_, index) => {
11647
12152
  const monthDate = new Date(fromDate);
@@ -11744,6 +12249,15 @@ export class OperationsService {
11744
12249
  : scenario === 'conservative'
11745
12250
  ? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
11746
12251
  : { revenue: 1, cost: 1, capacity: 1 };
12252
+ const fromDate = new Date(`${from}T00:00:00`);
12253
+ const toDate = new Date(`${to}T00:00:00`);
12254
+ const periodDays = Math.max(
12255
+ 1,
12256
+ Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
12257
+ );
12258
+ const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
12259
+ const periodMonths = periodDays / 30.4375;
12260
+
11747
12261
  const params: unknown[] = [from, to];
11748
12262
  const where = [
11749
12263
  'c.deleted_at IS NULL',
@@ -11774,6 +12288,11 @@ export class OperationsService {
11774
12288
  taxesCost: string | null;
11775
12289
  toolsCost: string | null;
11776
12290
  billableValue: string | null;
12291
+ plannedAllocatedHours: string | null;
12292
+ plannedBillableHours: string | null;
12293
+ openTaskHours: string | null;
12294
+ openTaskBillableHours: string | null;
12295
+ openTasks: string | null;
11777
12296
  allocatedHours: string | null;
11778
12297
  billableHours: string | null;
11779
12298
  projects: string | null;
@@ -11792,9 +12311,14 @@ export class OperationsService {
11792
12311
  COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
11793
12312
  COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
11794
12313
  COALESCE(value_stats.billable_value, 0)::text AS "billableValue",
12314
+ COALESCE(assignment_stats.planned_allocated_hours, 0)::text AS "plannedAllocatedHours",
12315
+ COALESCE(assignment_stats.planned_billable_hours, 0)::text AS "plannedBillableHours",
12316
+ COALESCE(task_stats.open_task_hours, 0)::text AS "openTaskHours",
12317
+ COALESCE(task_stats.open_task_billable_hours, 0)::text AS "openTaskBillableHours",
12318
+ COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
11795
12319
  COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
11796
12320
  COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
11797
- COALESCE(project_stats.projects, 0)::text AS projects
12321
+ COALESCE(assignment_stats.projects, 0)::text AS projects
11798
12322
  FROM operations_collaborator c
11799
12323
  LEFT JOIN person person_record ON person_record.id = c.person_id
11800
12324
  LEFT JOIN operations_department department_record
@@ -11807,16 +12331,85 @@ export class OperationsService {
11807
12331
  ON collaborator_type.id = c.collaborator_type_id
11808
12332
  AND collaborator_type.deleted_at IS NULL
11809
12333
  LEFT JOIN LATERAL (
11810
- SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
11811
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
11812
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
11813
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
11814
- FROM operations_collaborator_cost cost
11815
- LEFT JOIN operations_cost_type cost_type ON cost_type.id = cost.cost_type_id
11816
- WHERE cost.collaborator_id = c.id
11817
- AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
11818
- AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
12334
+ SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
12335
+ cost_totals.benefits_cost,
12336
+ cost_totals.taxes_cost,
12337
+ cost_totals.tools_cost
12338
+ FROM (
12339
+ SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
12340
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
12341
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
12342
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
12343
+ FROM operations_collaborator_cost cost
12344
+ LEFT JOIN operations_cost_type cost_type
12345
+ ON cost_type.id = cost.cost_type_id
12346
+ WHERE cost.collaborator_id = c.id
12347
+ AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
12348
+ AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
12349
+ ) cost_totals
12350
+ LEFT JOIN LATERAL (
12351
+ SELECT h.amount
12352
+ FROM operations_collaborator_compensation_history h
12353
+ WHERE h.collaborator_id = c.id
12354
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
12355
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
12356
+ LIMIT 1
12357
+ ) compensation_history ON TRUE
12358
+ LEFT JOIN LATERAL (
12359
+ SELECT oc.budget_amount
12360
+ FROM operations_contract oc
12361
+ WHERE oc.related_collaborator_id = c.id
12362
+ AND oc.deleted_at IS NULL
12363
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
12364
+ oc.created_at DESC
12365
+ LIMIT 1
12366
+ ) hiring_contract ON TRUE
11819
12367
  ) cost_stats ON TRUE
12368
+ LEFT JOIN LATERAL (
12369
+ SELECT COALESCE(
12370
+ SUM(
12371
+ COALESCE(
12372
+ pa.weekly_hours,
12373
+ COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
12374
+ ) * GREATEST(
12375
+ CEIL(
12376
+ (
12377
+ LEAST(COALESCE(pa.end_date, $2::date), $2::date)
12378
+ - GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
12379
+ + 1
12380
+ ) / 7.0
12381
+ ),
12382
+ 0
12383
+ )
12384
+ ),
12385
+ 0
12386
+ ) AS planned_allocated_hours,
12387
+ COALESCE(
12388
+ SUM(
12389
+ COALESCE(
12390
+ pa.weekly_hours,
12391
+ COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
12392
+ ) * GREATEST(
12393
+ CEIL(
12394
+ (
12395
+ LEAST(COALESCE(pa.end_date, $2::date), $2::date)
12396
+ - GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
12397
+ + 1
12398
+ ) / 7.0
12399
+ ),
12400
+ 0
12401
+ )
12402
+ ) FILTER (WHERE pa.is_billable = true),
12403
+ 0
12404
+ ) AS planned_billable_hours,
12405
+ COUNT(DISTINCT pa.project_id) AS projects
12406
+ FROM operations_project_assignment pa
12407
+ WHERE pa.collaborator_id = c.id
12408
+ AND pa.deleted_at IS NULL
12409
+ AND pa.status IN ('planned', 'active')
12410
+ AND (pa.start_date IS NULL OR pa.start_date <= $2::date)
12411
+ AND (pa.end_date IS NULL OR pa.end_date >= $1::date)
12412
+ ) assignment_stats ON TRUE
11820
12413
  LEFT JOIN LATERAL (
11821
12414
  SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
11822
12415
  COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
@@ -11830,31 +12423,50 @@ export class OperationsService {
11830
12423
  AND entry.work_date BETWEEN $1::date AND $2::date
11831
12424
  ) value_stats ON TRUE
11832
12425
  LEFT JOIN LATERAL (
11833
- SELECT COUNT(DISTINCT pa.project_id) AS projects
11834
- FROM operations_project_assignment pa
11835
- WHERE pa.collaborator_id = c.id
12426
+ SELECT COUNT(*) AS open_tasks,
12427
+ COALESCE(SUM(COALESCE(task.estimate_hours, 0)), 0) AS open_task_hours,
12428
+ COALESCE(
12429
+ SUM(COALESCE(task.estimate_hours, 0)) FILTER (WHERE pa.is_billable = true),
12430
+ 0
12431
+ ) AS open_task_billable_hours
12432
+ FROM operations_task task
12433
+ LEFT JOIN operations_project_assignment pa
12434
+ ON pa.id = task.project_assignment_id
11836
12435
  AND pa.deleted_at IS NULL
11837
- AND pa.status IN ('planned', 'active')
11838
- ) project_stats ON TRUE
12436
+ WHERE task.deleted_at IS NULL
12437
+ AND task.status IN ('todo', 'doing', 'review')
12438
+ AND (
12439
+ task.assignee_collaborator_id = c.id
12440
+ OR pa.collaborator_id = c.id
12441
+ )
12442
+ ) task_stats ON TRUE
11839
12443
  WHERE ${where.join(' AND ')}
11840
12444
  ORDER BY name ASC`,
11841
12445
  params
11842
12446
  );
11843
12447
 
11844
- const fromDate = new Date(`${from}T00:00:00`);
11845
- const toDate = new Date(`${to}T00:00:00`);
11846
- const periodWeeks = Math.max(
11847
- 1,
11848
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11849
- );
11850
12448
  const rows = dbRows.map((row) => {
11851
- const salaryCost = Number(row.salaryCost ?? 0);
11852
- const benefitsCost = Number(row.benefitsCost ?? 0);
11853
- const taxesCost = Number(row.taxesCost ?? 0);
11854
- const toolsCost = Number(row.toolsCost ?? 0);
12449
+ const salaryCost = Number(row.salaryCost ?? 0) * periodMonths;
12450
+ const benefitsCost = Number(row.benefitsCost ?? 0) * periodMonths;
12451
+ const taxesCost = Number(row.taxesCost ?? 0) * periodMonths;
12452
+ const toolsCost = Number(row.toolsCost ?? 0) * periodMonths;
11855
12453
  const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
11856
- const allocatedHours = Number(row.allocatedHours ?? 0);
11857
- const billableHours = Number(row.billableHours ?? 0);
12454
+ const plannedAllocatedHours = Number(row.plannedAllocatedHours ?? 0);
12455
+ const plannedBillableHours = Number(row.plannedBillableHours ?? 0);
12456
+ const openTaskHours = Number(row.openTaskHours ?? 0);
12457
+ const openTaskBillableHours = Number(row.openTaskBillableHours ?? 0);
12458
+ const actualAllocatedHours = Number(row.allocatedHours ?? 0);
12459
+ const actualBillableHours = Number(row.billableHours ?? 0);
12460
+ const allocatedHours = Math.max(
12461
+ actualAllocatedHours,
12462
+ plannedAllocatedHours,
12463
+ openTaskHours
12464
+ );
12465
+ const billableHours = Math.max(
12466
+ actualBillableHours,
12467
+ plannedBillableHours,
12468
+ openTaskBillableHours
12469
+ );
11858
12470
  const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
11859
12471
  const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
11860
12472
  return {
@@ -11929,7 +12541,11 @@ export class OperationsService {
11929
12541
  summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
11930
12542
  summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
11931
12543
  summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
11932
- summary.hourlyCost = summary.allocatedHours ? summary.cost / summary.allocatedHours : 0;
12544
+ summary.hourlyCost = summary.allocatedHours
12545
+ ? summary.cost / summary.allocatedHours
12546
+ : summary.availableHours
12547
+ ? summary.cost / summary.availableHours
12548
+ : 0;
11933
12549
 
11934
12550
  const forecast = Array.from({ length: 12 }, (_, index) => {
11935
12551
  const monthDate = new Date(fromDate);
@@ -12173,4 +12789,1623 @@ export class OperationsService {
12173
12789
 
12174
12790
  return { success: true };
12175
12791
  }
12792
+
12793
+ // ──────────────────────────────────────────────────────────────────────────
12794
+ // Project Cost Categories
12795
+ // ──────────────────────────────────────────────────────────────────────────
12796
+
12797
+ async listProjectCostCategories(userId: number, filters: { search?: string; is_active?: boolean; pageSize?: number; page?: number } = {}) {
12798
+ await this.getActorContext(userId);
12799
+ const localeId = await this.resolvePreferredLocaleId();
12800
+
12801
+ const params: unknown[] = [localeId];
12802
+ const where: string[] = ['pcc.deleted_at IS NULL'];
12803
+
12804
+ if (filters.is_active === true) {
12805
+ where.push('pcc.is_active = true');
12806
+ }
12807
+
12808
+ if (filters.search?.trim()) {
12809
+ const p = this.param(params, `%${filters.search.trim()}%`);
12810
+ where.push(`(COALESCE(pccl.name, pcc.slug) ILIKE ${p} OR COALESCE(pcc.slug, '') ILIKE ${p})`);
12811
+ }
12812
+
12813
+ const whereClause = `WHERE ${where.join(' AND ')}`;
12814
+
12815
+ return this.queryRows<{
12816
+ id: number;
12817
+ slug: string;
12818
+ name: string | null;
12819
+ description: string | null;
12820
+ icon: string | null;
12821
+ color: string | null;
12822
+ isActive: boolean;
12823
+ sortOrder: number;
12824
+ createdAt: string;
12825
+ }>(
12826
+ `SELECT pcc.id,
12827
+ pcc.slug,
12828
+ COALESCE(pccl.name, pcc.slug) AS name,
12829
+ pccl.description,
12830
+ pcc.icon,
12831
+ pcc.color,
12832
+ pcc.is_active AS "isActive",
12833
+ pcc.sort_order AS "sortOrder",
12834
+ pcc.created_at AS "createdAt"
12835
+ FROM operations_project_cost_category pcc
12836
+ LEFT JOIN LATERAL (
12837
+ SELECT l.name, l.description
12838
+ FROM operations_project_cost_category_locale l
12839
+ WHERE l.operations_project_cost_category_id = pcc.id
12840
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
12841
+ l.id ASC
12842
+ LIMIT 1
12843
+ ) pccl ON TRUE
12844
+ ${whereClause}
12845
+ ORDER BY pcc.sort_order ASC, COALESCE(pccl.name, pcc.slug) ASC`,
12846
+ params
12847
+ );
12848
+ }
12849
+
12850
+ async createProjectCostCategory(userId: number, data: { slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }) {
12851
+ const actor = await this.getActorContext(userId);
12852
+ this.ensureDirector(actor);
12853
+
12854
+ const slug = data.slug?.trim();
12855
+ if (!slug) {
12856
+ throw new BadRequestException('Cost category slug is required.');
12857
+ }
12858
+
12859
+ return this.prisma.$transaction(async (tx) => {
12860
+ const localeId = await this.resolvePreferredLocaleId(tx as any);
12861
+
12862
+ const created = (await (tx as any).$queryRawUnsafe(
12863
+ `INSERT INTO operations_project_cost_category (slug, icon, color, is_active, sort_order, created_at, updated_at)
12864
+ VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
12865
+ RETURNING id`,
12866
+ slug,
12867
+ data.icon ?? null,
12868
+ data.color ?? null,
12869
+ data.is_active ?? true,
12870
+ data.sort_order ?? 0,
12871
+ )) as { id: number }[];
12872
+
12873
+ const createdId = created[0]?.id;
12874
+ if (!createdId) {
12875
+ throw new BadRequestException('Unable to create project cost category.');
12876
+ }
12877
+
12878
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
12879
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
12880
+
12881
+ if (localeId && name) {
12882
+ await (tx as any).$executeRawUnsafe(
12883
+ `INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description)
12884
+ VALUES ($1, $2, $3, $4)`,
12885
+ createdId,
12886
+ localeId,
12887
+ name,
12888
+ description ?? null,
12889
+ );
12890
+ }
12891
+
12892
+ const rows = (await (tx as any).$queryRawUnsafe(
12893
+ `SELECT pcc.id,
12894
+ pcc.slug,
12895
+ COALESCE(pccl.name, pcc.slug) AS name,
12896
+ pccl.description,
12897
+ pcc.icon,
12898
+ pcc.color,
12899
+ pcc.is_active AS "isActive",
12900
+ pcc.sort_order AS "sortOrder",
12901
+ pcc.created_at AS "createdAt"
12902
+ FROM operations_project_cost_category pcc
12903
+ LEFT JOIN operations_project_cost_category_locale pccl
12904
+ ON pccl.operations_project_cost_category_id = pcc.id AND pccl.locale_id = $2
12905
+ WHERE pcc.id = $1`,
12906
+ createdId,
12907
+ localeId,
12908
+ )) as { id: number; slug: string; name: string; description: string | null; icon: string | null; color: string | null; isActive: boolean; sortOrder: number; createdAt: string }[];
12909
+
12910
+ return rows[0] ?? null;
12911
+ });
12912
+ }
12913
+
12914
+ async updateProjectCostCategory(userId: number, id: number, data: Partial<{ slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }>) {
12915
+ const actor = await this.getActorContext(userId);
12916
+ this.ensureDirector(actor);
12917
+
12918
+ const category = await this.querySingle<{ id: number }>(
12919
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
12920
+ [id]
12921
+ );
12922
+ if (!category) {
12923
+ throw new NotFoundException('Project cost category not found.');
12924
+ }
12925
+
12926
+ const sets: string[] = [];
12927
+ const params: unknown[] = [];
12928
+
12929
+ if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
12930
+ if (data.icon !== undefined) sets.push(`icon = ${this.param(params, data.icon)}`);
12931
+ if (data.color !== undefined) sets.push(`color = ${this.param(params, data.color)}`);
12932
+ if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
12933
+ if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
12934
+
12935
+ if (sets.length > 0) {
12936
+ sets.push(`updated_at = NOW()`);
12937
+ await this.prisma.$queryRawUnsafe(
12938
+ `UPDATE operations_project_cost_category SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
12939
+ ...params
12940
+ );
12941
+ }
12942
+
12943
+ if (data.name !== undefined || data.description !== undefined) {
12944
+ const localeId = await this.resolvePreferredLocaleId();
12945
+ if (localeId) {
12946
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
12947
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
12948
+ const existing = await this.querySingle<{ id: number }>(
12949
+ `SELECT id FROM operations_project_cost_category_locale WHERE operations_project_cost_category_id = $1 AND locale_id = $2 LIMIT 1`,
12950
+ [id, localeId]
12951
+ );
12952
+ if (existing) {
12953
+ const localeSets: string[] = [];
12954
+ const localeParams: unknown[] = [];
12955
+ if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
12956
+ if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
12957
+ if (localeSets.length > 0) {
12958
+ await this.prisma.$queryRawUnsafe(
12959
+ `UPDATE operations_project_cost_category_locale SET ${localeSets.join(', ')} WHERE operations_project_cost_category_id = ${this.param(localeParams, id)} AND locale_id = ${this.param(localeParams, localeId)}`,
12960
+ ...localeParams
12961
+ );
12962
+ }
12963
+ } else if (name) {
12964
+ await this.prisma.$queryRawUnsafe(
12965
+ `INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
12966
+ id, localeId, name, description ?? null
12967
+ );
12968
+ }
12969
+ }
12970
+ }
12971
+
12972
+ return this.querySingle<{ id: number; slug: string }>(
12973
+ `SELECT id, slug FROM operations_project_cost_category WHERE id = $1`,
12974
+ [id]
12975
+ );
12976
+ }
12977
+
12978
+ async deleteProjectCostCategory(userId: number, id: number) {
12979
+ const actor = await this.getActorContext(userId);
12980
+ this.ensureDirector(actor);
12981
+
12982
+ const category = await this.querySingle<{ id: number }>(
12983
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
12984
+ [id]
12985
+ );
12986
+ if (!category) {
12987
+ throw new NotFoundException('Project cost category not found.');
12988
+ }
12989
+
12990
+ await this.prisma.$queryRawUnsafe(
12991
+ `UPDATE operations_project_cost_category SET deleted_at = NOW() WHERE id = $1`,
12992
+ id
12993
+ );
12994
+
12995
+ return { success: true };
12996
+ }
12997
+
12998
+ async getProjectCostCategory(userId: number, id: number) {
12999
+ await this.getActorContext(userId);
13000
+ const localeId = await this.resolvePreferredLocaleId();
13001
+
13002
+ const row = await this.querySingle<{
13003
+ id: number;
13004
+ slug: string;
13005
+ name: string | null;
13006
+ description: string | null;
13007
+ icon: string | null;
13008
+ color: string | null;
13009
+ isActive: boolean;
13010
+ sortOrder: number;
13011
+ createdAt: string;
13012
+ }>(
13013
+ `SELECT pcc.id,
13014
+ pcc.slug,
13015
+ COALESCE(pccl.name, pcc.slug) AS name,
13016
+ pccl.description,
13017
+ pcc.icon,
13018
+ pcc.color,
13019
+ pcc.is_active AS "isActive",
13020
+ pcc.sort_order AS "sortOrder",
13021
+ pcc.created_at AS "createdAt"
13022
+ FROM operations_project_cost_category pcc
13023
+ LEFT JOIN LATERAL (
13024
+ SELECT l.name, l.description
13025
+ FROM operations_project_cost_category_locale l
13026
+ WHERE l.operations_project_cost_category_id = pcc.id
13027
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13028
+ l.id ASC
13029
+ LIMIT 1
13030
+ ) pccl ON TRUE
13031
+ WHERE pcc.id = $2 AND pcc.deleted_at IS NULL`,
13032
+ [localeId, id]
13033
+ );
13034
+
13035
+ if (!row) {
13036
+ throw new NotFoundException('Project cost category not found.');
13037
+ }
13038
+
13039
+ return row;
13040
+ }
13041
+
13042
+ // ──────────────────────────────────────────────────────────────────────────
13043
+ // Project Cost Types
13044
+ // ──────────────────────────────────────────────────────────────────────────
13045
+
13046
+ async listProjectCostTypes(userId: number, filters: { search?: string; category_id?: number; is_active?: boolean; default_calculation_type?: string; pageSize?: number; page?: number } = {}) {
13047
+ await this.getActorContext(userId);
13048
+ const localeId = await this.resolvePreferredLocaleId();
13049
+
13050
+ const params: unknown[] = [localeId];
13051
+ const where: string[] = ['pct.deleted_at IS NULL'];
13052
+
13053
+ if (filters.is_active === true) {
13054
+ where.push('pct.is_active = true');
13055
+ }
13056
+
13057
+ if (filters.category_id) {
13058
+ where.push(`pct.category_id = ${this.param(params, filters.category_id)}`);
13059
+ }
13060
+
13061
+ if (filters.default_calculation_type) {
13062
+ where.push(`pct.default_calculation_type = ${this.param(params, filters.default_calculation_type)}`);
13063
+ }
13064
+
13065
+ if (filters.search?.trim()) {
13066
+ const p = this.param(params, `%${filters.search.trim()}%`);
13067
+ where.push(`(COALESCE(pctl.name, pct.slug) ILIKE ${p} OR COALESCE(pct.code, '') ILIKE ${p} OR COALESCE(pct.slug, '') ILIKE ${p})`);
13068
+ }
13069
+
13070
+ const whereClause = `WHERE ${where.join(' AND ')}`;
13071
+
13072
+ return this.queryRows<{
13073
+ id: number;
13074
+ slug: string;
13075
+ code: string;
13076
+ name: string | null;
13077
+ description: string | null;
13078
+ categoryId: number | null;
13079
+ categorySlug: string | null;
13080
+ categoryName: string | null;
13081
+ defaultUnit: string | null;
13082
+ defaultCalculationType: string | null;
13083
+ isRecurringAllowed: boolean;
13084
+ isActive: boolean;
13085
+ sortOrder: number;
13086
+ createdAt: string;
13087
+ }>(
13088
+ `SELECT pct.id,
13089
+ pct.slug,
13090
+ pct.code,
13091
+ COALESCE(pctl.name, pct.slug) AS name,
13092
+ pctl.description,
13093
+ pct.category_id AS "categoryId",
13094
+ pcc.slug AS "categorySlug",
13095
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13096
+ pct.default_unit AS "defaultUnit",
13097
+ pct.default_calculation_type AS "defaultCalculationType",
13098
+ pct.is_recurring_allowed AS "isRecurringAllowed",
13099
+ pct.is_active AS "isActive",
13100
+ pct.sort_order AS "sortOrder",
13101
+ pct.created_at AS "createdAt"
13102
+ FROM operations_project_cost_type pct
13103
+ LEFT JOIN operations_project_cost_category pcc
13104
+ ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
13105
+ LEFT JOIN LATERAL (
13106
+ SELECT l.name, l.description
13107
+ FROM operations_project_cost_category_locale l
13108
+ WHERE l.operations_project_cost_category_id = pcc.id
13109
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13110
+ l.id ASC
13111
+ LIMIT 1
13112
+ ) pccl ON TRUE
13113
+ LEFT JOIN LATERAL (
13114
+ SELECT l.name, l.description
13115
+ FROM operations_project_cost_type_locale l
13116
+ WHERE l.operations_project_cost_type_id = pct.id
13117
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13118
+ l.id ASC
13119
+ LIMIT 1
13120
+ ) pctl ON TRUE
13121
+ ${whereClause}
13122
+ ORDER BY pct.sort_order ASC, COALESCE(pctl.name, pct.slug) ASC`,
13123
+ params
13124
+ );
13125
+ }
13126
+
13127
+ async createProjectCostType(userId: number, data: { category_id?: number; slug: string; code: string; name?: any; description?: any; default_unit?: string | null; default_calculation_type?: string | null; is_recurring_allowed?: boolean; is_active?: boolean; sort_order?: number }) {
13128
+ const actor = await this.getActorContext(userId);
13129
+ this.ensureDirector(actor);
13130
+
13131
+ const slug = data.slug?.trim();
13132
+ if (!slug) {
13133
+ throw new BadRequestException('Cost type slug is required.');
13134
+ }
13135
+
13136
+ if (data.category_id) {
13137
+ const category = await this.querySingle<{ id: number }>(
13138
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13139
+ [data.category_id]
13140
+ );
13141
+ if (!category) {
13142
+ throw new BadRequestException(`Category with id ${data.category_id} not found.`);
13143
+ }
13144
+ }
13145
+
13146
+ const existingSlug = await this.querySingle<{ id: number }>(
13147
+ `SELECT id FROM operations_project_cost_type WHERE slug = $1 AND deleted_at IS NULL LIMIT 1`,
13148
+ [slug]
13149
+ );
13150
+ if (existingSlug) {
13151
+ throw new ConflictException(`A cost type with slug '${slug}' already exists.`);
13152
+ }
13153
+
13154
+ const code = data.code?.trim() ?? slug;
13155
+ const existingCode = await this.querySingle<{ id: number }>(
13156
+ `SELECT id FROM operations_project_cost_type WHERE code = $1 AND deleted_at IS NULL LIMIT 1`,
13157
+ [code]
13158
+ );
13159
+ if (existingCode) {
13160
+ throw new ConflictException(`A cost type with code '${code}' already exists.`);
13161
+ }
13162
+
13163
+ return this.prisma.$transaction(async (tx) => {
13164
+ const localeId = await this.resolvePreferredLocaleId(tx as any);
13165
+
13166
+ const created = (await (tx as any).$queryRawUnsafe(
13167
+ `INSERT INTO operations_project_cost_type (category_id, slug, code, default_unit, default_calculation_type, is_recurring_allowed, is_active, sort_order, created_at, updated_at)
13168
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
13169
+ RETURNING id`,
13170
+ data.category_id ?? null,
13171
+ slug,
13172
+ data.code?.trim() ?? slug,
13173
+ data.default_unit ?? null,
13174
+ data.default_calculation_type ?? 'fixed',
13175
+ data.is_recurring_allowed ?? true,
13176
+ data.is_active ?? true,
13177
+ data.sort_order ?? 0,
13178
+ )) as { id: number }[];
13179
+
13180
+ const createdId = created[0]?.id;
13181
+ if (!createdId) {
13182
+ throw new BadRequestException('Unable to create project cost type.');
13183
+ }
13184
+
13185
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
13186
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13187
+
13188
+ if (localeId && name) {
13189
+ await (tx as any).$executeRawUnsafe(
13190
+ `INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description)
13191
+ VALUES ($1, $2, $3, $4)`,
13192
+ createdId,
13193
+ localeId,
13194
+ name,
13195
+ description ?? null,
13196
+ );
13197
+ }
13198
+
13199
+ const rows = (await (tx as any).$queryRawUnsafe(
13200
+ `SELECT pct.id,
13201
+ pct.slug,
13202
+ pct.code,
13203
+ COALESCE(pctl.name, pct.slug) AS name,
13204
+ pctl.description,
13205
+ pct.category_id AS "categoryId",
13206
+ pct.default_unit AS "defaultUnit",
13207
+ pct.default_calculation_type AS "defaultCalculationType",
13208
+ pct.is_recurring_allowed AS "isRecurringAllowed",
13209
+ pct.is_active AS "isActive",
13210
+ pct.sort_order AS "sortOrder",
13211
+ pct.created_at AS "createdAt"
13212
+ FROM operations_project_cost_type pct
13213
+ LEFT JOIN operations_project_cost_type_locale pctl
13214
+ ON pctl.operations_project_cost_type_id = pct.id AND pctl.locale_id = $2
13215
+ WHERE pct.id = $1`,
13216
+ createdId,
13217
+ localeId,
13218
+ )) as any[];
13219
+
13220
+ return rows[0] ?? null;
13221
+ });
13222
+ }
13223
+
13224
+ async updateProjectCostType(userId: number, id: number, data: Partial<{ category_id: number; slug: string; code: string; name?: any; description?: any; default_unit?: string | null; default_calculation_type?: string | null; is_recurring_allowed?: boolean; is_active?: boolean; sort_order?: number }>) {
13225
+ const actor = await this.getActorContext(userId);
13226
+ this.ensureDirector(actor);
13227
+
13228
+ const costType = await this.querySingle<{ id: number }>(
13229
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13230
+ [id]
13231
+ );
13232
+ if (!costType) {
13233
+ throw new NotFoundException('Project cost type not found.');
13234
+ }
13235
+
13236
+ if (data.category_id !== undefined && data.category_id !== null) {
13237
+ const category = await this.querySingle<{ id: number }>(
13238
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13239
+ [data.category_id]
13240
+ );
13241
+ if (!category) {
13242
+ throw new BadRequestException(`Category with id ${data.category_id} not found.`);
13243
+ }
13244
+ }
13245
+
13246
+ if (data.slug !== undefined) {
13247
+ const existingSlug = await this.querySingle<{ id: number }>(
13248
+ `SELECT id FROM operations_project_cost_type WHERE slug = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
13249
+ [data.slug, id]
13250
+ );
13251
+ if (existingSlug) {
13252
+ throw new ConflictException(`A cost type with slug '${data.slug}' already exists.`);
13253
+ }
13254
+ }
13255
+
13256
+ if (data.code !== undefined) {
13257
+ const existingCode = await this.querySingle<{ id: number }>(
13258
+ `SELECT id FROM operations_project_cost_type WHERE code = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
13259
+ [data.code, id]
13260
+ );
13261
+ if (existingCode) {
13262
+ throw new ConflictException(`A cost type with code '${data.code}' already exists.`);
13263
+ }
13264
+ }
13265
+
13266
+ const sets: string[] = [];
13267
+ const params: unknown[] = [];
13268
+
13269
+ if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
13270
+ if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
13271
+ if (data.code !== undefined) sets.push(`code = ${this.param(params, data.code)}`);
13272
+ if (data.default_unit !== undefined) sets.push(`default_unit = ${this.param(params, data.default_unit)}`);
13273
+ if (data.default_calculation_type !== undefined) sets.push(`default_calculation_type = ${this.param(params, data.default_calculation_type)}`);
13274
+ if (data.is_recurring_allowed !== undefined) sets.push(`is_recurring_allowed = ${this.param(params, data.is_recurring_allowed)}`);
13275
+ if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
13276
+ if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
13277
+
13278
+ if (sets.length > 0) {
13279
+ sets.push(`updated_at = NOW()`);
13280
+ await this.prisma.$queryRawUnsafe(
13281
+ `UPDATE operations_project_cost_type SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
13282
+ ...params
13283
+ );
13284
+ }
13285
+
13286
+ if (data.name !== undefined || data.description !== undefined) {
13287
+ const localeId = await this.resolvePreferredLocaleId();
13288
+ if (localeId) {
13289
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
13290
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13291
+ const existing = await this.querySingle<{ id: number }>(
13292
+ `SELECT id FROM operations_project_cost_type_locale WHERE operations_project_cost_type_id = $1 AND locale_id = $2 LIMIT 1`,
13293
+ [id, localeId]
13294
+ );
13295
+ if (existing) {
13296
+ const localeSets: string[] = [];
13297
+ const localeParams: unknown[] = [];
13298
+ if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
13299
+ if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
13300
+ if (localeSets.length > 0) {
13301
+ await this.prisma.$queryRawUnsafe(
13302
+ `UPDATE operations_project_cost_type_locale SET ${localeSets.join(', ')} WHERE operations_project_cost_type_id = ${this.param(localeParams, id)} AND locale_id = ${this.param(localeParams, localeId)}`,
13303
+ ...localeParams
13304
+ );
13305
+ }
13306
+ } else if (name) {
13307
+ await this.prisma.$queryRawUnsafe(
13308
+ `INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
13309
+ id, localeId, name, description ?? null
13310
+ );
13311
+ }
13312
+ }
13313
+ }
13314
+
13315
+ return this.querySingle<{ id: number; slug: string }>(
13316
+ `SELECT id, slug FROM operations_project_cost_type WHERE id = $1`,
13317
+ [id]
13318
+ );
13319
+ }
13320
+
13321
+ async deleteProjectCostType(userId: number, id: number) {
13322
+ const actor = await this.getActorContext(userId);
13323
+ this.ensureDirector(actor);
13324
+
13325
+ const costType = await this.querySingle<{ id: number }>(
13326
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13327
+ [id]
13328
+ );
13329
+ if (!costType) {
13330
+ throw new NotFoundException('Project cost type not found.');
13331
+ }
13332
+
13333
+ await this.prisma.$queryRawUnsafe(
13334
+ `UPDATE operations_project_cost_type SET deleted_at = NOW() WHERE id = $1`,
13335
+ id
13336
+ );
13337
+
13338
+ return { success: true };
13339
+ }
13340
+
13341
+ async getProjectCostType(userId: number, id: number) {
13342
+ await this.getActorContext(userId);
13343
+ const localeId = await this.resolvePreferredLocaleId();
13344
+
13345
+ const row = await this.querySingle<{
13346
+ id: number;
13347
+ slug: string;
13348
+ code: string;
13349
+ name: string | null;
13350
+ description: string | null;
13351
+ default_unit: string | null;
13352
+ default_calculation_type: string | null;
13353
+ is_recurring_allowed: boolean;
13354
+ is_active: boolean;
13355
+ sort_order: number;
13356
+ category_id: number | null;
13357
+ category: { id: number; slug: string; name: string | null; color: string | null; icon: string | null } | null;
13358
+ }>(
13359
+ `SELECT pct.id,
13360
+ pct.slug,
13361
+ pct.code,
13362
+ COALESCE(pctl.name, pct.slug) AS name,
13363
+ pctl.description,
13364
+ pct.default_unit,
13365
+ pct.default_calculation_type,
13366
+ pct.is_recurring_allowed,
13367
+ pct.is_active,
13368
+ pct.sort_order,
13369
+ pct.category_id,
13370
+ CASE WHEN pcc.id IS NOT NULL THEN
13371
+ jsonb_build_object(
13372
+ 'id', pcc.id,
13373
+ 'slug', pcc.slug,
13374
+ 'name', COALESCE(pccl.name, pcc.slug),
13375
+ 'color', pcc.color,
13376
+ 'icon', pcc.icon
13377
+ )
13378
+ ELSE NULL END AS category
13379
+ FROM operations_project_cost_type pct
13380
+ LEFT JOIN operations_project_cost_category pcc
13381
+ ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
13382
+ LEFT JOIN LATERAL (
13383
+ SELECT l.name, l.description
13384
+ FROM operations_project_cost_category_locale l
13385
+ WHERE l.operations_project_cost_category_id = pcc.id
13386
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13387
+ l.id ASC
13388
+ LIMIT 1
13389
+ ) pccl ON TRUE
13390
+ LEFT JOIN LATERAL (
13391
+ SELECT l.name, l.description
13392
+ FROM operations_project_cost_type_locale l
13393
+ WHERE l.operations_project_cost_type_id = pct.id
13394
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13395
+ l.id ASC
13396
+ LIMIT 1
13397
+ ) pctl ON TRUE
13398
+ WHERE pct.id = $2 AND pct.deleted_at IS NULL`,
13399
+ [localeId, id]
13400
+ );
13401
+
13402
+ if (!row) {
13403
+ throw new NotFoundException('Project cost type not found.');
13404
+ }
13405
+
13406
+ return row;
13407
+ }
13408
+
13409
+ // ──────────────────────────────────────────────────────────────────────────
13410
+ // Project Costs
13411
+ // ──────────────────────────────────────────────────────────────────────────
13412
+
13413
+ async listProjectCosts(userId: number, projectId: number, filters: {
13414
+ search?: string;
13415
+ cost_type_id?: number;
13416
+ category_id?: number;
13417
+ recurrence_type?: string;
13418
+ calculation_type?: string;
13419
+ status?: string;
13420
+ is_billable?: boolean;
13421
+ is_reimbursable?: boolean;
13422
+ date_from?: string;
13423
+ date_to?: string;
13424
+ } = {}) {
13425
+ await this.getActorContext(userId);
13426
+ const localeId = await this.resolvePreferredLocaleId();
13427
+
13428
+ const params: unknown[] = [localeId, projectId];
13429
+ const where: string[] = ['pc.deleted_at IS NULL', 'pc.project_id = $2'];
13430
+
13431
+ if (filters.cost_type_id) {
13432
+ where.push(`pc.cost_type_id = ${this.param(params, filters.cost_type_id)}`);
13433
+ }
13434
+
13435
+ if (filters.category_id) {
13436
+ where.push(`COALESCE(pc.category_id, pct.category_id) = ${this.param(params, filters.category_id)}`);
13437
+ }
13438
+
13439
+ if (filters.recurrence_type) {
13440
+ where.push(`pc.recurrence_type = ${this.param(params, filters.recurrence_type)}`);
13441
+ }
13442
+
13443
+ if (filters.calculation_type) {
13444
+ where.push(`pc.calculation_type = ${this.param(params, filters.calculation_type)}`);
13445
+ }
13446
+
13447
+ if (filters.status) {
13448
+ where.push(`pc.status = ${this.param(params, filters.status)}`);
13449
+ }
13450
+
13451
+ if (filters.is_billable !== undefined) {
13452
+ where.push(`pc.is_billable = ${this.param(params, filters.is_billable)}`);
13453
+ }
13454
+
13455
+ if (filters.is_reimbursable !== undefined) {
13456
+ where.push(`pc.is_reimbursable = ${this.param(params, filters.is_reimbursable)}`);
13457
+ }
13458
+
13459
+ if (filters.date_from) {
13460
+ where.push(`pc.cost_date >= ${this.param(params, filters.date_from)}::date`);
13461
+ }
13462
+
13463
+ if (filters.date_to) {
13464
+ where.push(`pc.cost_date <= ${this.param(params, filters.date_to)}::date`);
13465
+ }
13466
+
13467
+ if (filters.search?.trim()) {
13468
+ const p = this.param(params, `%${filters.search.trim()}%`);
13469
+ where.push(`(COALESCE(pc.description, '') ILIKE ${p} OR COALESCE(pc.notes, '') ILIKE ${p})`);
13470
+ }
13471
+
13472
+ const whereClause = `WHERE ${where.join(' AND ')}`;
13473
+
13474
+ const rows = await this.queryRows<{
13475
+ id: number;
13476
+ projectId: number;
13477
+ costTypeId: number | null;
13478
+ costTypeSlug: string | null;
13479
+ costTypeCode: string | null;
13480
+ costTypeName: string | null;
13481
+ categoryId: number | null;
13482
+ resolvedCategoryId: number | null;
13483
+ categorySlug: string | null;
13484
+ categoryName: string | null;
13485
+ categoryColor: string | null;
13486
+ categoryIcon: string | null;
13487
+ description: string | null;
13488
+ amount: string;
13489
+ quantity: string;
13490
+ unitAmount: string | null;
13491
+ currency: string;
13492
+ costDate: string | null;
13493
+ periodStart: string | null;
13494
+ periodEnd: string | null;
13495
+ calculationType: string;
13496
+ recurrenceType: string;
13497
+ isBillable: boolean;
13498
+ isReimbursable: boolean;
13499
+ notes: string | null;
13500
+ status: string;
13501
+ createdAt: string;
13502
+ }>(
13503
+ `SELECT pc.id,
13504
+ pc.project_id AS "projectId",
13505
+ pc.cost_type_id AS "costTypeId",
13506
+ pct.slug AS "costTypeSlug",
13507
+ pct.code AS "costTypeCode",
13508
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
13509
+ pc.category_id AS "categoryId",
13510
+ COALESCE(pc.category_id, pct.category_id) AS "resolvedCategoryId",
13511
+ pcc.slug AS "categorySlug",
13512
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13513
+ pcc.color AS "categoryColor",
13514
+ pcc.icon AS "categoryIcon",
13515
+ pc.description,
13516
+ pc.amount::text AS amount,
13517
+ pc.quantity::text AS quantity,
13518
+ pc.unit_amount::text AS "unitAmount",
13519
+ pc.currency,
13520
+ TO_CHAR(pc.cost_date, 'YYYY-MM-DD') AS "costDate",
13521
+ TO_CHAR(pc.period_start, 'YYYY-MM-DD') AS "periodStart",
13522
+ TO_CHAR(pc.period_end, 'YYYY-MM-DD') AS "periodEnd",
13523
+ pc.calculation_type AS "calculationType",
13524
+ pc.recurrence_type AS "recurrenceType",
13525
+ pc.is_billable AS "isBillable",
13526
+ pc.is_reimbursable AS "isReimbursable",
13527
+ pc.notes,
13528
+ pc.status,
13529
+ pc.created_at AS "createdAt"
13530
+ FROM operations_project_cost pc
13531
+ LEFT JOIN operations_project_cost_type pct
13532
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13533
+ LEFT JOIN operations_project_cost_category pcc
13534
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
13535
+ LEFT JOIN LATERAL (
13536
+ SELECT l.name
13537
+ FROM operations_project_cost_type_locale l
13538
+ WHERE l.operations_project_cost_type_id = pct.id
13539
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13540
+ l.id ASC
13541
+ LIMIT 1
13542
+ ) pctl ON TRUE
13543
+ LEFT JOIN LATERAL (
13544
+ SELECT l.name
13545
+ FROM operations_project_cost_category_locale l
13546
+ WHERE l.operations_project_cost_category_id = pcc.id
13547
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13548
+ l.id ASC
13549
+ LIMIT 1
13550
+ ) pccl ON TRUE
13551
+ ${whereClause}
13552
+ ORDER BY pc.created_at DESC`,
13553
+ params
13554
+ );
13555
+
13556
+ return rows.map((row) => ({
13557
+ id: row.id,
13558
+ project_id: row.projectId,
13559
+ cost_type_id: row.costTypeId,
13560
+ category_id: row.categoryId,
13561
+ description: row.description,
13562
+ amount: row.amount,
13563
+ quantity: row.quantity,
13564
+ unit_amount: row.unitAmount,
13565
+ currency: row.currency,
13566
+ cost_date: row.costDate,
13567
+ period_start: row.periodStart,
13568
+ period_end: row.periodEnd,
13569
+ calculation_type: row.calculationType,
13570
+ recurrence_type: row.recurrenceType,
13571
+ is_billable: row.isBillable,
13572
+ is_reimbursable: row.isReimbursable,
13573
+ notes: row.notes,
13574
+ status: row.status,
13575
+ created_at: row.createdAt,
13576
+ cost_type: row.costTypeId
13577
+ ? { id: row.costTypeId, slug: row.costTypeSlug, name: row.costTypeName, code: row.costTypeCode }
13578
+ : null,
13579
+ category: row.resolvedCategoryId
13580
+ ? { id: row.resolvedCategoryId, slug: row.categorySlug, name: row.categoryName, color: row.categoryColor, icon: row.categoryIcon }
13581
+ : null,
13582
+ }));
13583
+ }
13584
+
13585
+ async getProjectCostsSummaryGrouped(userId: number, projectId: number) {
13586
+ const items = await this.listProjectCosts(userId, projectId, {});
13587
+
13588
+ // Group by resolved category
13589
+ const categoryMap = new Map<
13590
+ number | null,
13591
+ {
13592
+ category: { id: number; slug: string | null; name: string | null; color: string | null; icon: string | null } | null;
13593
+ items: typeof items;
13594
+ total_amount: number;
13595
+ }
13596
+ >();
13597
+
13598
+ for (const cost of items) {
13599
+ const cat = cost.category ?? null;
13600
+ const key = cat?.id ?? null;
13601
+ if (!categoryMap.has(key)) {
13602
+ categoryMap.set(key, { category: cat, items: [], total_amount: 0 });
13603
+ }
13604
+ const group = categoryMap.get(key)!;
13605
+ group.items.push(cost);
13606
+ group.total_amount += (parseFloat(String(cost.amount)) || 0) * (parseFloat(String(cost.quantity)) || 1);
13607
+ }
13608
+
13609
+ const grand_total = Array.from(categoryMap.values()).reduce(
13610
+ (sum, g) => sum + g.total_amount,
13611
+ 0,
13612
+ );
13613
+
13614
+ return {
13615
+ categories: Array.from(categoryMap.values()).map((g) => ({
13616
+ category: g.category,
13617
+ items: g.items,
13618
+ total_amount: Math.round(g.total_amount * 100) / 100,
13619
+ count: g.items.length,
13620
+ })),
13621
+ grand_total: Math.round(grand_total * 100) / 100,
13622
+ };
13623
+ }
13624
+
13625
+ async getProjectCost(userId: number, projectId: number, id: number) {
13626
+ const rows = await this.listProjectCosts(userId, projectId, {});
13627
+ const cost = rows.find((r) => r.id === id);
13628
+ if (!cost) {
13629
+ throw new NotFoundException('Project cost not found.');
13630
+ }
13631
+ return cost;
13632
+ }
13633
+
13634
+ async getProjectCostsSummary(userId: number, projectId: number) {
13635
+ await this.getActorContext(userId);
13636
+ const localeId = await this.resolvePreferredLocaleId();
13637
+
13638
+ // ── 1. Verify project exists and fetch budget_amount ──────────────────
13639
+ const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
13640
+ `SELECT id, budget_amount::text AS "budgetAmount"
13641
+ FROM operations_project
13642
+ WHERE id = $1 AND deleted_at IS NULL
13643
+ LIMIT 1`,
13644
+ [projectId]
13645
+ );
13646
+ if (!project) {
13647
+ throw new NotFoundException('Project not found.');
13648
+ }
13649
+
13650
+ const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
13651
+
13652
+ // ── 2. Aggregated cost totals ─────────────────────────────────────────
13653
+ const totals = await this.querySingle<{
13654
+ extraCostTotal: string;
13655
+ plannedTotal: string;
13656
+ approvedTotal: string;
13657
+ realizedTotal: string;
13658
+ cancelledTotal: string;
13659
+ billableTotal: string;
13660
+ nonBillableTotal: string;
13661
+ reimbursableTotal: string;
13662
+ }>(
13663
+ `SELECT
13664
+ COALESCE(SUM(CASE WHEN status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "extraCostTotal",
13665
+ COALESCE(SUM(CASE WHEN status = 'planned' THEN amount * quantity ELSE 0 END), 0)::text AS "plannedTotal",
13666
+ COALESCE(SUM(CASE WHEN status = 'approved' THEN amount * quantity ELSE 0 END), 0)::text AS "approvedTotal",
13667
+ COALESCE(SUM(CASE WHEN status = 'realized' THEN amount * quantity ELSE 0 END), 0)::text AS "realizedTotal",
13668
+ COALESCE(SUM(CASE WHEN status = 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "cancelledTotal",
13669
+ COALESCE(SUM(CASE WHEN is_billable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "billableTotal",
13670
+ COALESCE(SUM(CASE WHEN is_billable = false AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
13671
+ COALESCE(SUM(CASE WHEN is_reimbursable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "reimbursableTotal"
13672
+ FROM operations_project_cost
13673
+ WHERE deleted_at IS NULL
13674
+ AND project_id = $1`,
13675
+ [projectId]
13676
+ );
13677
+
13678
+ const extraCostTotal = Math.round((parseFloat(totals?.extraCostTotal ?? '0') || 0) * 100) / 100;
13679
+ const plannedTotal = Math.round((parseFloat(totals?.plannedTotal ?? '0') || 0) * 100) / 100;
13680
+ const approvedTotal = Math.round((parseFloat(totals?.approvedTotal ?? '0') || 0) * 100) / 100;
13681
+ const realizedTotal = Math.round((parseFloat(totals?.realizedTotal ?? '0') || 0) * 100) / 100;
13682
+ const cancelledTotal = Math.round((parseFloat(totals?.cancelledTotal ?? '0') || 0) * 100) / 100;
13683
+ const billableTotal = Math.round((parseFloat(totals?.billableTotal ?? '0') || 0) * 100) / 100;
13684
+ const nonBillableTotal = Math.round((parseFloat(totals?.nonBillableTotal ?? '0') || 0) * 100) / 100;
13685
+ const reimbursableTotal = Math.round((parseFloat(totals?.reimbursableTotal ?? '0') || 0) * 100) / 100;
13686
+
13687
+ const teamCostTotal = 0;
13688
+ const totalProjectCost = Math.round((teamCostTotal + extraCostTotal) * 100) / 100;
13689
+ const remainingBudget = Math.round((budgetAmount - totalProjectCost) * 100) / 100;
13690
+ const budgetUsagePercent = budgetAmount > 0
13691
+ ? Math.round((totalProjectCost / budgetAmount) * 10000) / 100
13692
+ : 0;
13693
+
13694
+ // ── 3. cost_by_category ───────────────────────────────────────────────
13695
+ const costByCategory = await this.queryRows<{
13696
+ categoryId: number | null;
13697
+ categorySlug: string | null;
13698
+ categoryName: string | null;
13699
+ categoryColor: string | null;
13700
+ categoryIcon: string | null;
13701
+ total: string;
13702
+ count: number;
13703
+ }>(
13704
+ `SELECT
13705
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
13706
+ pcc.slug AS "categorySlug",
13707
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13708
+ pcc.color AS "categoryColor",
13709
+ pcc.icon AS "categoryIcon",
13710
+ SUM(pc.amount * pc.quantity)::text AS total,
13711
+ COUNT(*)::int AS count
13712
+ FROM operations_project_cost pc
13713
+ LEFT JOIN operations_project_cost_type pct
13714
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13715
+ LEFT JOIN operations_project_cost_category pcc
13716
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
13717
+ LEFT JOIN LATERAL (
13718
+ SELECT l.name
13719
+ FROM operations_project_cost_category_locale l
13720
+ WHERE l.operations_project_cost_category_id = pcc.id
13721
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13722
+ l.id ASC
13723
+ LIMIT 1
13724
+ ) pccl ON TRUE
13725
+ WHERE pc.deleted_at IS NULL
13726
+ AND pc.project_id = $2
13727
+ AND pc.status != 'cancelled'
13728
+ GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
13729
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
13730
+ [localeId, projectId]
13731
+ );
13732
+
13733
+ // ── 4. cost_by_type ───────────────────────────────────────────────────
13734
+ const costByType = await this.queryRows<{
13735
+ costTypeId: number | null;
13736
+ costTypeSlug: string | null;
13737
+ costTypeName: string | null;
13738
+ costTypeCode: string | null;
13739
+ total: string;
13740
+ count: number;
13741
+ }>(
13742
+ `SELECT
13743
+ pc.cost_type_id AS "costTypeId",
13744
+ pct.slug AS "costTypeSlug",
13745
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
13746
+ pct.code AS "costTypeCode",
13747
+ SUM(pc.amount * pc.quantity)::text AS total,
13748
+ COUNT(*)::int AS count
13749
+ FROM operations_project_cost pc
13750
+ LEFT JOIN operations_project_cost_type pct
13751
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13752
+ LEFT JOIN LATERAL (
13753
+ SELECT l.name
13754
+ FROM operations_project_cost_type_locale l
13755
+ WHERE l.operations_project_cost_type_id = pct.id
13756
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13757
+ l.id ASC
13758
+ LIMIT 1
13759
+ ) pctl ON TRUE
13760
+ WHERE pc.deleted_at IS NULL
13761
+ AND pc.project_id = $2
13762
+ AND pc.status != 'cancelled'
13763
+ GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
13764
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
13765
+ [localeId, projectId]
13766
+ );
13767
+
13768
+ // ── 5. cost_by_month ──────────────────────────────────────────────────
13769
+ const costByMonth = await this.queryRows<{
13770
+ month: string;
13771
+ total: string;
13772
+ count: number;
13773
+ }>(
13774
+ `SELECT
13775
+ TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM') AS month,
13776
+ SUM(pc.amount * pc.quantity)::text AS total,
13777
+ COUNT(*)::int AS count
13778
+ FROM operations_project_cost pc
13779
+ WHERE pc.deleted_at IS NULL
13780
+ AND pc.project_id = $1
13781
+ AND pc.status != 'cancelled'
13782
+ GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM')
13783
+ ORDER BY month ASC`,
13784
+ [projectId]
13785
+ );
13786
+
13787
+ // ── 6. top_cost_types (top 5) ─────────────────────────────────────────
13788
+ const topCostTypes = costByType.slice(0, 5).map((ct) => {
13789
+ const typeTotal = Math.round((parseFloat(ct.total) || 0) * 100) / 100;
13790
+ const percentage = extraCostTotal > 0
13791
+ ? Math.round((typeTotal / extraCostTotal) * 10000) / 100
13792
+ : 0;
13793
+ return {
13794
+ cost_type_id: ct.costTypeId,
13795
+ cost_type_slug: ct.costTypeSlug,
13796
+ cost_type_name: ct.costTypeName,
13797
+ cost_type_code: ct.costTypeCode,
13798
+ total: typeTotal,
13799
+ percentage,
13800
+ };
13801
+ });
13802
+
13803
+ return {
13804
+ project_id: projectId,
13805
+ budget_amount: budgetAmount,
13806
+ team_cost_total: teamCostTotal,
13807
+ extra_cost_total: extraCostTotal,
13808
+ total_project_cost: totalProjectCost,
13809
+ remaining_budget: remainingBudget,
13810
+ budget_usage_percent: budgetUsagePercent,
13811
+ planned_total: plannedTotal,
13812
+ approved_total: approvedTotal,
13813
+ realized_total: realizedTotal,
13814
+ cancelled_total: cancelledTotal,
13815
+ billable_total: billableTotal,
13816
+ non_billable_total: nonBillableTotal,
13817
+ reimbursable_total: reimbursableTotal,
13818
+ cost_by_category: costByCategory.map((c) => ({
13819
+ category_id: c.categoryId,
13820
+ category_slug: c.categorySlug,
13821
+ category_name: c.categoryName,
13822
+ category_color: c.categoryColor,
13823
+ category_icon: c.categoryIcon,
13824
+ total: Math.round((parseFloat(c.total) || 0) * 100) / 100,
13825
+ count: Number(c.count),
13826
+ })),
13827
+ cost_by_type: costByType.map((t) => ({
13828
+ cost_type_id: t.costTypeId,
13829
+ cost_type_slug: t.costTypeSlug,
13830
+ cost_type_name: t.costTypeName,
13831
+ cost_type_code: t.costTypeCode,
13832
+ total: Math.round((parseFloat(t.total) || 0) * 100) / 100,
13833
+ count: Number(t.count),
13834
+ })),
13835
+ cost_by_month: costByMonth.map((m) => ({
13836
+ month: m.month,
13837
+ total: Math.round((parseFloat(m.total) || 0) * 100) / 100,
13838
+ count: Number(m.count),
13839
+ })),
13840
+ top_cost_types: topCostTypes,
13841
+ };
13842
+ }
13843
+
13844
+ async getProjectCostReport(
13845
+ userId: number,
13846
+ projectId: number,
13847
+ filters: {
13848
+ date_from?: string;
13849
+ date_to?: string;
13850
+ category_id?: number;
13851
+ cost_type_id?: number;
13852
+ status?: string;
13853
+ is_billable?: boolean;
13854
+ is_reimbursable?: boolean;
13855
+ },
13856
+ ) {
13857
+ await this.getActorContext(userId);
13858
+ const localeId = await this.resolvePreferredLocaleId();
13859
+
13860
+ // ── Verify project ───────────────────────────────────────────────────
13861
+ const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
13862
+ `SELECT id, budget_amount::text AS "budgetAmount"
13863
+ FROM operations_project
13864
+ WHERE id = $1 AND deleted_at IS NULL
13865
+ LIMIT 1`,
13866
+ [projectId],
13867
+ );
13868
+ if (!project) {
13869
+ throw new NotFoundException('Project not found.');
13870
+ }
13871
+ const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
13872
+
13873
+ // ── Build dynamic WHERE clause ────────────────────────────────────────
13874
+ const conditions: string[] = [
13875
+ 'pc.deleted_at IS NULL',
13876
+ 'pc.project_id = $1',
13877
+ ];
13878
+ const params: unknown[] = [projectId];
13879
+
13880
+ if (filters.date_from) {
13881
+ params.push(filters.date_from);
13882
+ conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) >= $${params.length}::date`);
13883
+ }
13884
+ if (filters.date_to) {
13885
+ params.push(filters.date_to);
13886
+ conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) <= $${params.length}::date`);
13887
+ }
13888
+ if (filters.category_id !== undefined) {
13889
+ params.push(filters.category_id);
13890
+ conditions.push(
13891
+ `(pc.category_id = $${params.length} OR (pc.category_id IS NULL AND EXISTS (
13892
+ SELECT 1 FROM operations_project_cost_type pct2
13893
+ WHERE pct2.id = pc.cost_type_id AND pct2.category_id = $${params.length} AND pct2.deleted_at IS NULL
13894
+ )))`,
13895
+ );
13896
+ }
13897
+ if (filters.cost_type_id !== undefined) {
13898
+ params.push(filters.cost_type_id);
13899
+ conditions.push(`pc.cost_type_id = $${params.length}`);
13900
+ }
13901
+ if (filters.status !== undefined) {
13902
+ params.push(filters.status);
13903
+ conditions.push(`pc.status = $${params.length}`);
13904
+ }
13905
+ if (filters.is_billable !== undefined) {
13906
+ params.push(filters.is_billable);
13907
+ conditions.push(`pc.is_billable = $${params.length}`);
13908
+ }
13909
+ if (filters.is_reimbursable !== undefined) {
13910
+ params.push(filters.is_reimbursable);
13911
+ conditions.push(`pc.is_reimbursable = $${params.length}`);
13912
+ }
13913
+
13914
+ const whereClause = conditions.join(' AND ');
13915
+
13916
+ // ── Totals ────────────────────────────────────────────────────────────
13917
+ const totals = await this.querySingle<{
13918
+ grandTotal: string;
13919
+ plannedTotal: string;
13920
+ approvedTotal: string;
13921
+ realizedTotal: string;
13922
+ cancelledTotal: string;
13923
+ billableTotal: string;
13924
+ nonBillableTotal: string;
13925
+ reimbursableTotal: string;
13926
+ totalCount: number;
13927
+ }>(
13928
+ `SELECT
13929
+ COALESCE(SUM(pc.amount * pc.quantity), 0)::text AS "grandTotal",
13930
+ COALESCE(SUM(CASE WHEN pc.status = 'planned' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "plannedTotal",
13931
+ COALESCE(SUM(CASE WHEN pc.status = 'approved' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "approvedTotal",
13932
+ COALESCE(SUM(CASE WHEN pc.status = 'realized' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "realizedTotal",
13933
+ COALESCE(SUM(CASE WHEN pc.status = 'cancelled' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "cancelledTotal",
13934
+ COALESCE(SUM(CASE WHEN pc.is_billable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "billableTotal",
13935
+ COALESCE(SUM(CASE WHEN pc.is_billable = false THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
13936
+ COALESCE(SUM(CASE WHEN pc.is_reimbursable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "reimbursableTotal",
13937
+ COUNT(*)::int AS "totalCount"
13938
+ FROM operations_project_cost pc
13939
+ WHERE ${whereClause}`,
13940
+ params,
13941
+ );
13942
+
13943
+ const round2 = (v: string | null | undefined) =>
13944
+ Math.round((parseFloat(v ?? '0') || 0) * 100) / 100;
13945
+
13946
+ const grandTotal = round2(totals?.grandTotal);
13947
+ const plannedTotal = round2(totals?.plannedTotal);
13948
+ const approvedTotal = round2(totals?.approvedTotal);
13949
+ const realizedTotal = round2(totals?.realizedTotal);
13950
+ const cancelledTotal = round2(totals?.cancelledTotal);
13951
+ const billableTotal = round2(totals?.billableTotal);
13952
+ const nonBillableTotal = round2(totals?.nonBillableTotal);
13953
+ const reimbursableTotal= round2(totals?.reimbursableTotal);
13954
+
13955
+ // ── By category ───────────────────────────────────────────────────────
13956
+ const costByCategory = await this.queryRows<{
13957
+ categoryId: number | null;
13958
+ categorySlug: string | null;
13959
+ categoryName: string | null;
13960
+ categoryColor: string | null;
13961
+ categoryIcon: string | null;
13962
+ total: string;
13963
+ count: number;
13964
+ plannedSubtotal: string;
13965
+ realizedSubtotal: string;
13966
+ }>(
13967
+ `SELECT
13968
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
13969
+ pcc.slug AS "categorySlug",
13970
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13971
+ pcc.color AS "categoryColor",
13972
+ pcc.icon AS "categoryIcon",
13973
+ SUM(pc.amount * pc.quantity)::text AS total,
13974
+ COUNT(*)::int AS count,
13975
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
13976
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
13977
+ FROM operations_project_cost pc
13978
+ LEFT JOIN operations_project_cost_type pct
13979
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13980
+ LEFT JOIN operations_project_cost_category pcc
13981
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
13982
+ LEFT JOIN LATERAL (
13983
+ SELECT l.name
13984
+ FROM operations_project_cost_category_locale l
13985
+ WHERE l.operations_project_cost_category_id = pcc.id
13986
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
13987
+ LIMIT 1
13988
+ ) pccl ON TRUE
13989
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
13990
+ GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
13991
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
13992
+ [localeId, ...params],
13993
+ );
13994
+
13995
+ // ── By type ───────────────────────────────────────────────────────────
13996
+ const costByType = await this.queryRows<{
13997
+ costTypeId: number | null;
13998
+ costTypeSlug: string | null;
13999
+ costTypeName: string | null;
14000
+ costTypeCode: string | null;
14001
+ total: string;
14002
+ count: number;
14003
+ plannedSubtotal: string;
14004
+ realizedSubtotal: string;
14005
+ }>(
14006
+ `SELECT
14007
+ pc.cost_type_id AS "costTypeId",
14008
+ pct.slug AS "costTypeSlug",
14009
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14010
+ pct.code AS "costTypeCode",
14011
+ SUM(pc.amount * pc.quantity)::text AS total,
14012
+ COUNT(*)::int AS count,
14013
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
14014
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
14015
+ FROM operations_project_cost pc
14016
+ LEFT JOIN operations_project_cost_type pct
14017
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14018
+ LEFT JOIN LATERAL (
14019
+ SELECT l.name
14020
+ FROM operations_project_cost_type_locale l
14021
+ WHERE l.operations_project_cost_type_id = pct.id
14022
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14023
+ LIMIT 1
14024
+ ) pctl ON TRUE
14025
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14026
+ GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
14027
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
14028
+ [localeId, ...params],
14029
+ );
14030
+
14031
+ // ── By month ──────────────────────────────────────────────────────────
14032
+ const costByMonth = await this.queryRows<{
14033
+ month: string;
14034
+ total: string;
14035
+ plannedSubtotal: string;
14036
+ realizedSubtotal: string;
14037
+ count: number;
14038
+ }>(
14039
+ `SELECT
14040
+ TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM') AS month,
14041
+ SUM(pc.amount * pc.quantity)::text AS total,
14042
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
14043
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal",
14044
+ COUNT(*)::int AS count
14045
+ FROM operations_project_cost pc
14046
+ WHERE ${whereClause}
14047
+ GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM')
14048
+ ORDER BY month ASC`,
14049
+ params,
14050
+ );
14051
+
14052
+ // ── Top 5 individual costs ────────────────────────────────────────────
14053
+ const top5Costs = await this.queryRows<{
14054
+ id: number;
14055
+ description: string | null;
14056
+ amount: string;
14057
+ quantity: string;
14058
+ status: string;
14059
+ costTypeName: string | null;
14060
+ categoryName: string | null;
14061
+ categoryColor: string | null;
14062
+ costDate: string | null;
14063
+ }>(
14064
+ `SELECT
14065
+ pc.id,
14066
+ pc.description,
14067
+ pc.amount::text,
14068
+ pc.quantity::text,
14069
+ pc.status,
14070
+ pc.cost_date AS "costDate",
14071
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14072
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
14073
+ pcc.color AS "categoryColor"
14074
+ FROM operations_project_cost pc
14075
+ LEFT JOIN operations_project_cost_type pct
14076
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14077
+ LEFT JOIN LATERAL (
14078
+ SELECT l.name
14079
+ FROM operations_project_cost_type_locale l
14080
+ WHERE l.operations_project_cost_type_id = pct.id
14081
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14082
+ LIMIT 1
14083
+ ) pctl ON TRUE
14084
+ LEFT JOIN operations_project_cost_category pcc
14085
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
14086
+ LEFT JOIN LATERAL (
14087
+ SELECT l.name
14088
+ FROM operations_project_cost_category_locale l
14089
+ WHERE l.operations_project_cost_category_id = pcc.id
14090
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14091
+ LIMIT 1
14092
+ ) pccl ON TRUE
14093
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14094
+ ORDER BY (pc.amount * pc.quantity) DESC
14095
+ LIMIT 5`,
14096
+ [localeId, ...params],
14097
+ );
14098
+
14099
+ // ── Detailed list ─────────────────────────────────────────────────────
14100
+ const detailedList = await this.queryRows<{
14101
+ id: number;
14102
+ description: string | null;
14103
+ amount: string;
14104
+ quantity: string;
14105
+ unitAmount: string | null;
14106
+ currency: string | null;
14107
+ calculationType: string | null;
14108
+ recurrenceType: string | null;
14109
+ status: string;
14110
+ isBillable: boolean;
14111
+ isReimbursable: boolean;
14112
+ costDate: string | null;
14113
+ periodStart: string | null;
14114
+ periodEnd: string | null;
14115
+ notes: string | null;
14116
+ costTypeId: number | null;
14117
+ costTypeName: string | null;
14118
+ costTypeCode: string | null;
14119
+ categoryId: number | null;
14120
+ categoryName: string | null;
14121
+ categoryColor: string | null;
14122
+ createdAt: string;
14123
+ }>(
14124
+ `SELECT
14125
+ pc.id,
14126
+ pc.description,
14127
+ pc.amount::text,
14128
+ pc.quantity::text,
14129
+ pc.unit_amount::text AS "unitAmount",
14130
+ pc.currency,
14131
+ pc.calculation_type AS "calculationType",
14132
+ pc.recurrence_type AS "recurrenceType",
14133
+ pc.status,
14134
+ pc.is_billable AS "isBillable",
14135
+ pc.is_reimbursable AS "isReimbursable",
14136
+ pc.cost_date AS "costDate",
14137
+ pc.period_start AS "periodStart",
14138
+ pc.period_end AS "periodEnd",
14139
+ pc.notes,
14140
+ pc.cost_type_id AS "costTypeId",
14141
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14142
+ pct.code AS "costTypeCode",
14143
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
14144
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
14145
+ pcc.color AS "categoryColor",
14146
+ pc.created_at::text AS "createdAt"
14147
+ FROM operations_project_cost pc
14148
+ LEFT JOIN operations_project_cost_type pct
14149
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14150
+ LEFT JOIN LATERAL (
14151
+ SELECT l.name
14152
+ FROM operations_project_cost_type_locale l
14153
+ WHERE l.operations_project_cost_type_id = pct.id
14154
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14155
+ LIMIT 1
14156
+ ) pctl ON TRUE
14157
+ LEFT JOIN operations_project_cost_category pcc
14158
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
14159
+ LEFT JOIN LATERAL (
14160
+ SELECT l.name
14161
+ FROM operations_project_cost_category_locale l
14162
+ WHERE l.operations_project_cost_category_id = pcc.id
14163
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14164
+ LIMIT 1
14165
+ ) pccl ON TRUE
14166
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14167
+ ORDER BY (pc.amount * pc.quantity) DESC, pc.cost_date DESC NULLS LAST`,
14168
+ [localeId, ...params],
14169
+ );
14170
+
14171
+ return {
14172
+ project_id: projectId,
14173
+ budget_amount: budgetAmount,
14174
+ filters_applied: {
14175
+ date_from: filters.date_from ?? null,
14176
+ date_to: filters.date_to ?? null,
14177
+ category_id: filters.category_id ?? null,
14178
+ cost_type_id: filters.cost_type_id ?? null,
14179
+ status: filters.status ?? null,
14180
+ is_billable: filters.is_billable ?? null,
14181
+ is_reimbursable:filters.is_reimbursable ?? null,
14182
+ },
14183
+ totals: {
14184
+ grand_total: grandTotal,
14185
+ planned_total: plannedTotal,
14186
+ approved_total: approvedTotal,
14187
+ realized_total: realizedTotal,
14188
+ cancelled_total: cancelledTotal,
14189
+ billable_total: billableTotal,
14190
+ non_billable_total: nonBillableTotal,
14191
+ reimbursable_total: reimbursableTotal,
14192
+ total_count: Number(totals?.totalCount ?? 0),
14193
+ },
14194
+ cost_by_category: costByCategory.map((c) => ({
14195
+ category_id: c.categoryId,
14196
+ category_slug: c.categorySlug,
14197
+ category_name: c.categoryName,
14198
+ category_color: c.categoryColor,
14199
+ category_icon: c.categoryIcon,
14200
+ total: round2(c.total),
14201
+ count: Number(c.count),
14202
+ planned_subtotal: round2(c.plannedSubtotal),
14203
+ realized_subtotal: round2(c.realizedSubtotal),
14204
+ })),
14205
+ cost_by_type: costByType.map((t) => ({
14206
+ cost_type_id: t.costTypeId,
14207
+ cost_type_slug: t.costTypeSlug,
14208
+ cost_type_name: t.costTypeName,
14209
+ cost_type_code: t.costTypeCode,
14210
+ total: round2(t.total),
14211
+ count: Number(t.count),
14212
+ planned_subtotal: round2(t.plannedSubtotal),
14213
+ realized_subtotal: round2(t.realizedSubtotal),
14214
+ })),
14215
+ cost_by_month: costByMonth.map((m) => ({
14216
+ month: m.month,
14217
+ total: round2(m.total),
14218
+ planned_subtotal: round2(m.plannedSubtotal),
14219
+ realized_subtotal: round2(m.realizedSubtotal),
14220
+ count: Number(m.count),
14221
+ })),
14222
+ top_5_costs: top5Costs.map((c) => ({
14223
+ id: c.id,
14224
+ description: c.description,
14225
+ total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
14226
+ amount: round2(c.amount),
14227
+ quantity: parseFloat(c.quantity),
14228
+ status: c.status,
14229
+ cost_type_name: c.costTypeName,
14230
+ category_name: c.categoryName,
14231
+ category_color: c.categoryColor,
14232
+ cost_date: c.costDate,
14233
+ })),
14234
+ detailed_list: detailedList.map((c) => ({
14235
+ id: c.id,
14236
+ description: c.description,
14237
+ amount: round2(c.amount),
14238
+ quantity: parseFloat(c.quantity),
14239
+ unit_amount: c.unitAmount ? round2(c.unitAmount) : null,
14240
+ total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
14241
+ currency: c.currency,
14242
+ calculation_type: c.calculationType,
14243
+ recurrence_type: c.recurrenceType,
14244
+ status: c.status,
14245
+ is_billable: c.isBillable,
14246
+ is_reimbursable: c.isReimbursable,
14247
+ cost_date: c.costDate,
14248
+ period_start: c.periodStart,
14249
+ period_end: c.periodEnd,
14250
+ notes: c.notes,
14251
+ cost_type_id: c.costTypeId,
14252
+ cost_type_name: c.costTypeName,
14253
+ cost_type_code: c.costTypeCode,
14254
+ category_id: c.categoryId,
14255
+ category_name: c.categoryName,
14256
+ category_color: c.categoryColor,
14257
+ created_at: c.createdAt,
14258
+ })),
14259
+ };
14260
+ }
14261
+
14262
+ async createProjectCost(userId: number, projectId: number, data: { cost_type_id?: number; category_id?: number; description?: string; amount: number; quantity?: number; unit_amount?: number; currency?: string; cost_date?: string; period_start?: string; period_end?: string; calculation_type?: string; recurrence_type?: string; is_billable?: boolean; is_reimbursable?: boolean; notes?: string; status?: string }) {
14263
+ const actor = await this.getActorContext(userId);
14264
+ this.ensureSupervisor(actor);
14265
+
14266
+ const project = await this.querySingle<{ id: number }>(
14267
+ `SELECT id FROM operations_project WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14268
+ [projectId]
14269
+ );
14270
+ if (!project) {
14271
+ throw new NotFoundException('Project not found.');
14272
+ }
14273
+
14274
+ if (data.cost_type_id) {
14275
+ const costType = await this.querySingle<{ id: number }>(
14276
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14277
+ [data.cost_type_id]
14278
+ );
14279
+ if (!costType) {
14280
+ throw new BadRequestException(`Cost type with id ${data.cost_type_id} not found.`);
14281
+ }
14282
+ }
14283
+
14284
+ if (data.category_id) {
14285
+ const category = await this.querySingle<{ id: number }>(
14286
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14287
+ [data.category_id]
14288
+ );
14289
+ if (!category) {
14290
+ throw new BadRequestException(`Cost category with id ${data.category_id} not found.`);
14291
+ }
14292
+ }
14293
+
14294
+ const calcType = data.calculation_type ?? 'fixed';
14295
+ let effectiveAmount = data.amount;
14296
+ if (['unit', 'hourly', 'monthly'].includes(calcType) && data.unit_amount !== undefined && data.unit_amount !== null) {
14297
+ const qty = data.quantity ?? 1;
14298
+ effectiveAmount = Math.round(qty * data.unit_amount * 100) / 100;
14299
+ }
14300
+
14301
+ const created = await this.querySingle<{ id: number }>(
14302
+ `INSERT INTO operations_project_cost
14303
+ (project_id, cost_type_id, category_id, description, amount, quantity, unit_amount, currency, cost_date, period_start, period_end, calculation_type, recurrence_type, is_billable, is_reimbursable, notes, status, created_at, updated_at)
14304
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::date, $10::date, $11::date, $12::operations_project_cost_calculation_type_134cdfb49c_enum, $13::operations_project_cost_recurrence_type_09baf0f043_enum, $14, $15, $16, $17::operations_project_cost_status_153e8592ce_enum, NOW(), NOW())
14305
+ RETURNING id`,
14306
+ [
14307
+ projectId,
14308
+ data.cost_type_id ?? null,
14309
+ data.category_id ?? null,
14310
+ data.description ?? null,
14311
+ effectiveAmount,
14312
+ data.quantity ?? 1,
14313
+ data.unit_amount ?? null,
14314
+ data.currency ?? 'BRL',
14315
+ data.cost_date ?? null,
14316
+ data.period_start ?? null,
14317
+ data.period_end ?? null,
14318
+ calcType,
14319
+ data.recurrence_type ?? 'none',
14320
+ data.is_billable ?? false,
14321
+ data.is_reimbursable ?? false,
14322
+ data.notes ?? null,
14323
+ data.status ?? 'planned',
14324
+ ]
14325
+ );
14326
+
14327
+ if (!created?.id) {
14328
+ throw new BadRequestException('Unable to create project cost.');
14329
+ }
14330
+
14331
+ const rows = await this.listProjectCosts(userId, projectId, {});
14332
+ return rows.find((r) => r.id === created.id) ?? null;
14333
+ }
14334
+
14335
+ async updateProjectCost(userId: number, id: number, data: Partial<{ cost_type_id: number; category_id: number; description: string; amount: number; quantity: number; unit_amount: number; currency: string; cost_date: string; period_start: string; period_end: string; calculation_type: string; recurrence_type: string; is_billable: boolean; is_reimbursable: boolean; notes: string; status: string }>) {
14336
+ const actor = await this.getActorContext(userId);
14337
+ this.ensureSupervisor(actor);
14338
+
14339
+ const cost = await this.querySingle<{ id: number; projectId: number; calculationType: string; unitAmount: string | null; quantity: string }>(
14340
+ `SELECT id, project_id AS "projectId", calculation_type AS "calculationType", unit_amount::text AS "unitAmount", quantity::text AS quantity FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14341
+ [id]
14342
+ );
14343
+ if (!cost) {
14344
+ throw new NotFoundException('Project cost not found.');
14345
+ }
14346
+
14347
+ // Auto-calculate amount when applicable
14348
+ const effectiveCalcType = data.calculation_type ?? cost.calculationType;
14349
+ if (['unit', 'hourly', 'monthly'].includes(effectiveCalcType)) {
14350
+ const ua = data.unit_amount !== undefined ? data.unit_amount : (cost.unitAmount !== null ? parseFloat(cost.unitAmount) : null);
14351
+ const qty = data.quantity !== undefined ? data.quantity : parseFloat(cost.quantity);
14352
+ if (ua !== null && ua !== undefined) {
14353
+ data = { ...data, amount: Math.round(qty * ua * 100) / 100 };
14354
+ }
14355
+ }
14356
+
14357
+ const sets: string[] = [];
14358
+ const params: unknown[] = [];
14359
+
14360
+ if (data.cost_type_id !== undefined) sets.push(`cost_type_id = ${this.param(params, data.cost_type_id)}`);
14361
+ if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
14362
+ if (data.description !== undefined) sets.push(`description = ${this.param(params, data.description)}`);
14363
+ if (data.amount !== undefined) sets.push(`amount = ${this.param(params, data.amount)}`);
14364
+ if (data.currency !== undefined) sets.push(`currency = ${this.param(params, data.currency)}`);
14365
+ if (data.quantity !== undefined) sets.push(`quantity = ${this.param(params, data.quantity)}`);
14366
+ if (data.unit_amount !== undefined) sets.push(`unit_amount = ${this.param(params, data.unit_amount)}`);
14367
+ if (data.calculation_type !== undefined) sets.push(`calculation_type = ${this.param(params, data.calculation_type)}::operations_project_cost_calculation_type_134cdfb49c_enum`);
14368
+ if (data.recurrence_type !== undefined) sets.push(`recurrence_type = ${this.param(params, data.recurrence_type)}::operations_project_cost_recurrence_type_09baf0f043_enum`);
14369
+ if (data.is_billable !== undefined) sets.push(`is_billable = ${this.param(params, data.is_billable)}`);
14370
+ if (data.is_reimbursable !== undefined) sets.push(`is_reimbursable = ${this.param(params, data.is_reimbursable)}`);
14371
+ if (data.cost_date !== undefined) sets.push(`cost_date = ${this.param(params, data.cost_date)}::date`);
14372
+ if (data.period_start !== undefined) sets.push(`period_start = ${this.param(params, data.period_start)}::date`);
14373
+ if (data.period_end !== undefined) sets.push(`period_end = ${this.param(params, data.period_end)}::date`);
14374
+ if (data.notes !== undefined) sets.push(`notes = ${this.param(params, data.notes)}`);
14375
+ if (data.status !== undefined) sets.push(`status = ${this.param(params, data.status)}::operations_project_cost_status_153e8592ce_enum`);
14376
+
14377
+ if (sets.length === 0) {
14378
+ const rows = await this.listProjectCosts(userId, cost.projectId, {});
14379
+ return rows.find((r) => r.id === id) ?? null;
14380
+ }
14381
+
14382
+ sets.push(`updated_at = NOW()`);
14383
+ await this.prisma.$queryRawUnsafe(
14384
+ `UPDATE operations_project_cost SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
14385
+ ...params
14386
+ );
14387
+
14388
+ const rows = await this.listProjectCosts(userId, cost.projectId, {});
14389
+ return rows.find((r) => r.id === id) ?? null;
14390
+ }
14391
+
14392
+ async deleteProjectCost(userId: number, id: number) {
14393
+ const actor = await this.getActorContext(userId);
14394
+ this.ensureSupervisor(actor);
14395
+
14396
+ const cost = await this.querySingle<{ id: number }>(
14397
+ `SELECT id FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14398
+ [id]
14399
+ );
14400
+ if (!cost) {
14401
+ throw new NotFoundException('Project cost not found.');
14402
+ }
14403
+
14404
+ await this.prisma.$queryRawUnsafe(
14405
+ `UPDATE operations_project_cost SET deleted_at = NOW() WHERE id = $1`,
14406
+ id
14407
+ );
14408
+
14409
+ return { success: true };
14410
+ }
12176
14411
  }