@hed-hog/operations 0.0.321 → 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 (701) 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-contracts.controller.d.ts +9 -9
  6. package/dist/controllers/operations-project-costs.controller.d.ts +422 -0
  7. package/dist/controllers/operations-project-costs.controller.d.ts.map +1 -0
  8. package/dist/controllers/operations-project-costs.controller.js +250 -0
  9. package/dist/controllers/operations-project-costs.controller.js.map +1 -0
  10. package/dist/controllers/operations-reports.controller.d.ts +9 -0
  11. package/dist/controllers/operations-reports.controller.d.ts.map +1 -1
  12. package/dist/controllers/operations-tasks.controller.d.ts +64 -0
  13. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  14. package/dist/controllers/operations-tasks.controller.js +85 -0
  15. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  16. package/dist/controllers/operations-timesheets.controller.d.ts +1 -0
  17. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  18. package/dist/dto/create-collaborator-project-assignment.dto.d.ts +5 -0
  19. package/dist/dto/create-collaborator-project-assignment.dto.d.ts.map +1 -0
  20. package/dist/dto/create-collaborator-project-assignment.dto.js +30 -0
  21. package/dist/dto/create-collaborator-project-assignment.dto.js.map +1 -0
  22. package/dist/dto/create-project-cost-category.dto.d.ts +10 -0
  23. package/dist/dto/create-project-cost-category.dto.d.ts.map +1 -0
  24. package/dist/dto/create-project-cost-category.dto.js +59 -0
  25. package/dist/dto/create-project-cost-category.dto.js.map +1 -0
  26. package/dist/dto/create-project-cost-type.dto.d.ts +14 -0
  27. package/dist/dto/create-project-cost-type.dto.d.ts.map +1 -0
  28. package/dist/dto/create-project-cost-type.dto.js +87 -0
  29. package/dist/dto/create-project-cost-type.dto.js.map +1 -0
  30. package/dist/dto/create-project-cost.dto.d.ts +22 -0
  31. package/dist/dto/create-project-cost.dto.d.ts.map +1 -0
  32. package/dist/dto/create-project-cost.dto.js +135 -0
  33. package/dist/dto/create-project-cost.dto.js.map +1 -0
  34. package/dist/dto/create-task.dto.d.ts.map +1 -1
  35. package/dist/dto/create-task.dto.js +0 -1
  36. package/dist/dto/create-task.dto.js.map +1 -1
  37. package/dist/dto/get-project-cost-report.dto.d.ts +10 -0
  38. package/dist/dto/get-project-cost-report.dto.d.ts.map +1 -0
  39. package/dist/dto/get-project-cost-report.dto.js +65 -0
  40. package/dist/dto/get-project-cost-report.dto.js.map +1 -0
  41. package/dist/dto/list-project-cost-categories.dto.d.ts +6 -0
  42. package/dist/dto/list-project-cost-categories.dto.d.ts.map +1 -0
  43. package/dist/dto/list-project-cost-categories.dto.js +34 -0
  44. package/dist/dto/list-project-cost-categories.dto.js.map +1 -0
  45. package/dist/dto/list-project-cost-types.dto.d.ts +8 -0
  46. package/dist/dto/list-project-cost-types.dto.d.ts.map +1 -0
  47. package/dist/dto/list-project-cost-types.dto.js +45 -0
  48. package/dist/dto/list-project-cost-types.dto.js.map +1 -0
  49. package/dist/dto/list-project-costs.dto.d.ts +14 -0
  50. package/dist/dto/list-project-costs.dto.d.ts.map +1 -0
  51. package/dist/dto/list-project-costs.dto.js +81 -0
  52. package/dist/dto/list-project-costs.dto.js.map +1 -0
  53. package/dist/dto/list-tasks.dto.d.ts +1 -0
  54. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  55. package/dist/dto/list-tasks.dto.js +6 -0
  56. package/dist/dto/list-tasks.dto.js.map +1 -1
  57. package/dist/dto/list-timesheets.dto.d.ts +1 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  59. package/dist/dto/list-timesheets.dto.js +7 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -1
  61. package/dist/dto/update-collaborator-project-assignment.dto.d.ts +11 -0
  62. package/dist/dto/update-collaborator-project-assignment.dto.d.ts.map +1 -0
  63. package/dist/dto/update-collaborator-project-assignment.dto.js +65 -0
  64. package/dist/dto/update-collaborator-project-assignment.dto.js.map +1 -0
  65. package/dist/dto/update-project-cost-category.dto.d.ts +6 -0
  66. package/dist/dto/update-project-cost-category.dto.d.ts.map +1 -0
  67. package/dist/dto/update-project-cost-category.dto.js +9 -0
  68. package/dist/dto/update-project-cost-category.dto.js.map +1 -0
  69. package/dist/dto/update-project-cost-type.dto.d.ts +6 -0
  70. package/dist/dto/update-project-cost-type.dto.d.ts.map +1 -0
  71. package/dist/dto/update-project-cost-type.dto.js +9 -0
  72. package/dist/dto/update-project-cost-type.dto.js.map +1 -0
  73. package/dist/dto/update-project-cost.dto.d.ts +6 -0
  74. package/dist/dto/update-project-cost.dto.d.ts.map +1 -0
  75. package/dist/dto/update-project-cost.dto.js +9 -0
  76. package/dist/dto/update-project-cost.dto.js.map +1 -0
  77. package/dist/dto/update-task.dto.d.ts.map +1 -1
  78. package/dist/dto/update-task.dto.js +0 -1
  79. package/dist/dto/update-task.dto.js.map +1 -1
  80. package/dist/operations.module.d.ts.map +1 -1
  81. package/dist/operations.module.js +2 -0
  82. package/dist/operations.module.js.map +1 -1
  83. package/dist/operations.service.d.ts +584 -0
  84. package/dist/operations.service.d.ts.map +1 -1
  85. package/dist/operations.service.js +1734 -69
  86. package/dist/operations.service.js.map +1 -1
  87. package/hedhog/data/menu.yaml +52 -0
  88. package/hedhog/data/operations_project_cost_category.yaml +80 -0
  89. package/hedhog/data/operations_project_cost_type.yaml +503 -0
  90. package/hedhog/data/route.yaml +313 -0
  91. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +2 -18
  92. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +185 -276
  93. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -0
  94. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +242 -0
  95. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +167 -59
  96. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -826
  97. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +450 -0
  98. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +602 -0
  99. package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +1401 -0
  100. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +3390 -889
  101. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +118 -79
  102. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +297 -2
  103. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
  104. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +530 -0
  105. package/hedhog/frontend/app/_lib/api.ts.ejs +247 -0
  106. package/hedhog/frontend/app/_lib/types.ts.ejs +197 -7
  107. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +9 -3
  108. package/hedhog/frontend/app/collaborators/page.tsx.ejs +18 -7
  109. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +340 -133
  110. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +674 -0
  111. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +845 -0
  112. package/hedhog/frontend/app/projects/[id]/costs-report/page.tsx.ejs +10 -0
  113. package/hedhog/frontend/app/projects/page.tsx.ejs +105 -22
  114. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +20 -349
  115. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +192 -484
  116. package/hedhog/frontend/messages/en.json +421 -11
  117. package/hedhog/frontend/messages/en.json.ejs +2043 -0
  118. package/hedhog/frontend/messages/operations/en.json +2068 -0
  119. package/hedhog/frontend/messages/operations/operations/en.json +2102 -0
  120. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  121. package/hedhog/frontend/messages/operations/pt.json +2072 -0
  122. package/hedhog/frontend/messages/pt.json +426 -14
  123. package/hedhog/frontend/messages/pt.json.ejs +2056 -0
  124. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts +29 -0
  125. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts.map +1 -0
  126. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js +95 -0
  127. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js.map +1 -0
  128. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.tsx +233 -0
  129. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts +10 -0
  130. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  131. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js +577 -0
  132. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js.map +1 -0
  133. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.tsx +868 -0
  134. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts +4 -0
  135. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  136. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js +337 -0
  137. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js.map +1 -0
  138. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.tsx +476 -0
  139. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts +9 -0
  140. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  141. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js +1348 -0
  142. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js.map +1 -0
  143. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.tsx +2233 -0
  144. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts +12 -0
  145. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  146. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js +162 -0
  147. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js.map +1 -0
  148. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.tsx +261 -0
  149. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts +18 -0
  150. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts.map +1 -0
  151. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js +145 -0
  152. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js.map +1 -0
  153. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.tsx +258 -0
  154. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts +4 -0
  155. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts.map +1 -0
  156. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js +223 -0
  157. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js.map +1 -0
  158. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.tsx +342 -0
  159. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts +58 -0
  160. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts.map +1 -0
  161. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js +438 -0
  162. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js.map +1 -0
  163. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.tsx +698 -0
  164. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts +20 -0
  165. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts.map +1 -0
  166. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js +233 -0
  167. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js.map +1 -0
  168. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.tsx +392 -0
  169. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts +4 -0
  170. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  171. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js +814 -0
  172. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js.map +1 -0
  173. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.tsx +1288 -0
  174. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts +21 -0
  175. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts.map +1 -0
  176. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js +174 -0
  177. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js.map +1 -0
  178. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.tsx +306 -0
  179. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts +10 -0
  180. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts.map +1 -0
  181. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js +12 -0
  182. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js.map +1 -0
  183. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.tsx +29 -0
  184. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts +15 -0
  185. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts.map +1 -0
  186. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js +501 -0
  187. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js.map +1 -0
  188. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.tsx +853 -0
  189. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts +6 -0
  190. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts.map +1 -0
  191. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js +847 -0
  192. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js.map +1 -0
  193. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.tsx +1340 -0
  194. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts +4 -0
  195. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts.map +1 -0
  196. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js +2930 -0
  197. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js.map +1 -0
  198. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.tsx +4378 -0
  199. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts +9 -0
  200. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts.map +1 -0
  201. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js +1013 -0
  202. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js.map +1 -0
  203. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.tsx +1745 -0
  204. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts +13 -0
  205. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts.map +1 -0
  206. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js +38 -0
  207. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js.map +1 -0
  208. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.tsx +74 -0
  209. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts +7 -0
  210. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts.map +1 -0
  211. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js +11 -0
  212. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js.map +1 -0
  213. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.tsx +15 -0
  214. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts +18 -0
  215. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  216. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js +406 -0
  217. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js.map +1 -0
  218. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.tsx +660 -0
  219. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts +26 -0
  220. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts.map +1 -0
  221. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js +332 -0
  222. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js.map +1 -0
  223. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.tsx +518 -0
  224. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts +6 -0
  225. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts.map +1 -0
  226. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js +255 -0
  227. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js.map +1 -0
  228. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.tsx +388 -0
  229. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  230. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  231. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js +131 -0
  232. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  233. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  234. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts +108 -0
  235. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts.map +1 -0
  236. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js +162 -0
  237. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js.map +1 -0
  238. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.ts +428 -0
  239. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  240. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  241. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js +36 -0
  242. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js.map +1 -0
  243. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.ts +44 -0
  244. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +836 -0
  245. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -0
  246. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js +3 -0
  247. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js.map +1 -0
  248. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +860 -0
  249. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts +16 -0
  250. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts.map +1 -0
  251. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js +182 -0
  252. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js.map +1 -0
  253. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.ts +250 -0
  254. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts +4 -0
  255. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts.map +1 -0
  256. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js +51 -0
  257. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js.map +1 -0
  258. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.ts +61 -0
  259. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts +2 -0
  260. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts.map +1 -0
  261. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js +954 -0
  262. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js.map +1 -0
  263. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.tsx +1277 -0
  264. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts +2 -0
  265. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts.map +1 -0
  266. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js +488 -0
  267. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js.map +1 -0
  268. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.tsx +805 -0
  269. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts +6 -0
  270. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  271. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js +9 -0
  272. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js.map +1 -0
  273. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.tsx +11 -0
  274. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts +6 -0
  275. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts.map +1 -0
  276. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js +9 -0
  277. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js.map +1 -0
  278. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.tsx +11 -0
  279. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts +2 -0
  280. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts.map +1 -0
  281. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js +8 -0
  282. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js.map +1 -0
  283. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.tsx +5 -0
  284. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts +2 -0
  285. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts.map +1 -0
  286. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js +612 -0
  287. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js.map +1 -0
  288. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.tsx +939 -0
  289. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts +6 -0
  290. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  291. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js +9 -0
  292. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js.map +1 -0
  293. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.tsx +11 -0
  294. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts +6 -0
  295. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts.map +1 -0
  296. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js +9 -0
  297. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js.map +1 -0
  298. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.tsx +11 -0
  299. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts +6 -0
  300. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts.map +1 -0
  301. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js +9 -0
  302. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js.map +1 -0
  303. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.tsx +17 -0
  304. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts +2 -0
  305. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts.map +1 -0
  306. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js +348 -0
  307. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js.map +1 -0
  308. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.tsx +536 -0
  309. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts +2 -0
  310. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts.map +1 -0
  311. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js +401 -0
  312. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js.map +1 -0
  313. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.tsx +607 -0
  314. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts +5 -0
  315. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts.map +1 -0
  316. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js +7 -0
  317. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js.map +1 -0
  318. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.tsx +9 -0
  319. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts +6 -0
  320. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts.map +1 -0
  321. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js +9 -0
  322. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js.map +1 -0
  323. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.tsx +11 -0
  324. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts +2 -0
  325. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts.map +1 -0
  326. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js +321 -0
  327. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js.map +1 -0
  328. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.tsx +440 -0
  329. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts +2 -0
  330. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts.map +1 -0
  331. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js +939 -0
  332. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js.map +1 -0
  333. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.tsx +1499 -0
  334. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts +29 -0
  335. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts.map +1 -0
  336. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js +95 -0
  337. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js.map +1 -0
  338. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.tsx +233 -0
  339. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts +10 -0
  340. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  341. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js +577 -0
  342. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js.map +1 -0
  343. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.tsx +868 -0
  344. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts +4 -0
  345. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  346. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js +337 -0
  347. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js.map +1 -0
  348. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.tsx +476 -0
  349. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts +9 -0
  350. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  351. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js +1348 -0
  352. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js.map +1 -0
  353. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.tsx +2233 -0
  354. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts +12 -0
  355. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  356. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js +162 -0
  357. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js.map +1 -0
  358. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.tsx +261 -0
  359. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts +18 -0
  360. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts.map +1 -0
  361. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js +145 -0
  362. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js.map +1 -0
  363. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.tsx +258 -0
  364. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts +4 -0
  365. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts.map +1 -0
  366. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js +223 -0
  367. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js.map +1 -0
  368. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.tsx +342 -0
  369. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts +58 -0
  370. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts.map +1 -0
  371. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js +438 -0
  372. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js.map +1 -0
  373. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.tsx +698 -0
  374. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts +20 -0
  375. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts.map +1 -0
  376. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js +233 -0
  377. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js.map +1 -0
  378. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.tsx +392 -0
  379. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts +4 -0
  380. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  381. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js +814 -0
  382. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js.map +1 -0
  383. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.tsx +1288 -0
  384. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts +21 -0
  385. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts.map +1 -0
  386. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js +174 -0
  387. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js.map +1 -0
  388. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.tsx +306 -0
  389. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts +10 -0
  390. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts.map +1 -0
  391. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js +12 -0
  392. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js.map +1 -0
  393. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.tsx +29 -0
  394. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts +15 -0
  395. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts.map +1 -0
  396. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js +501 -0
  397. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js.map +1 -0
  398. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.tsx +853 -0
  399. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts +6 -0
  400. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts.map +1 -0
  401. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js +459 -0
  402. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js.map +1 -0
  403. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.tsx +598 -0
  404. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts +6 -0
  405. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts.map +1 -0
  406. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js +876 -0
  407. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js.map +1 -0
  408. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.tsx +1368 -0
  409. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts +4 -0
  410. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts.map +1 -0
  411. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js +2930 -0
  412. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js.map +1 -0
  413. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.tsx +4378 -0
  414. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts +9 -0
  415. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts.map +1 -0
  416. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js +1013 -0
  417. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js.map +1 -0
  418. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.tsx +1745 -0
  419. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts +13 -0
  420. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts.map +1 -0
  421. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js +38 -0
  422. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js.map +1 -0
  423. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.tsx +74 -0
  424. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts +7 -0
  425. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts.map +1 -0
  426. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js +11 -0
  427. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js.map +1 -0
  428. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.tsx +15 -0
  429. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts +18 -0
  430. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  431. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js +406 -0
  432. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js.map +1 -0
  433. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.tsx +660 -0
  434. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts +26 -0
  435. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts.map +1 -0
  436. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js +332 -0
  437. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js.map +1 -0
  438. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.tsx +518 -0
  439. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts +6 -0
  440. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts.map +1 -0
  441. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js +255 -0
  442. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js.map +1 -0
  443. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.tsx +388 -0
  444. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  445. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  446. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js +131 -0
  447. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  448. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  449. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts +108 -0
  450. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts.map +1 -0
  451. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js +162 -0
  452. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js.map +1 -0
  453. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.ts +428 -0
  454. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  455. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  456. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js +36 -0
  457. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js.map +1 -0
  458. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.ts +44 -0
  459. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +836 -0
  460. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -0
  461. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js +3 -0
  462. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js.map +1 -0
  463. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +860 -0
  464. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts +16 -0
  465. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts.map +1 -0
  466. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js +182 -0
  467. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js.map +1 -0
  468. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.ts +250 -0
  469. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts +4 -0
  470. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts.map +1 -0
  471. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js +51 -0
  472. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js.map +1 -0
  473. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.ts +61 -0
  474. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts +2 -0
  475. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts.map +1 -0
  476. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js +954 -0
  477. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js.map +1 -0
  478. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.tsx +1277 -0
  479. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts +2 -0
  480. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts.map +1 -0
  481. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js +488 -0
  482. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js.map +1 -0
  483. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.tsx +805 -0
  484. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts +6 -0
  485. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  486. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js +9 -0
  487. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js.map +1 -0
  488. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.tsx +11 -0
  489. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts +6 -0
  490. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts.map +1 -0
  491. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js +9 -0
  492. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js.map +1 -0
  493. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.tsx +11 -0
  494. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts +2 -0
  495. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts.map +1 -0
  496. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js +8 -0
  497. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js.map +1 -0
  498. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.tsx +5 -0
  499. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts +2 -0
  500. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts.map +1 -0
  501. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js +612 -0
  502. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js.map +1 -0
  503. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.tsx +939 -0
  504. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts +6 -0
  505. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  506. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js +9 -0
  507. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js.map +1 -0
  508. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.tsx +11 -0
  509. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts +6 -0
  510. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts.map +1 -0
  511. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js +9 -0
  512. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js.map +1 -0
  513. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.tsx +11 -0
  514. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts +6 -0
  515. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts.map +1 -0
  516. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js +9 -0
  517. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js.map +1 -0
  518. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.tsx +17 -0
  519. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts +2 -0
  520. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts.map +1 -0
  521. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js +348 -0
  522. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js.map +1 -0
  523. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.tsx +536 -0
  524. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts +2 -0
  525. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts.map +1 -0
  526. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js +401 -0
  527. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js.map +1 -0
  528. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.tsx +607 -0
  529. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts +5 -0
  530. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts.map +1 -0
  531. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js +7 -0
  532. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js.map +1 -0
  533. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.tsx +9 -0
  534. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts +6 -0
  535. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts.map +1 -0
  536. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js +9 -0
  537. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js.map +1 -0
  538. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.tsx +11 -0
  539. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts +2 -0
  540. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts.map +1 -0
  541. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js +321 -0
  542. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js.map +1 -0
  543. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.tsx +440 -0
  544. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts +2 -0
  545. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts.map +1 -0
  546. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js +939 -0
  547. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js.map +1 -0
  548. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.tsx +1499 -0
  549. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts +2 -0
  550. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts.map +1 -0
  551. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js +8 -0
  552. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js.map +1 -0
  553. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.tsx +5 -0
  554. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts +2 -0
  555. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts.map +1 -0
  556. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js +436 -0
  557. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js.map +1 -0
  558. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.tsx +675 -0
  559. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts +2 -0
  560. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts.map +1 -0
  561. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js +563 -0
  562. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js.map +1 -0
  563. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.tsx +846 -0
  564. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts +6 -0
  565. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts.map +1 -0
  566. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js +9 -0
  567. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js.map +1 -0
  568. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.tsx +10 -0
  569. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts +6 -0
  570. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts.map +1 -0
  571. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js +9 -0
  572. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js.map +1 -0
  573. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.tsx +11 -0
  574. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts +6 -0
  575. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts.map +1 -0
  576. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js +9 -0
  577. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js.map +1 -0
  578. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.tsx +11 -0
  579. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts +2 -0
  580. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts.map +1 -0
  581. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js +8 -0
  582. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js.map +1 -0
  583. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.tsx +5 -0
  584. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts +2 -0
  585. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts.map +1 -0
  586. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js +492 -0
  587. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js.map +1 -0
  588. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.tsx +757 -0
  589. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts +2 -0
  590. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts.map +1 -0
  591. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js +342 -0
  592. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js.map +1 -0
  593. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.tsx +430 -0
  594. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts +2 -0
  595. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts.map +1 -0
  596. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js +338 -0
  597. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js.map +1 -0
  598. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.tsx +428 -0
  599. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts +2 -0
  600. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts.map +1 -0
  601. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js +660 -0
  602. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js.map +1 -0
  603. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.tsx +992 -0
  604. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts +2 -0
  605. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts.map +1 -0
  606. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js +515 -0
  607. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js.map +1 -0
  608. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.tsx +707 -0
  609. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts +2 -0
  610. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts.map +1 -0
  611. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js +1141 -0
  612. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js.map +1 -0
  613. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.tsx +1705 -0
  614. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts +2 -0
  615. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts.map +1 -0
  616. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js +8 -0
  617. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js.map +1 -0
  618. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.tsx +5 -0
  619. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts +2 -0
  620. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts.map +1 -0
  621. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js +436 -0
  622. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js.map +1 -0
  623. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.tsx +675 -0
  624. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts +2 -0
  625. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts.map +1 -0
  626. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js +563 -0
  627. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js.map +1 -0
  628. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.tsx +846 -0
  629. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts +6 -0
  630. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts.map +1 -0
  631. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js +9 -0
  632. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js.map +1 -0
  633. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.tsx +11 -0
  634. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts +6 -0
  635. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts.map +1 -0
  636. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js +9 -0
  637. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js.map +1 -0
  638. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.tsx +11 -0
  639. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts +2 -0
  640. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts.map +1 -0
  641. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js +8 -0
  642. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js.map +1 -0
  643. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.tsx +5 -0
  644. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts +2 -0
  645. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts.map +1 -0
  646. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js +492 -0
  647. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js.map +1 -0
  648. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.tsx +757 -0
  649. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts +2 -0
  650. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts.map +1 -0
  651. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js +342 -0
  652. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js.map +1 -0
  653. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.tsx +430 -0
  654. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts +2 -0
  655. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts.map +1 -0
  656. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js +338 -0
  657. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js.map +1 -0
  658. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.tsx +428 -0
  659. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts +2 -0
  660. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts.map +1 -0
  661. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js +660 -0
  662. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js.map +1 -0
  663. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.tsx +992 -0
  664. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts +2 -0
  665. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts.map +1 -0
  666. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js +515 -0
  667. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js.map +1 -0
  668. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.tsx +707 -0
  669. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts +2 -0
  670. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts.map +1 -0
  671. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js +1141 -0
  672. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js.map +1 -0
  673. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.tsx +1705 -0
  674. package/hedhog/table/operations_project_assignment.yaml +1 -0
  675. package/hedhog/table/operations_project_cost.yaml +93 -0
  676. package/hedhog/table/operations_project_cost_category.yaml +37 -0
  677. package/hedhog/table/operations_project_cost_type.yaml +55 -0
  678. package/hedhog/table/operations_task_comment.yaml +26 -0
  679. package/hedhog/table/operations_task_file.yaml +23 -0
  680. package/package.json +5 -5
  681. package/src/controllers/operations-collaborators.controller.ts +26 -0
  682. package/src/controllers/operations-project-costs.controller.ts +249 -0
  683. package/src/controllers/operations-tasks.controller.ts +92 -9
  684. package/src/dto/create-collaborator-project-assignment.dto.ts +14 -0
  685. package/src/dto/create-project-cost-category.dto.ts +37 -0
  686. package/src/dto/create-project-cost-type.dto.ts +64 -0
  687. package/src/dto/create-project-cost.dto.ts +126 -0
  688. package/src/dto/create-task.dto.ts +0 -1
  689. package/src/dto/get-project-cost-report.dto.ts +46 -0
  690. package/src/dto/list-project-cost-categories.dto.ts +17 -0
  691. package/src/dto/list-project-cost-types.dto.ts +28 -0
  692. package/src/dto/list-project-costs.dto.ts +59 -0
  693. package/src/dto/list-tasks.dto.ts +7 -0
  694. package/src/dto/list-timesheets.dto.ts +7 -1
  695. package/src/dto/update-collaborator-project-assignment.dto.ts +58 -0
  696. package/src/dto/update-project-cost-category.dto.ts +4 -0
  697. package/src/dto/update-project-cost-type.dto.ts +4 -0
  698. package/src/dto/update-project-cost.dto.ts +4 -0
  699. package/src/dto/update-task.dto.ts +0 -1
  700. package/src/operations.module.ts +2 -0
  701. package/src/operations.service.ts +2421 -64
@@ -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
@@ -2570,6 +2672,7 @@ export class OperationsService {
2570
2672
  p.code,
2571
2673
  p.name,
2572
2674
  p.client_name AS "clientName",
2675
+ cp.avatar_id AS "clientAvatarId",
2573
2676
  p.summary,
2574
2677
  p.status,
2575
2678
  p.progress_percent AS "progressPercent",
@@ -2580,17 +2683,20 @@ export class OperationsService {
2580
2683
  c.name AS "contractName",
2581
2684
  c.status AS "contractStatus",
2582
2685
  m.display_name AS "managerName",
2686
+ mp.avatar_id AS "managerAvatarId",
2583
2687
  ${ownAssignmentSelect}
2584
2688
  COUNT(DISTINCT pa.id)::int AS "teamSize"
2585
2689
  FROM operations_project p
2586
2690
  LEFT JOIN operations_contract c ON c.id = p.contract_id
2587
2691
  LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
2692
+ LEFT JOIN person cp ON cp.id = p.client_person_id
2693
+ LEFT JOIN person mp ON mp.id = m.person_id
2588
2694
  LEFT JOIN operations_project_assignment pa
2589
2695
  ON pa.project_id = p.id
2590
2696
  AND pa.deleted_at IS NULL
2591
2697
  AND pa.status IN ('planned', 'active')
2592
2698
  WHERE ${whereClause}
2593
- GROUP BY p.id, c.id, m.id`;
2699
+ GROUP BY p.id, c.id, m.id, cp.id, mp.id`;
2594
2700
 
2595
2701
  if (!pagination) {
2596
2702
  return this.queryRows(`${baseQuery} ORDER BY p.name ASC`, params);
@@ -2751,6 +2857,7 @@ export class OperationsService {
2751
2857
  status?: string;
2752
2858
  myOnly?: boolean;
2753
2859
  archived?: boolean;
2860
+ collaboratorId?: number;
2754
2861
  }
2755
2862
  ) {
2756
2863
  const actor = await this.getActorContext(userId);
@@ -2824,6 +2931,13 @@ export class OperationsService {
2824
2931
  filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
2825
2932
  }
2826
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
+
2827
2941
  const whereClause = filters.join(' AND ');
2828
2942
  const totalRow = await this.querySingle<{ total: string }>(
2829
2943
  `SELECT COUNT(*)::text AS total
@@ -2864,6 +2978,8 @@ export class OperationsService {
2864
2978
  assigneeName: string | null;
2865
2979
  assigneeUserPhotoId: number | null;
2866
2980
  assigneePersonAvatarId: number | null;
2981
+ commentCount: number;
2982
+ fileCount: number;
2867
2983
  createdAt: string;
2868
2984
  deletedAt: string | null;
2869
2985
  }>(
@@ -2882,6 +2998,8 @@ export class OperationsService {
2882
2998
  ac.display_name AS "assigneeName",
2883
2999
  au.photo_id AS "assigneeUserPhotoId",
2884
3000
  ap.avatar_id AS "assigneePersonAvatarId",
3001
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
3002
+ COALESCE(task_files.count, 0)::int AS "fileCount",
2885
3003
  t.created_at AS "createdAt",
2886
3004
  t.deleted_at AS "deletedAt"
2887
3005
  FROM operations_task t
@@ -2894,6 +3012,16 @@ export class OperationsService {
2894
3012
  ON au.id = ac.user_id
2895
3013
  LEFT JOIN person ap
2896
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
2897
3025
  JOIN operations_project p
2898
3026
  ON p.id = COALESCE(t.project_id, pa.project_id)
2899
3027
  WHERE ${whereClause}
@@ -2924,29 +3052,39 @@ export class OperationsService {
2924
3052
  this.requireFields(data as Record<string, unknown>, ['name']);
2925
3053
 
2926
3054
  let assignmentId: number | null = null;
2927
- let projectId: number | null = null;
3055
+ let projectId: number | null = data.projectId ?? null;
2928
3056
 
2929
3057
  if (data.projectId || data.projectAssignmentId) {
2930
- const assignment = await this.resolveProjectAssignmentForActor(
2931
- this.prisma,
2932
- actor,
2933
- {
2934
- projectId: data.projectId ?? null,
2935
- projectAssignmentId: data.projectAssignmentId ?? null,
3058
+ if (actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3059
+ const assignment = await this.resolveProjectAssignmentForActor(
3060
+ this.prisma,
3061
+ actor,
3062
+ {
3063
+ projectId: data.projectId ?? null,
3064
+ projectAssignmentId: data.projectAssignmentId ?? null,
3065
+ }
3066
+ );
3067
+ await this.assertProjectAccess(actor, assignment.projectId);
3068
+ assignmentId = assignment.id;
3069
+ projectId = assignment.projectId;
3070
+ } else {
3071
+ if (data.projectId) {
3072
+ await this.assertProjectAccess(actor, data.projectId);
3073
+ projectId = data.projectId;
2936
3074
  }
2937
- );
2938
- await this.assertProjectAccess(actor, assignment.projectId);
2939
- assignmentId = assignment.id;
2940
- projectId = assignment.projectId;
2941
- } else if (data.projectId) {
2942
- projectId = data.projectId;
2943
- await this.assertProjectAccess(actor, projectId);
2944
- } else {
2945
- throw new BadRequestException('Either projectId or projectAssignmentId is required.');
2946
- }
2947
-
2948
- if (!projectId) {
2949
- projectId = data.projectId ?? null;
3075
+ if (data.projectAssignmentId) {
3076
+ const assignment = await this.resolveProjectAssignmentForActor(
3077
+ this.prisma,
3078
+ actor,
3079
+ {
3080
+ projectId: data.projectId ?? null,
3081
+ projectAssignmentId: data.projectAssignmentId,
3082
+ }
3083
+ );
3084
+ assignmentId = assignment.id;
3085
+ projectId = assignment.projectId;
3086
+ }
3087
+ }
2950
3088
  }
2951
3089
 
2952
3090
  const name = this.normalizeOptionalText(data.name);
@@ -2989,7 +3127,7 @@ export class OperationsService {
2989
3127
  [
2990
3128
  projectId,
2991
3129
  assignmentId,
2992
- data.assigneeCollaboratorId ?? actor.collaboratorId ?? null,
3130
+ data.assigneeCollaboratorId ?? null,
2993
3131
  name,
2994
3132
  this.normalizeOptionalText(data.description),
2995
3133
  data.priority ?? 'medium',
@@ -3140,6 +3278,291 @@ export class OperationsService {
3140
3278
  return { success: true };
3141
3279
  }
3142
3280
 
3281
+ async listTaskFiles(userId: number, taskId: number) {
3282
+ const actor = await this.getActorContext(userId);
3283
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3284
+ throw new ForbiddenException(
3285
+ 'Operations collaborator access is required.'
3286
+ );
3287
+ }
3288
+
3289
+ const current = await this.getTaskRecordForActor(
3290
+ this.prisma,
3291
+ actor,
3292
+ taskId
3293
+ );
3294
+ await this.assertProjectAccess(actor, current.projectId);
3295
+
3296
+ const rows = (await (this.prisma as any).$queryRawUnsafe(
3297
+ `SELECT tf.id, tf.file_id, f.filename, f.size, m.name AS mimetype, tf.created_at
3298
+ FROM operations_task_file tf
3299
+ JOIN file f ON f.id = tf.file_id
3300
+ JOIN file_mimetype m ON m.id = f.mimetype_id
3301
+ WHERE tf.operations_task_id = $1
3302
+ ORDER BY tf.created_at ASC`,
3303
+ taskId
3304
+ )) as Array<{
3305
+ id: number;
3306
+ file_id: number;
3307
+ filename: string;
3308
+ size: number;
3309
+ mimetype: string;
3310
+ created_at: Date;
3311
+ }>;
3312
+
3313
+ return rows;
3314
+ }
3315
+
3316
+ async addTaskFile(
3317
+ userId: number,
3318
+ taskId: number,
3319
+ file: MulterFile
3320
+ ) {
3321
+ const actor = await this.getActorContext(userId);
3322
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3323
+ throw new ForbiddenException(
3324
+ 'Operations collaborator access is required.'
3325
+ );
3326
+ }
3327
+
3328
+ const current = await this.getTaskRecordForActor(
3329
+ this.prisma,
3330
+ actor,
3331
+ taskId
3332
+ );
3333
+ await this.assertProjectAccess(actor, current.projectId);
3334
+
3335
+ const uploaded = await this.fileService.upload(
3336
+ `operations/tasks/${taskId}`,
3337
+ file
3338
+ );
3339
+
3340
+ await (this.prisma as any).$executeRawUnsafe(
3341
+ `INSERT INTO operations_task_file (operations_task_id, file_id, created_at, updated_at)
3342
+ VALUES ($1, $2, NOW(), NOW())`,
3343
+ taskId,
3344
+ uploaded.id
3345
+ );
3346
+
3347
+ return uploaded;
3348
+ }
3349
+
3350
+ async removeTaskFile(userId: number, taskId: number, fileRelationId: number) {
3351
+ const actor = await this.getActorContext(userId);
3352
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3353
+ throw new ForbiddenException(
3354
+ 'Operations collaborator access is required.'
3355
+ );
3356
+ }
3357
+
3358
+ const current = await this.getTaskRecordForActor(
3359
+ this.prisma,
3360
+ actor,
3361
+ taskId
3362
+ );
3363
+ await this.assertProjectAccess(actor, current.projectId);
3364
+
3365
+ const rows = (await (this.prisma as any).$queryRawUnsafe(
3366
+ `SELECT file_id FROM operations_task_file WHERE id = $1 AND operations_task_id = $2`,
3367
+ fileRelationId,
3368
+ taskId
3369
+ )) as Array<{ file_id: number }>;
3370
+
3371
+ if (!rows.length) {
3372
+ throw new NotFoundException('Task file attachment not found.');
3373
+ }
3374
+
3375
+ const fileId = rows[0].file_id;
3376
+
3377
+ await (this.prisma as any).$executeRawUnsafe(
3378
+ `DELETE FROM operations_task_file WHERE id = $1`,
3379
+ fileRelationId
3380
+ );
3381
+
3382
+ if (fileId) {
3383
+ await this.fileService.delete('en', { ids: [fileId] });
3384
+ }
3385
+
3386
+ return { success: true };
3387
+ }
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
+
3143
3566
  async listTimesheetEntries(
3144
3567
  userId: number,
3145
3568
  paginationParams: {
@@ -4977,6 +5400,7 @@ export class OperationsService {
4977
5400
  status?: string;
4978
5401
  dateFrom?: string;
4979
5402
  dateTo?: string;
5403
+ collaboratorId?: number;
4980
5404
  } = {}
4981
5405
  ) {
4982
5406
  const actor = await this.getActorContext(userId);
@@ -5003,6 +5427,10 @@ export class OperationsService {
5003
5427
  where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
5004
5428
  }
5005
5429
 
5430
+ if (filters.collaboratorId) {
5431
+ where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
5432
+ }
5433
+
5006
5434
  if (pagination?.search) {
5007
5435
  const searchPlaceholder = this.param(params, `%${pagination.search}%`);
5008
5436
  where.push(`(
@@ -5042,6 +5470,7 @@ export class OperationsService {
5042
5470
  reviewedAt: string | null;
5043
5471
  notes: string | null;
5044
5472
  decisionNote: string | null;
5473
+ approvalId: number | null;
5045
5474
  }>(
5046
5475
  `SELECT t.id,
5047
5476
  t.collaborator_id AS "collaboratorId",
@@ -5055,7 +5484,8 @@ export class OperationsService {
5055
5484
  t.submitted_at AS "submittedAt",
5056
5485
  t.reviewed_at AS "reviewedAt",
5057
5486
  t.notes,
5058
- approval.decision_note AS "decisionNote"
5487
+ approval.decision_note AS "decisionNote",
5488
+ approval.id AS "approvalId"
5059
5489
  FROM operations_timesheet t
5060
5490
  JOIN operations_collaborator c ON c.id = t.collaborator_id
5061
5491
  LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
@@ -7904,6 +8334,7 @@ export class OperationsService {
7904
8334
  assigneeUserPhotoId: number | null;
7905
8335
  assigneePersonAvatarId: number | null;
7906
8336
  projectAssignmentId: number | null;
8337
+ commentCount: number;
7907
8338
  createdAt: string;
7908
8339
  }>(
7909
8340
  `SELECT t.id,
@@ -7920,6 +8351,7 @@ export class OperationsService {
7920
8351
  au.photo_id AS "assigneeUserPhotoId",
7921
8352
  ap.avatar_id AS "assigneePersonAvatarId",
7922
8353
  t.project_assignment_id AS "projectAssignmentId",
8354
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
7923
8355
  t.created_at AS "createdAt"
7924
8356
  FROM operations_task t
7925
8357
  LEFT JOIN operations_collaborator ac
@@ -7928,6 +8360,11 @@ export class OperationsService {
7928
8360
  ON au.id = ac.user_id
7929
8361
  LEFT JOIN person ap
7930
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
7931
8368
  WHERE COALESCE(t.project_id, (
7932
8369
  SELECT pa.project_id FROM operations_project_assignment pa
7933
8370
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -7958,6 +8395,7 @@ export class OperationsService {
7958
8395
  assigneePersonAvatarId: number | null;
7959
8396
  projectAssignmentId: number | null;
7960
8397
  projectId: number | null;
8398
+ commentCount: number;
7961
8399
  createdAt: string;
7962
8400
  deletedAt: string | null;
7963
8401
  }>(
@@ -7976,6 +8414,7 @@ export class OperationsService {
7976
8414
  ap.avatar_id AS "assigneePersonAvatarId",
7977
8415
  t.project_assignment_id AS "projectAssignmentId",
7978
8416
  COALESCE(t.project_id, pa.project_id) AS "projectId",
8417
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
7979
8418
  t.created_at AS "createdAt",
7980
8419
  t.deleted_at AS "deletedAt"
7981
8420
  FROM operations_task t
@@ -7985,6 +8424,11 @@ export class OperationsService {
7985
8424
  LEFT JOIN person ap ON ap.id = ac.person_id
7986
8425
  LEFT JOIN operations_project_assignment pa
7987
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
7988
8432
  WHERE t.id = $1`,
7989
8433
  [taskId]
7990
8434
  );
@@ -10930,12 +11374,18 @@ export class OperationsService {
10930
11374
  au.photo_id AS "assigneeUserPhotoId",
10931
11375
  ap.avatar_id AS "assigneePersonAvatarId",
10932
11376
  t.project_assignment_id AS "projectAssignmentId",
11377
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
10933
11378
  t.created_at AS "createdAt"
10934
11379
  FROM operations_task t
10935
11380
  LEFT JOIN operations_collaborator ac
10936
11381
  ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
10937
11382
  LEFT JOIN "user" au ON au.id = ac.user_id
10938
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
10939
11389
  WHERE COALESCE(t.project_id, (
10940
11390
  SELECT pa.project_id FROM operations_project_assignment pa
10941
11391
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -10999,7 +11449,9 @@ export class OperationsService {
10999
11449
  ];
11000
11450
 
11001
11451
  if (actor.collaboratorId) {
11002
- 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})`);
11003
11455
  }
11004
11456
 
11005
11457
  if (pagination.search) {
@@ -11056,6 +11508,7 @@ export class OperationsService {
11056
11508
  assigneeName: string | null;
11057
11509
  assigneeUserPhotoId: number | null;
11058
11510
  assigneePersonAvatarId: number | null;
11511
+ commentCount: number;
11059
11512
  createdAt: string;
11060
11513
  deletedAt: string | null;
11061
11514
  }>(
@@ -11074,6 +11527,7 @@ export class OperationsService {
11074
11527
  ac.display_name AS "assigneeName",
11075
11528
  au.photo_id AS "assigneeUserPhotoId",
11076
11529
  ap.avatar_id AS "assigneePersonAvatarId",
11530
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
11077
11531
  t.created_at AS "createdAt",
11078
11532
  t.deleted_at AS "deletedAt"
11079
11533
  FROM operations_task t
@@ -11086,6 +11540,11 @@ export class OperationsService {
11086
11540
  ON au.id = ac.user_id
11087
11541
  LEFT JOIN person ap
11088
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
11089
11548
  JOIN operations_project p
11090
11549
  ON p.id = COALESCE(t.project_id, pa.project_id)
11091
11550
  WHERE ${whereClause}
@@ -11320,10 +11779,19 @@ export class OperationsService {
11320
11779
  ? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
11321
11780
  : { revenue: 1, cost: 1, backlog: 1 };
11322
11781
 
11323
- const params: unknown[] = [from, to];
11324
- const where = [
11325
- 'p.deleted_at IS NULL',
11326
- '(p.end_date IS NULL OR p.end_date >= $1::date)',
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];
11792
+ const where = [
11793
+ 'p.deleted_at IS NULL',
11794
+ '(p.end_date IS NULL OR p.end_date >= $1::date)',
11327
11795
  '(p.start_date IS NULL OR p.start_date <= $2::date)',
11328
11796
  ];
11329
11797
  if (filters.client && filters.client !== 'all') {
@@ -11347,6 +11815,8 @@ export class OperationsService {
11347
11815
  weeklyHours: string | null;
11348
11816
  actualHours: string | null;
11349
11817
  billableHours: string | null;
11818
+ realizedCost: string | null;
11819
+ allocatedCost: string | null;
11350
11820
  openTasks: string | null;
11351
11821
  backlogHours: string | null;
11352
11822
  futureDeliveries: string | null;
@@ -11365,6 +11835,8 @@ export class OperationsService {
11365
11835
  COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
11366
11836
  COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
11367
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",
11368
11840
  COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
11369
11841
  COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
11370
11842
  COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
@@ -11393,6 +11865,146 @@ export class OperationsService {
11393
11865
  AND entry.deleted_at IS NULL
11394
11866
  AND entry.work_date BETWEEN $1::date AND $2::date
11395
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
11396
12008
  LEFT JOIN LATERAL (
11397
12009
  SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
11398
12010
  COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
@@ -11406,12 +12018,6 @@ export class OperationsService {
11406
12018
  params
11407
12019
  );
11408
12020
 
11409
- const fromDate = new Date(`${from}T00:00:00`);
11410
- const toDate = new Date(`${to}T00:00:00`);
11411
- const periodWeeks = Math.max(
11412
- 1,
11413
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11414
- );
11415
12021
  const rows = dbRows
11416
12022
  .map((row) => {
11417
12023
  const progress = Number(row.progressPercent ?? 0);
@@ -11419,7 +12025,12 @@ export class OperationsService {
11419
12025
  const recognizedRevenue = contractedRevenue * (progress / 100);
11420
12026
  const actualHours = Number(row.actualHours ?? 0);
11421
12027
  const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
11422
- 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);
11423
12034
  const reportStatus =
11424
12035
  row.status === 'paused'
11425
12036
  ? 'paused'
@@ -11454,7 +12065,7 @@ export class OperationsService {
11454
12065
  contractedRevenue,
11455
12066
  recognizedRevenue,
11456
12067
  realizedCost,
11457
- forecastCost: realizedCost,
12068
+ forecastCost: realizedCost * multiplier.cost,
11458
12069
  teamCost: realizedCost,
11459
12070
  infraCost: 0,
11460
12071
  licenseCost: 0,
@@ -11470,6 +12081,10 @@ export class OperationsService {
11470
12081
  financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
11471
12082
  backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
11472
12083
  futureDeliveries: Number(row.futureDeliveries ?? 0),
12084
+ allocatedCost,
12085
+ consumedHoursCost,
12086
+ idlenessRate,
12087
+ idlenessCost,
11473
12088
  risk,
11474
12089
  recommendation:
11475
12090
  risk === 'alto'
@@ -11495,6 +12110,9 @@ export class OperationsService {
11495
12110
  acc.avgDeadline += row.physicalProgress;
11496
12111
  acc.avgAllocation += row.allocatedCapacity;
11497
12112
  acc.atRisk += row.risk === 'alto' ? 1 : 0;
12113
+ acc.allocatedCost += row.allocatedCost;
12114
+ acc.consumedHoursCost += row.consumedHoursCost;
12115
+ acc.idlenessCost += row.idlenessCost;
11498
12116
  return acc;
11499
12117
  },
11500
12118
  {
@@ -11513,6 +12131,11 @@ export class OperationsService {
11513
12131
  avgAllocation: 0,
11514
12132
  atRisk: 0,
11515
12133
  burnRate: 0,
12134
+ allocatedCost: 0,
12135
+ consumedHoursCost: 0,
12136
+ idlenessCost: 0,
12137
+ idlenessRate: 0,
12138
+ plannedProfit: 0,
11516
12139
  }
11517
12140
  );
11518
12141
  summary.profit = summary.recognizedRevenue - summary.realizedCost;
@@ -11520,6 +12143,10 @@ export class OperationsService {
11520
12143
  summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
11521
12144
  summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
11522
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;
11523
12150
 
11524
12151
  const forecast = Array.from({ length: 12 }, (_, index) => {
11525
12152
  const monthDate = new Date(fromDate);
@@ -11622,6 +12249,15 @@ export class OperationsService {
11622
12249
  : scenario === 'conservative'
11623
12250
  ? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
11624
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
+
11625
12261
  const params: unknown[] = [from, to];
11626
12262
  const where = [
11627
12263
  'c.deleted_at IS NULL',
@@ -11652,6 +12288,11 @@ export class OperationsService {
11652
12288
  taxesCost: string | null;
11653
12289
  toolsCost: string | null;
11654
12290
  billableValue: string | null;
12291
+ plannedAllocatedHours: string | null;
12292
+ plannedBillableHours: string | null;
12293
+ openTaskHours: string | null;
12294
+ openTaskBillableHours: string | null;
12295
+ openTasks: string | null;
11655
12296
  allocatedHours: string | null;
11656
12297
  billableHours: string | null;
11657
12298
  projects: string | null;
@@ -11670,9 +12311,14 @@ export class OperationsService {
11670
12311
  COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
11671
12312
  COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
11672
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",
11673
12319
  COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
11674
12320
  COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
11675
- COALESCE(project_stats.projects, 0)::text AS projects
12321
+ COALESCE(assignment_stats.projects, 0)::text AS projects
11676
12322
  FROM operations_collaborator c
11677
12323
  LEFT JOIN person person_record ON person_record.id = c.person_id
11678
12324
  LEFT JOIN operations_department department_record
@@ -11685,16 +12331,85 @@ export class OperationsService {
11685
12331
  ON collaborator_type.id = c.collaborator_type_id
11686
12332
  AND collaborator_type.deleted_at IS NULL
11687
12333
  LEFT JOIN LATERAL (
11688
- SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
11689
- 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,
11690
- 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,
11691
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
11692
- FROM operations_collaborator_cost cost
11693
- LEFT JOIN operations_cost_type cost_type ON cost_type.id = cost.cost_type_id
11694
- WHERE cost.collaborator_id = c.id
11695
- AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
11696
- 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
11697
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
11698
12413
  LEFT JOIN LATERAL (
11699
12414
  SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
11700
12415
  COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
@@ -11708,31 +12423,50 @@ export class OperationsService {
11708
12423
  AND entry.work_date BETWEEN $1::date AND $2::date
11709
12424
  ) value_stats ON TRUE
11710
12425
  LEFT JOIN LATERAL (
11711
- SELECT COUNT(DISTINCT pa.project_id) AS projects
11712
- FROM operations_project_assignment pa
11713
- 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
11714
12435
  AND pa.deleted_at IS NULL
11715
- AND pa.status IN ('planned', 'active')
11716
- ) 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
11717
12443
  WHERE ${where.join(' AND ')}
11718
12444
  ORDER BY name ASC`,
11719
12445
  params
11720
12446
  );
11721
12447
 
11722
- const fromDate = new Date(`${from}T00:00:00`);
11723
- const toDate = new Date(`${to}T00:00:00`);
11724
- const periodWeeks = Math.max(
11725
- 1,
11726
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11727
- );
11728
12448
  const rows = dbRows.map((row) => {
11729
- const salaryCost = Number(row.salaryCost ?? 0);
11730
- const benefitsCost = Number(row.benefitsCost ?? 0);
11731
- const taxesCost = Number(row.taxesCost ?? 0);
11732
- 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;
11733
12453
  const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
11734
- const allocatedHours = Number(row.allocatedHours ?? 0);
11735
- 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
+ );
11736
12470
  const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
11737
12471
  const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
11738
12472
  return {
@@ -11807,7 +12541,11 @@ export class OperationsService {
11807
12541
  summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
11808
12542
  summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
11809
12543
  summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
11810
- 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;
11811
12549
 
11812
12550
  const forecast = Array.from({ length: 12 }, (_, index) => {
11813
12551
  const monthDate = new Date(fromDate);
@@ -12051,4 +12789,1623 @@ export class OperationsService {
12051
12789
 
12052
12790
  return { success: true };
12053
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
+ }
12054
14411
  }