@hed-hog/operations 0.0.322 → 0.0.326

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 (694) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +25 -0
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/controllers/operations-project-costs.controller.d.ts +422 -0
  6. package/dist/controllers/operations-project-costs.controller.d.ts.map +1 -0
  7. package/dist/controllers/operations-project-costs.controller.js +250 -0
  8. package/dist/controllers/operations-project-costs.controller.js.map +1 -0
  9. package/dist/controllers/operations-reports.controller.d.ts +9 -0
  10. package/dist/controllers/operations-reports.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-tasks.controller.d.ts +42 -0
  12. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  13. package/dist/controllers/operations-tasks.controller.js +48 -0
  14. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  15. package/dist/controllers/operations-timesheets.controller.d.ts +1 -0
  16. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  17. package/dist/dto/create-collaborator-project-assignment.dto.d.ts +5 -0
  18. package/dist/dto/create-collaborator-project-assignment.dto.d.ts.map +1 -0
  19. package/dist/dto/create-collaborator-project-assignment.dto.js +30 -0
  20. package/dist/dto/create-collaborator-project-assignment.dto.js.map +1 -0
  21. package/dist/dto/create-project-cost-category.dto.d.ts +10 -0
  22. package/dist/dto/create-project-cost-category.dto.d.ts.map +1 -0
  23. package/dist/dto/create-project-cost-category.dto.js +59 -0
  24. package/dist/dto/create-project-cost-category.dto.js.map +1 -0
  25. package/dist/dto/create-project-cost-type.dto.d.ts +14 -0
  26. package/dist/dto/create-project-cost-type.dto.d.ts.map +1 -0
  27. package/dist/dto/create-project-cost-type.dto.js +87 -0
  28. package/dist/dto/create-project-cost-type.dto.js.map +1 -0
  29. package/dist/dto/create-project-cost.dto.d.ts +22 -0
  30. package/dist/dto/create-project-cost.dto.d.ts.map +1 -0
  31. package/dist/dto/create-project-cost.dto.js +135 -0
  32. package/dist/dto/create-project-cost.dto.js.map +1 -0
  33. package/dist/dto/get-project-cost-report.dto.d.ts +10 -0
  34. package/dist/dto/get-project-cost-report.dto.d.ts.map +1 -0
  35. package/dist/dto/get-project-cost-report.dto.js +65 -0
  36. package/dist/dto/get-project-cost-report.dto.js.map +1 -0
  37. package/dist/dto/list-project-cost-categories.dto.d.ts +6 -0
  38. package/dist/dto/list-project-cost-categories.dto.d.ts.map +1 -0
  39. package/dist/dto/list-project-cost-categories.dto.js +34 -0
  40. package/dist/dto/list-project-cost-categories.dto.js.map +1 -0
  41. package/dist/dto/list-project-cost-types.dto.d.ts +8 -0
  42. package/dist/dto/list-project-cost-types.dto.d.ts.map +1 -0
  43. package/dist/dto/list-project-cost-types.dto.js +45 -0
  44. package/dist/dto/list-project-cost-types.dto.js.map +1 -0
  45. package/dist/dto/list-project-costs.dto.d.ts +14 -0
  46. package/dist/dto/list-project-costs.dto.d.ts.map +1 -0
  47. package/dist/dto/list-project-costs.dto.js +81 -0
  48. package/dist/dto/list-project-costs.dto.js.map +1 -0
  49. package/dist/dto/list-tasks.dto.d.ts +1 -0
  50. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  51. package/dist/dto/list-tasks.dto.js +6 -0
  52. package/dist/dto/list-tasks.dto.js.map +1 -1
  53. package/dist/dto/list-timesheets.dto.d.ts +1 -0
  54. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  55. package/dist/dto/list-timesheets.dto.js +7 -0
  56. package/dist/dto/list-timesheets.dto.js.map +1 -1
  57. package/dist/dto/update-collaborator-project-assignment.dto.d.ts +11 -0
  58. package/dist/dto/update-collaborator-project-assignment.dto.d.ts.map +1 -0
  59. package/dist/dto/update-collaborator-project-assignment.dto.js +65 -0
  60. package/dist/dto/update-collaborator-project-assignment.dto.js.map +1 -0
  61. package/dist/dto/update-project-cost-category.dto.d.ts +6 -0
  62. package/dist/dto/update-project-cost-category.dto.d.ts.map +1 -0
  63. package/dist/dto/update-project-cost-category.dto.js +9 -0
  64. package/dist/dto/update-project-cost-category.dto.js.map +1 -0
  65. package/dist/dto/update-project-cost-type.dto.d.ts +6 -0
  66. package/dist/dto/update-project-cost-type.dto.d.ts.map +1 -0
  67. package/dist/dto/update-project-cost-type.dto.js +9 -0
  68. package/dist/dto/update-project-cost-type.dto.js.map +1 -0
  69. package/dist/dto/update-project-cost.dto.d.ts +6 -0
  70. package/dist/dto/update-project-cost.dto.d.ts.map +1 -0
  71. package/dist/dto/update-project-cost.dto.js +9 -0
  72. package/dist/dto/update-project-cost.dto.js.map +1 -0
  73. package/dist/operations.module.d.ts.map +1 -1
  74. package/dist/operations.module.js +2 -0
  75. package/dist/operations.module.js.map +1 -1
  76. package/dist/operations.service.d.ts +571 -1
  77. package/dist/operations.service.d.ts.map +1 -1
  78. package/dist/operations.service.js +1793 -69
  79. package/dist/operations.service.js.map +1 -1
  80. package/hedhog/data/integration_event_catalog.yaml +313 -0
  81. package/hedhog/data/menu.yaml +52 -0
  82. package/hedhog/data/operations_project_cost_category.yaml +80 -0
  83. package/hedhog/data/operations_project_cost_type.yaml +503 -0
  84. package/hedhog/data/route.yaml +274 -0
  85. package/hedhog/data/setting_group.yaml +21 -0
  86. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +2 -18
  87. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +593 -297
  88. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -0
  89. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +242 -0
  90. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +533 -296
  91. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -853
  92. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +450 -0
  93. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +602 -0
  94. package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +1401 -0
  95. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2248 -2063
  96. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +56 -11
  97. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +454 -96
  98. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +784 -0
  99. package/hedhog/frontend/app/_lib/api.ts.ejs +256 -0
  100. package/hedhog/frontend/app/_lib/hooks/use-mention-items.ts.ejs +28 -0
  101. package/hedhog/frontend/app/_lib/types.ts.ejs +190 -0
  102. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +9 -3
  103. package/hedhog/frontend/app/collaborators/page.tsx.ejs +18 -7
  104. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +536 -328
  105. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +674 -0
  106. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +845 -0
  107. package/hedhog/frontend/app/projects/[id]/costs-report/page.tsx.ejs +10 -0
  108. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +20 -349
  109. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +217 -485
  110. package/hedhog/frontend/messages/en.json +257 -5
  111. package/hedhog/frontend/messages/en.json.ejs +2060 -0
  112. package/hedhog/frontend/messages/operations/en.json +2068 -0
  113. package/hedhog/frontend/messages/operations/operations/en.json +2102 -0
  114. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  115. package/hedhog/frontend/messages/operations/pt.json +2072 -0
  116. package/hedhog/frontend/messages/pt.json +256 -4
  117. package/hedhog/frontend/messages/pt.json.ejs +2067 -0
  118. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts +29 -0
  119. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.d.ts.map +1 -0
  120. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js +95 -0
  121. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.js.map +1 -0
  122. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/async-options-combobox.tsx +233 -0
  123. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts +10 -0
  124. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  125. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js +577 -0
  126. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.js.map +1 -0
  127. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-costs-section.tsx +868 -0
  128. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts +4 -0
  129. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  130. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js +337 -0
  131. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.js.map +1 -0
  132. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-details-screen.tsx +476 -0
  133. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts +9 -0
  134. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  135. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js +1348 -0
  136. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.js.map +1 -0
  137. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-form-screen.tsx +2233 -0
  138. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts +12 -0
  139. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  140. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js +162 -0
  141. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.js.map +1 -0
  142. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/collaborator-select-with-create.tsx +261 -0
  143. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts +18 -0
  144. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.d.ts.map +1 -0
  145. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js +145 -0
  146. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.js.map +1 -0
  147. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-content-editor.tsx +258 -0
  148. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts +4 -0
  149. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.d.ts.map +1 -0
  150. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js +223 -0
  151. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.js.map +1 -0
  152. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-details-screen.tsx +342 -0
  153. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts +58 -0
  154. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.d.ts.map +1 -0
  155. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js +438 -0
  156. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.js.map +1 -0
  157. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/contract-form-screen.tsx +698 -0
  158. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts +20 -0
  159. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.d.ts.map +1 -0
  160. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js +233 -0
  161. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.js.map +1 -0
  162. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/department-select-with-create.tsx +392 -0
  163. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts +4 -0
  164. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  165. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js +814 -0
  166. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.js.map +1 -0
  167. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/my-project-summary-screen.tsx +1288 -0
  168. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts +21 -0
  169. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.d.ts.map +1 -0
  170. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js +174 -0
  171. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.js.map +1 -0
  172. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-calendar-view.tsx +306 -0
  173. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts +10 -0
  174. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.d.ts.map +1 -0
  175. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js +12 -0
  176. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.js.map +1 -0
  177. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/operations-header.tsx +29 -0
  178. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts +15 -0
  179. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.d.ts.map +1 -0
  180. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js +501 -0
  181. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.js.map +1 -0
  182. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/person-select-with-create.tsx +853 -0
  183. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts +6 -0
  184. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.d.ts.map +1 -0
  185. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js +847 -0
  186. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.js.map +1 -0
  187. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-costs-section.tsx +1340 -0
  188. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts +4 -0
  189. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.d.ts.map +1 -0
  190. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js +2930 -0
  191. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.js.map +1 -0
  192. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-details-screen.tsx +4378 -0
  193. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts +9 -0
  194. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.d.ts.map +1 -0
  195. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js +1013 -0
  196. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.js.map +1 -0
  197. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/project-form-screen.tsx +1745 -0
  198. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts +13 -0
  199. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.d.ts.map +1 -0
  200. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js +38 -0
  201. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.js.map +1 -0
  202. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/section-card.tsx +74 -0
  203. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts +7 -0
  204. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.d.ts.map +1 -0
  205. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js +11 -0
  206. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.js.map +1 -0
  207. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/status-badge.tsx +15 -0
  208. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts +18 -0
  209. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  210. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js +406 -0
  211. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.js.map +1 -0
  212. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/system-user-select-with-create.tsx +660 -0
  213. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts +26 -0
  214. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.d.ts.map +1 -0
  215. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js +332 -0
  216. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.js.map +1 -0
  217. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-detail-sheet.tsx +518 -0
  218. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts +6 -0
  219. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.d.ts.map +1 -0
  220. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js +255 -0
  221. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.js.map +1 -0
  222. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/task-file-attachments.tsx +388 -0
  223. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  224. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  225. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js +131 -0
  226. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  227. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  228. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts +108 -0
  229. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.d.ts.map +1 -0
  230. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js +162 -0
  231. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.js.map +1 -0
  232. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/api.ts +428 -0
  233. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  234. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  235. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js +36 -0
  236. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.js.map +1 -0
  237. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/hooks/use-operations-access.ts +44 -0
  238. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +837 -0
  239. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -0
  240. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js +3 -0
  241. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.js.map +1 -0
  242. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +861 -0
  243. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts +16 -0
  244. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.d.ts.map +1 -0
  245. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js +182 -0
  246. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.js.map +1 -0
  247. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/format.ts +250 -0
  248. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts +4 -0
  249. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.d.ts.map +1 -0
  250. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js +51 -0
  251. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.js.map +1 -0
  252. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/utils/forms.ts +61 -0
  253. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts +2 -0
  254. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.d.ts.map +1 -0
  255. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js +954 -0
  256. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.js.map +1 -0
  257. package/hedhog/frontend/src/app/(app)/(libraries)/operations/approvals/page.tsx +1277 -0
  258. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts +2 -0
  259. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.d.ts.map +1 -0
  260. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js +488 -0
  261. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.js.map +1 -0
  262. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborator-types/page.tsx +805 -0
  263. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts +6 -0
  264. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  265. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js +9 -0
  266. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.js.map +1 -0
  267. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/edit/page.tsx +11 -0
  268. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts +6 -0
  269. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.d.ts.map +1 -0
  270. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js +9 -0
  271. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.js.map +1 -0
  272. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/[id]/page.tsx +11 -0
  273. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts +2 -0
  274. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.d.ts.map +1 -0
  275. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js +8 -0
  276. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.js.map +1 -0
  277. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/new/page.tsx +5 -0
  278. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts +2 -0
  279. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.d.ts.map +1 -0
  280. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js +612 -0
  281. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.js.map +1 -0
  282. package/hedhog/frontend/src/app/(app)/(libraries)/operations/collaborators/page.tsx +939 -0
  283. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts +6 -0
  284. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  285. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js +9 -0
  286. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.js.map +1 -0
  287. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/edit/page.tsx +11 -0
  288. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts +6 -0
  289. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.d.ts.map +1 -0
  290. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js +9 -0
  291. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.js.map +1 -0
  292. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/[id]/page.tsx +11 -0
  293. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts +6 -0
  294. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.d.ts.map +1 -0
  295. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js +9 -0
  296. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.js.map +1 -0
  297. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/new/page.tsx +17 -0
  298. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts +2 -0
  299. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.d.ts.map +1 -0
  300. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js +348 -0
  301. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.js.map +1 -0
  302. package/hedhog/frontend/src/app/(app)/(libraries)/operations/contracts/page.tsx +536 -0
  303. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts +2 -0
  304. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.d.ts.map +1 -0
  305. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js +401 -0
  306. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.js.map +1 -0
  307. package/hedhog/frontend/src/app/(app)/(libraries)/operations/departments/page.tsx +607 -0
  308. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts +5 -0
  309. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.d.ts.map +1 -0
  310. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js +7 -0
  311. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.js.map +1 -0
  312. package/hedhog/frontend/src/app/(app)/(libraries)/operations/layout.tsx +9 -0
  313. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts +6 -0
  314. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.d.ts.map +1 -0
  315. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js +9 -0
  316. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.js.map +1 -0
  317. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/[id]/page.tsx +11 -0
  318. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts +2 -0
  319. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.d.ts.map +1 -0
  320. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js +321 -0
  321. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.js.map +1 -0
  322. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-projects/page.tsx +440 -0
  323. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts +2 -0
  324. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.d.ts.map +1 -0
  325. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js +939 -0
  326. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.js.map +1 -0
  327. package/hedhog/frontend/src/app/(app)/(libraries)/operations/my-tasks/page.tsx +1499 -0
  328. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts +29 -0
  329. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.d.ts.map +1 -0
  330. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js +95 -0
  331. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.js.map +1 -0
  332. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/async-options-combobox.tsx +233 -0
  333. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts +10 -0
  334. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.d.ts.map +1 -0
  335. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js +577 -0
  336. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.js.map +1 -0
  337. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-costs-section.tsx +868 -0
  338. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts +4 -0
  339. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.d.ts.map +1 -0
  340. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js +337 -0
  341. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.js.map +1 -0
  342. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-details-screen.tsx +476 -0
  343. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts +9 -0
  344. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.d.ts.map +1 -0
  345. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js +1348 -0
  346. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.js.map +1 -0
  347. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-form-screen.tsx +2233 -0
  348. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts +12 -0
  349. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.d.ts.map +1 -0
  350. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js +162 -0
  351. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.js.map +1 -0
  352. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/collaborator-select-with-create.tsx +261 -0
  353. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts +18 -0
  354. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.d.ts.map +1 -0
  355. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js +145 -0
  356. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.js.map +1 -0
  357. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-content-editor.tsx +258 -0
  358. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts +4 -0
  359. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.d.ts.map +1 -0
  360. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js +223 -0
  361. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.js.map +1 -0
  362. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-details-screen.tsx +342 -0
  363. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts +58 -0
  364. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.d.ts.map +1 -0
  365. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js +438 -0
  366. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.js.map +1 -0
  367. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/contract-form-screen.tsx +698 -0
  368. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts +20 -0
  369. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.d.ts.map +1 -0
  370. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js +233 -0
  371. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.js.map +1 -0
  372. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/department-select-with-create.tsx +392 -0
  373. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts +4 -0
  374. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.d.ts.map +1 -0
  375. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js +814 -0
  376. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.js.map +1 -0
  377. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/my-project-summary-screen.tsx +1288 -0
  378. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts +21 -0
  379. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.d.ts.map +1 -0
  380. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js +174 -0
  381. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.js.map +1 -0
  382. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-calendar-view.tsx +306 -0
  383. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts +10 -0
  384. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.d.ts.map +1 -0
  385. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js +12 -0
  386. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.js.map +1 -0
  387. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/operations-header.tsx +29 -0
  388. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts +15 -0
  389. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.d.ts.map +1 -0
  390. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js +501 -0
  391. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.js.map +1 -0
  392. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/person-select-with-create.tsx +853 -0
  393. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts +6 -0
  394. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.d.ts.map +1 -0
  395. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js +459 -0
  396. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.js.map +1 -0
  397. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-cost-report-screen.tsx +598 -0
  398. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts +6 -0
  399. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.d.ts.map +1 -0
  400. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js +876 -0
  401. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.js.map +1 -0
  402. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-costs-section.tsx +1368 -0
  403. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts +4 -0
  404. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.d.ts.map +1 -0
  405. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js +2930 -0
  406. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.js.map +1 -0
  407. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-details-screen.tsx +4378 -0
  408. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts +9 -0
  409. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.d.ts.map +1 -0
  410. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js +1013 -0
  411. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.js.map +1 -0
  412. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/project-form-screen.tsx +1745 -0
  413. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts +13 -0
  414. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.d.ts.map +1 -0
  415. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js +38 -0
  416. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.js.map +1 -0
  417. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/section-card.tsx +74 -0
  418. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts +7 -0
  419. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.d.ts.map +1 -0
  420. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js +11 -0
  421. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.js.map +1 -0
  422. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/status-badge.tsx +15 -0
  423. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts +18 -0
  424. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.d.ts.map +1 -0
  425. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js +406 -0
  426. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.js.map +1 -0
  427. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/system-user-select-with-create.tsx +660 -0
  428. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts +26 -0
  429. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.d.ts.map +1 -0
  430. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js +332 -0
  431. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.js.map +1 -0
  432. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-detail-sheet.tsx +518 -0
  433. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts +6 -0
  434. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.d.ts.map +1 -0
  435. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js +255 -0
  436. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.js.map +1 -0
  437. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/task-file-attachments.tsx +388 -0
  438. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts +10 -0
  439. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.d.ts.map +1 -0
  440. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js +131 -0
  441. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.js.map +1 -0
  442. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_components/timesheet-task-create-sheet.tsx +214 -0
  443. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts +108 -0
  444. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.d.ts.map +1 -0
  445. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js +162 -0
  446. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.js.map +1 -0
  447. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/api.ts +428 -0
  448. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts +8 -0
  449. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.d.ts.map +1 -0
  450. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js +36 -0
  451. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.js.map +1 -0
  452. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/hooks/use-operations-access.ts +44 -0
  453. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +837 -0
  454. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -0
  455. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js +3 -0
  456. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.js.map +1 -0
  457. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +861 -0
  458. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts +16 -0
  459. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.d.ts.map +1 -0
  460. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js +182 -0
  461. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.js.map +1 -0
  462. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/format.ts +250 -0
  463. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts +4 -0
  464. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.d.ts.map +1 -0
  465. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js +51 -0
  466. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.js.map +1 -0
  467. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/utils/forms.ts +61 -0
  468. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts +2 -0
  469. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.d.ts.map +1 -0
  470. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js +954 -0
  471. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.js.map +1 -0
  472. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/approvals/page.tsx +1277 -0
  473. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts +2 -0
  474. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.d.ts.map +1 -0
  475. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js +488 -0
  476. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.js.map +1 -0
  477. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborator-types/page.tsx +805 -0
  478. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts +6 -0
  479. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.d.ts.map +1 -0
  480. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js +9 -0
  481. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.js.map +1 -0
  482. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/edit/page.tsx +11 -0
  483. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts +6 -0
  484. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.d.ts.map +1 -0
  485. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js +9 -0
  486. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.js.map +1 -0
  487. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/[id]/page.tsx +11 -0
  488. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts +2 -0
  489. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.d.ts.map +1 -0
  490. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js +8 -0
  491. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.js.map +1 -0
  492. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/new/page.tsx +5 -0
  493. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts +2 -0
  494. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.d.ts.map +1 -0
  495. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js +612 -0
  496. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.js.map +1 -0
  497. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/collaborators/page.tsx +939 -0
  498. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts +6 -0
  499. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.d.ts.map +1 -0
  500. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js +9 -0
  501. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.js.map +1 -0
  502. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/edit/page.tsx +11 -0
  503. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts +6 -0
  504. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.d.ts.map +1 -0
  505. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js +9 -0
  506. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.js.map +1 -0
  507. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/[id]/page.tsx +11 -0
  508. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts +6 -0
  509. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.d.ts.map +1 -0
  510. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js +9 -0
  511. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.js.map +1 -0
  512. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/new/page.tsx +17 -0
  513. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts +2 -0
  514. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.d.ts.map +1 -0
  515. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js +348 -0
  516. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.js.map +1 -0
  517. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/contracts/page.tsx +536 -0
  518. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts +2 -0
  519. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.d.ts.map +1 -0
  520. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js +401 -0
  521. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.js.map +1 -0
  522. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/departments/page.tsx +607 -0
  523. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts +5 -0
  524. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.d.ts.map +1 -0
  525. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js +7 -0
  526. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.js.map +1 -0
  527. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/layout.tsx +9 -0
  528. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts +6 -0
  529. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.d.ts.map +1 -0
  530. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js +9 -0
  531. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.js.map +1 -0
  532. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/[id]/page.tsx +11 -0
  533. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts +2 -0
  534. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.d.ts.map +1 -0
  535. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js +321 -0
  536. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.js.map +1 -0
  537. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-projects/page.tsx +440 -0
  538. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts +2 -0
  539. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.d.ts.map +1 -0
  540. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js +939 -0
  541. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.js.map +1 -0
  542. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/my-tasks/page.tsx +1499 -0
  543. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts +2 -0
  544. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.d.ts.map +1 -0
  545. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js +8 -0
  546. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.js.map +1 -0
  547. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/page.tsx +5 -0
  548. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts +2 -0
  549. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.d.ts.map +1 -0
  550. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js +436 -0
  551. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.js.map +1 -0
  552. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-categories/page.tsx +675 -0
  553. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts +2 -0
  554. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.d.ts.map +1 -0
  555. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js +563 -0
  556. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.js.map +1 -0
  557. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/project-cost-types/page.tsx +846 -0
  558. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts +6 -0
  559. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.d.ts.map +1 -0
  560. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js +9 -0
  561. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.js.map +1 -0
  562. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/costs-report/page.tsx +10 -0
  563. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts +6 -0
  564. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.d.ts.map +1 -0
  565. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js +9 -0
  566. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.js.map +1 -0
  567. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/edit/page.tsx +11 -0
  568. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts +6 -0
  569. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.d.ts.map +1 -0
  570. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js +9 -0
  571. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.js.map +1 -0
  572. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/[id]/page.tsx +11 -0
  573. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts +2 -0
  574. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.d.ts.map +1 -0
  575. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js +8 -0
  576. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.js.map +1 -0
  577. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/new/page.tsx +5 -0
  578. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts +2 -0
  579. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.d.ts.map +1 -0
  580. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js +492 -0
  581. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.js.map +1 -0
  582. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/projects/page.tsx +757 -0
  583. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts +2 -0
  584. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.d.ts.map +1 -0
  585. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js +342 -0
  586. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.js.map +1 -0
  587. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/collaborators/page.tsx +430 -0
  588. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts +2 -0
  589. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.d.ts.map +1 -0
  590. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js +338 -0
  591. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.js.map +1 -0
  592. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/reports/projects/page.tsx +428 -0
  593. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts +2 -0
  594. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.d.ts.map +1 -0
  595. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js +660 -0
  596. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.js.map +1 -0
  597. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/schedule-adjustments/page.tsx +992 -0
  598. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts +2 -0
  599. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.d.ts.map +1 -0
  600. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js +515 -0
  601. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.js.map +1 -0
  602. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/time-off/page.tsx +707 -0
  603. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts +2 -0
  604. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.d.ts.map +1 -0
  605. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js +1141 -0
  606. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.js.map +1 -0
  607. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/timesheets/page.tsx +1705 -0
  608. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts +2 -0
  609. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.d.ts.map +1 -0
  610. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js +8 -0
  611. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.js.map +1 -0
  612. package/hedhog/frontend/src/app/(app)/(libraries)/operations/page.tsx +5 -0
  613. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts +2 -0
  614. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.d.ts.map +1 -0
  615. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js +436 -0
  616. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.js.map +1 -0
  617. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-categories/page.tsx +675 -0
  618. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts +2 -0
  619. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.d.ts.map +1 -0
  620. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js +563 -0
  621. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.js.map +1 -0
  622. package/hedhog/frontend/src/app/(app)/(libraries)/operations/project-cost-types/page.tsx +846 -0
  623. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts +6 -0
  624. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.d.ts.map +1 -0
  625. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js +9 -0
  626. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.js.map +1 -0
  627. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/edit/page.tsx +11 -0
  628. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts +6 -0
  629. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.d.ts.map +1 -0
  630. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js +9 -0
  631. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.js.map +1 -0
  632. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/[id]/page.tsx +11 -0
  633. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts +2 -0
  634. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.d.ts.map +1 -0
  635. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js +8 -0
  636. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.js.map +1 -0
  637. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/new/page.tsx +5 -0
  638. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts +2 -0
  639. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.d.ts.map +1 -0
  640. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js +492 -0
  641. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.js.map +1 -0
  642. package/hedhog/frontend/src/app/(app)/(libraries)/operations/projects/page.tsx +757 -0
  643. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts +2 -0
  644. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.d.ts.map +1 -0
  645. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js +342 -0
  646. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.js.map +1 -0
  647. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/collaborators/page.tsx +430 -0
  648. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts +2 -0
  649. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.d.ts.map +1 -0
  650. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js +338 -0
  651. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.js.map +1 -0
  652. package/hedhog/frontend/src/app/(app)/(libraries)/operations/reports/projects/page.tsx +428 -0
  653. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts +2 -0
  654. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.d.ts.map +1 -0
  655. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js +660 -0
  656. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.js.map +1 -0
  657. package/hedhog/frontend/src/app/(app)/(libraries)/operations/schedule-adjustments/page.tsx +992 -0
  658. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts +2 -0
  659. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.d.ts.map +1 -0
  660. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js +515 -0
  661. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.js.map +1 -0
  662. package/hedhog/frontend/src/app/(app)/(libraries)/operations/time-off/page.tsx +707 -0
  663. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts +2 -0
  664. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.d.ts.map +1 -0
  665. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js +1141 -0
  666. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.js.map +1 -0
  667. package/hedhog/frontend/src/app/(app)/(libraries)/operations/timesheets/page.tsx +1705 -0
  668. package/hedhog/table/operations_collaborator.yaml +5 -0
  669. package/hedhog/table/operations_collaborator_compensation_history.yaml +4 -0
  670. package/hedhog/table/operations_project_assignment.yaml +1 -0
  671. package/hedhog/table/operations_project_cost.yaml +93 -0
  672. package/hedhog/table/operations_project_cost_category.yaml +37 -0
  673. package/hedhog/table/operations_project_cost_type.yaml +55 -0
  674. package/hedhog/table/operations_task_comment.yaml +26 -0
  675. package/package.json +6 -6
  676. package/src/controllers/operations-collaborators.controller.ts +26 -0
  677. package/src/controllers/operations-project-costs.controller.ts +249 -0
  678. package/src/controllers/operations-tasks.controller.ts +49 -0
  679. package/src/dto/create-collaborator-project-assignment.dto.ts +14 -0
  680. package/src/dto/create-project-cost-category.dto.ts +37 -0
  681. package/src/dto/create-project-cost-type.dto.ts +64 -0
  682. package/src/dto/create-project-cost.dto.ts +126 -0
  683. package/src/dto/get-project-cost-report.dto.ts +46 -0
  684. package/src/dto/list-project-cost-categories.dto.ts +17 -0
  685. package/src/dto/list-project-cost-types.dto.ts +28 -0
  686. package/src/dto/list-project-costs.dto.ts +59 -0
  687. package/src/dto/list-tasks.dto.ts +7 -0
  688. package/src/dto/list-timesheets.dto.ts +7 -1
  689. package/src/dto/update-collaborator-project-assignment.dto.ts +58 -0
  690. package/src/dto/update-project-cost-category.dto.ts +4 -0
  691. package/src/dto/update-project-cost-type.dto.ts +4 -0
  692. package/src/dto/update-project-cost.dto.ts +4 -0
  693. package/src/operations.module.ts +2 -0
  694. package/src/operations.service.ts +2472 -61
@@ -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,
@@ -142,6 +143,9 @@ type CollaboratorPayload = {
142
143
  joinedAt?: string | null;
143
144
  leftAt?: string | null;
144
145
  compensationAmount?: number | null;
146
+ hourlyRate?: number | null;
147
+ compensationEffectiveDate?: string | null;
148
+ compensationNotes?: string | null;
145
149
  contractDescription?: string | null;
146
150
  autoGenerateContractDraft?: boolean;
147
151
  weeklySchedule?: Array<{
@@ -544,6 +548,18 @@ type TaskPayload = {
544
548
  archived?: boolean;
545
549
  };
546
550
 
551
+ type TaskCommentRecord = {
552
+ id: number;
553
+ taskId: number;
554
+ content: string;
555
+ actorCollaboratorId: number | null;
556
+ actorName: string | null;
557
+ actorUserPhotoId: number | null;
558
+ actorPersonAvatarId: number | null;
559
+ createdAt: string;
560
+ updatedAt: string | null;
561
+ };
562
+
547
563
  type QuickTimesheetEntryPayload = {
548
564
  projectId?: number | null;
549
565
  projectAssignmentId?: number | null;
@@ -1967,14 +1983,42 @@ export class OperationsService {
1967
1983
  createdCollaboratorId,
1968
1984
  Number(data.compensationAmount),
1969
1985
  actor.userId,
1970
- null
1986
+ data.compensationNotes ?? null,
1987
+ data.compensationEffectiveDate ?? null
1988
+ );
1989
+ }
1990
+
1991
+ if (data.hourlyRate != null) {
1992
+ await (tx as any).$executeRawUnsafe(
1993
+ `UPDATE operations_collaborator SET hourly_rate = $1 WHERE id = $2`,
1994
+ Number(data.hourlyRate),
1995
+ createdCollaboratorId
1996
+ );
1997
+ await this.insertCollaboratorCompensationHistory(
1998
+ tx as any,
1999
+ createdCollaboratorId,
2000
+ Number(data.hourlyRate),
2001
+ actor.userId,
2002
+ data.compensationNotes ?? null,
2003
+ data.compensationEffectiveDate ?? null,
2004
+ 'hourly_rate'
1971
2005
  );
1972
2006
  }
1973
2007
 
1974
2008
  return createdCollaboratorId;
1975
2009
  });
1976
2010
 
1977
- return this.getCollaboratorByIdForUser(userId, collaboratorId);
2011
+ const result = await this.getCollaboratorByIdForUser(userId, collaboratorId);
2012
+
2013
+ await this.integrationApi.publishEvent({
2014
+ eventName: 'operations.collaborator.created',
2015
+ sourceModule: 'operations',
2016
+ aggregateType: 'collaborator',
2017
+ aggregateId: String(collaboratorId),
2018
+ payload: { id: collaboratorId, displayName: resolvedDisplayName, status: normalizedStatus },
2019
+ }).catch(() => null);
2020
+
2021
+ return result;
1978
2022
  }
1979
2023
 
1980
2024
  async updateCollaborator(
@@ -2010,6 +2054,7 @@ export class OperationsService {
2010
2054
  }
2011
2055
  this.pushUpdate(updates, params, 'level_label', data.levelLabel);
2012
2056
  this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
2057
+ this.pushUpdate(updates, params, 'hourly_rate', data.hourlyRate);
2013
2058
  this.pushUpdate(
2014
2059
  updates,
2015
2060
  params,
@@ -2021,7 +2066,18 @@ export class OperationsService {
2021
2066
  this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
2022
2067
  this.pushUpdate(updates, params, 'notes', data.notes);
2023
2068
 
2069
+ let currentHourlyRate: number | null = null;
2070
+
2024
2071
  await this.prisma.$transaction(async (tx) => {
2072
+ if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
2073
+ const curr = (await (tx as any).$queryRawUnsafe(
2074
+ `SELECT hourly_rate AS "hourlyRate" FROM operations_collaborator WHERE id = $1`,
2075
+ collaboratorId
2076
+ )) as { hourlyRate: string | null }[];
2077
+ currentHourlyRate =
2078
+ curr[0]?.hourlyRate != null ? Number(curr[0].hourlyRate) : null;
2079
+ }
2080
+
2025
2081
  if (
2026
2082
  data.collaboratorType !== undefined ||
2027
2083
  data.collaboratorTypeId !== undefined ||
@@ -2114,6 +2170,7 @@ export class OperationsService {
2114
2170
  data.compensationAmount !== undefined ||
2115
2171
  data.contractDescription !== undefined ||
2116
2172
  data.autoGenerateContractDraft !== undefined ||
2173
+ data.hourlyRate !== undefined ||
2117
2174
  data.joinedAt !== undefined ||
2118
2175
  data.weeklyCapacityHours !== undefined ||
2119
2176
  data.supervisorCollaboratorId !== undefined ||
@@ -2124,6 +2181,29 @@ export class OperationsService {
2124
2181
  data.personId !== undefined ||
2125
2182
  data.displayName !== undefined
2126
2183
  ) {
2184
+ let currentBudgetAmount: number | null = null;
2185
+
2186
+ if (
2187
+ data.compensationAmount !== undefined &&
2188
+ data.compensationAmount !== null
2189
+ ) {
2190
+ const hiringContracts = (await (tx as any).$queryRawUnsafe(
2191
+ `SELECT budget_amount AS "budgetAmount"
2192
+ FROM operations_contract
2193
+ WHERE related_collaborator_id = $1
2194
+ AND origin_type = 'employee_hiring'
2195
+ AND deleted_at IS NULL
2196
+ ORDER BY created_at DESC
2197
+ LIMIT 1`,
2198
+ collaboratorId
2199
+ )) as { budgetAmount: string | null }[];
2200
+
2201
+ currentBudgetAmount =
2202
+ hiringContracts[0]?.budgetAmount != null
2203
+ ? Number(hiringContracts[0].budgetAmount)
2204
+ : null;
2205
+ }
2206
+
2127
2207
  await this.syncHiringContractDraft(
2128
2208
  tx as any,
2129
2209
  actor.userId,
@@ -2135,18 +2215,140 @@ export class OperationsService {
2135
2215
  data.compensationAmount !== undefined &&
2136
2216
  data.compensationAmount !== null
2137
2217
  ) {
2138
- await this.insertCollaboratorCompensationHistory(
2139
- tx as any,
2140
- collaboratorId,
2141
- Number(data.compensationAmount),
2142
- actor.userId,
2143
- null
2144
- );
2218
+ const newAmount = Number(data.compensationAmount);
2219
+
2220
+ if (
2221
+ currentBudgetAmount === null ||
2222
+ newAmount !== currentBudgetAmount
2223
+ ) {
2224
+ await this.insertCollaboratorCompensationHistory(
2225
+ tx as any,
2226
+ collaboratorId,
2227
+ newAmount,
2228
+ actor.userId,
2229
+ data.compensationNotes ?? null,
2230
+ data.compensationEffectiveDate ?? null
2231
+ );
2232
+ }
2233
+ }
2234
+
2235
+ if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
2236
+ const newRate = Number(data.hourlyRate);
2237
+ if (currentHourlyRate === null || newRate !== currentHourlyRate) {
2238
+ await this.insertCollaboratorCompensationHistory(
2239
+ tx as any,
2240
+ collaboratorId,
2241
+ newRate,
2242
+ actor.userId,
2243
+ data.compensationNotes ?? null,
2244
+ data.compensationEffectiveDate ?? null,
2245
+ 'hourly_rate'
2246
+ );
2247
+ }
2145
2248
  }
2146
2249
  }
2147
2250
  });
2148
2251
 
2149
- return this.getCollaboratorByIdForUser(userId, collaboratorId);
2252
+ const collaboratorResult = await this.getCollaboratorByIdForUser(userId, collaboratorId);
2253
+
2254
+ await this.integrationApi.publishEvent({
2255
+ eventName: 'operations.collaborator.updated',
2256
+ sourceModule: 'operations',
2257
+ aggregateType: 'collaborator',
2258
+ aggregateId: String(collaboratorId),
2259
+ payload: { id: collaboratorId, displayName: data.displayName, status: data.status },
2260
+ }).catch(() => null);
2261
+
2262
+ return collaboratorResult;
2263
+ }
2264
+
2265
+ async updateCollaboratorProjectAssignment(
2266
+ collaboratorId: number,
2267
+ projectId: number,
2268
+ data: {
2269
+ projectRoleId?: number | null;
2270
+ roleLabel?: string | null;
2271
+ allocationPercent?: number | null;
2272
+ weeklyHours?: number | null;
2273
+ startDate?: string | null;
2274
+ endDate?: string | null;
2275
+ status?: string;
2276
+ }
2277
+ ) {
2278
+ const sets: string[] = [];
2279
+ const params: unknown[] = [collaboratorId, projectId];
2280
+ let idx = 3;
2281
+
2282
+ if ('projectRoleId' in data) {
2283
+ sets.push(`project_role_id = $${idx++}`);
2284
+ params.push(data.projectRoleId ?? null);
2285
+ }
2286
+ if ('roleLabel' in data) {
2287
+ sets.push(`role_label = $${idx++}`);
2288
+ params.push(data.roleLabel ?? null);
2289
+ }
2290
+ if ('allocationPercent' in data) {
2291
+ sets.push(`allocation_percent = $${idx++}`);
2292
+ params.push(data.allocationPercent ?? null);
2293
+ }
2294
+ if ('weeklyHours' in data) {
2295
+ sets.push(`weekly_hours = $${idx++}`);
2296
+ params.push(data.weeklyHours ?? null);
2297
+ }
2298
+ if ('startDate' in data) {
2299
+ sets.push(`start_date = $${idx++}::date`);
2300
+ params.push(data.startDate ?? null);
2301
+ }
2302
+ if ('endDate' in data) {
2303
+ sets.push(`end_date = $${idx++}::date`);
2304
+ params.push(data.endDate ?? null);
2305
+ }
2306
+ if ('status' in data) {
2307
+ sets.push(
2308
+ `status = $${idx++}::operations_project_assignment_status_155b459bbf_enum`
2309
+ );
2310
+ params.push(data.status);
2311
+ }
2312
+
2313
+ if (!sets.length) return { updated: false };
2314
+
2315
+ sets.push(`updated_at = NOW()`);
2316
+
2317
+ await this.prisma.$executeRawUnsafe(
2318
+ `UPDATE operations_project_assignment
2319
+ SET ${sets.join(', ')}
2320
+ WHERE collaborator_id = $1
2321
+ AND project_id = $2
2322
+ AND deleted_at IS NULL`,
2323
+ ...params
2324
+ );
2325
+
2326
+ return { updated: true };
2327
+ }
2328
+
2329
+ async addCollaboratorProjectAssignment(
2330
+ collaboratorId: number,
2331
+ data: { projectId: number; roleLabel?: string }
2332
+ ) {
2333
+ const existing = await this.querySingle<{ id: number }>(
2334
+ `SELECT id FROM operations_project_assignment
2335
+ WHERE collaborator_id = $1 AND project_id = $2 AND deleted_at IS NULL`,
2336
+ [collaboratorId, data.projectId]
2337
+ );
2338
+
2339
+ if (existing) {
2340
+ return { id: existing.id, created: false };
2341
+ }
2342
+
2343
+ const row = await this.querySingle<{ id: number }>(
2344
+ `INSERT INTO operations_project_assignment
2345
+ (collaborator_id, project_id, role_label, status)
2346
+ VALUES ($1, $2, $3, 'active')
2347
+ RETURNING id`,
2348
+ [collaboratorId, data.projectId, data.roleLabel ?? '']
2349
+ );
2350
+
2351
+ return { id: row!.id, created: true };
2150
2352
  }
2151
2353
 
2152
2354
  async getCollaboratorCompensationHistory(
@@ -2165,6 +2367,7 @@ export class OperationsService {
2165
2367
  actorUserId: number | null;
2166
2368
  actorName: string | null;
2167
2369
  notes: string | null;
2370
+ amountType: string;
2168
2371
  createdAt: string;
2169
2372
  }>(
2170
2373
  `SELECT h.id,
@@ -2174,6 +2377,7 @@ export class OperationsService {
2174
2377
  h.actor_user_id AS "actorUserId",
2175
2378
  u.name AS "actorName",
2176
2379
  h.notes,
2380
+ h.amount_type AS "amountType",
2177
2381
  h.created_at AS "createdAt"
2178
2382
  FROM operations_collaborator_compensation_history h
2179
2383
  LEFT JOIN "user" u ON u.id = h.actor_user_id
@@ -2755,6 +2959,7 @@ export class OperationsService {
2755
2959
  status?: string;
2756
2960
  myOnly?: boolean;
2757
2961
  archived?: boolean;
2962
+ collaboratorId?: number;
2758
2963
  }
2759
2964
  ) {
2760
2965
  const actor = await this.getActorContext(userId);
@@ -2828,6 +3033,13 @@ export class OperationsService {
2828
3033
  filters.push(`t.status::text = ${this.param(params, paginationParams.status)}`);
2829
3034
  }
2830
3035
 
3036
+ if (paginationParams.collaboratorId) {
3037
+ const colId = paginationParams.collaboratorId;
3038
+ filters.push(
3039
+ `(pa.collaborator_id = ${this.param(params, colId)} OR t.assignee_collaborator_id = ${this.param(params, colId)})`
3040
+ );
3041
+ }
3042
+
2831
3043
  const whereClause = filters.join(' AND ');
2832
3044
  const totalRow = await this.querySingle<{ total: string }>(
2833
3045
  `SELECT COUNT(*)::text AS total
@@ -2868,6 +3080,8 @@ export class OperationsService {
2868
3080
  assigneeName: string | null;
2869
3081
  assigneeUserPhotoId: number | null;
2870
3082
  assigneePersonAvatarId: number | null;
3083
+ commentCount: number;
3084
+ fileCount: number;
2871
3085
  createdAt: string;
2872
3086
  deletedAt: string | null;
2873
3087
  }>(
@@ -2886,6 +3100,8 @@ export class OperationsService {
2886
3100
  ac.display_name AS "assigneeName",
2887
3101
  au.photo_id AS "assigneeUserPhotoId",
2888
3102
  ap.avatar_id AS "assigneePersonAvatarId",
3103
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
3104
+ COALESCE(task_files.count, 0)::int AS "fileCount",
2889
3105
  t.created_at AS "createdAt",
2890
3106
  t.deleted_at AS "deletedAt"
2891
3107
  FROM operations_task t
@@ -2898,6 +3114,16 @@ export class OperationsService {
2898
3114
  ON au.id = ac.user_id
2899
3115
  LEFT JOIN person ap
2900
3116
  ON ap.id = ac.person_id
3117
+ LEFT JOIN LATERAL (
3118
+ SELECT COUNT(*) AS count
3119
+ FROM operations_task_comment tc
3120
+ WHERE tc.task_id = t.id
3121
+ ) task_comments ON TRUE
3122
+ LEFT JOIN LATERAL (
3123
+ SELECT COUNT(*) AS count
3124
+ FROM operations_task_file tf
3125
+ WHERE tf.operations_task_id = t.id
3126
+ ) task_files ON TRUE
2901
3127
  JOIN operations_project p
2902
3128
  ON p.id = COALESCE(t.project_id, pa.project_id)
2903
3129
  WHERE ${whereClause}
@@ -3015,14 +3241,20 @@ export class OperationsService {
3015
3241
  ]
3016
3242
  );
3017
3243
 
3018
- return this.getProjectBoardTask(created?.id ?? 0);
3244
+ const task = await this.getProjectBoardTask(created?.id ?? 0);
3245
+
3246
+ await this.integrationApi.publishEvent({
3247
+ eventName: 'operations.task.created',
3248
+ sourceModule: 'operations',
3249
+ aggregateType: 'task',
3250
+ aggregateId: String(created?.id ?? 0),
3251
+ payload: { id: created?.id, projectId, name, status: data.status ?? 'todo', priority: data.priority ?? 'medium' },
3252
+ }).catch(() => null);
3253
+
3254
+ return task;
3019
3255
  }
3020
3256
 
3021
- async updateTask(
3022
- userId: number,
3023
- taskId: number,
3024
- data: Partial<TaskPayload>
3025
- ) {
3257
+ async updateTask(userId: number, taskId: number, data: TaskPayload) {
3026
3258
  const actor = await this.getActorContext(userId);
3027
3259
  if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3028
3260
  throw new ForbiddenException(
@@ -3113,7 +3345,17 @@ export class OperationsService {
3113
3345
  );
3114
3346
  });
3115
3347
 
3116
- return this.getProjectBoardTask(taskId);
3348
+ const taskResult = await this.getProjectBoardTask(taskId);
3349
+
3350
+ await this.integrationApi.publishEvent({
3351
+ eventName: 'operations.task.updated',
3352
+ sourceModule: 'operations',
3353
+ aggregateType: 'task',
3354
+ aggregateId: String(taskId),
3355
+ payload: { id: taskId, name: data.name, status: data.status },
3356
+ }).catch(() => null);
3357
+
3358
+ return taskResult;
3117
3359
  }
3118
3360
 
3119
3361
  async removeTask(userId: number, taskId: number, permanent = false) {
@@ -3151,6 +3393,14 @@ export class OperationsService {
3151
3393
  );
3152
3394
  });
3153
3395
 
3396
+ await this.integrationApi.publishEvent({
3397
+ eventName: 'operations.task.deleted',
3398
+ sourceModule: 'operations',
3399
+ aggregateType: 'task',
3400
+ aggregateId: String(taskId),
3401
+ payload: { id: taskId, projectId: current.projectId, permanent },
3402
+ }).catch(() => null);
3403
+
3154
3404
  return { success: true };
3155
3405
  }
3156
3406
 
@@ -3262,6 +3512,205 @@ export class OperationsService {
3262
3512
  return { success: true };
3263
3513
  }
3264
3514
 
3515
+ async listTaskComments(userId: number, taskId: number) {
3516
+ const actor = await this.getActorContext(userId);
3517
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3518
+ throw new ForbiddenException(
3519
+ 'Operations collaborator access is required.'
3520
+ );
3521
+ }
3522
+
3523
+ const current = await this.getTaskRecordForActor(
3524
+ this.prisma,
3525
+ actor,
3526
+ taskId
3527
+ );
3528
+ await this.assertProjectAccess(actor, current.projectId);
3529
+
3530
+ return this.queryRows<TaskCommentRecord>(
3531
+ `SELECT tc.id,
3532
+ tc.task_id AS "taskId",
3533
+ tc.content,
3534
+ tc.actor_collaborator_id AS "actorCollaboratorId",
3535
+ actor.display_name AS "actorName",
3536
+ actor_user.photo_id AS "actorUserPhotoId",
3537
+ actor_person.avatar_id AS "actorPersonAvatarId",
3538
+ tc.created_at AS "createdAt",
3539
+ tc.updated_at AS "updatedAt"
3540
+ FROM operations_task_comment tc
3541
+ LEFT JOIN operations_collaborator actor
3542
+ ON actor.id = tc.actor_collaborator_id
3543
+ AND actor.deleted_at IS NULL
3544
+ LEFT JOIN "user" actor_user
3545
+ ON actor_user.id = actor.user_id
3546
+ LEFT JOIN person actor_person
3547
+ ON actor_person.id = actor.person_id
3548
+ WHERE tc.task_id = $1
3549
+ ORDER BY tc.created_at ASC, tc.id ASC`,
3550
+ [taskId]
3551
+ );
3552
+ }
3553
+
3554
+ async addTaskComment(userId: number, taskId: number, content: string) {
3555
+ const actor = await this.getActorContext(userId);
3556
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3557
+ throw new ForbiddenException(
3558
+ 'Operations collaborator access is required.'
3559
+ );
3560
+ }
3561
+
3562
+ const current = await this.getTaskRecordForActor(
3563
+ this.prisma,
3564
+ actor,
3565
+ taskId
3566
+ );
3567
+ await this.assertProjectAccess(actor, current.projectId);
3568
+
3569
+ const normalizedContent = this.normalizeOptionalText(content);
3570
+ if (!normalizedContent) {
3571
+ throw new BadRequestException('Comment content is required.');
3572
+ }
3573
+
3574
+ const inserted = await this.queryRows<{ id: number }>(
3575
+ `INSERT INTO operations_task_comment (
3576
+ task_id,
3577
+ actor_collaborator_id,
3578
+ content,
3579
+ created_at,
3580
+ updated_at
3581
+ ) VALUES ($1, $2, $3, NOW(), NOW())
3582
+ RETURNING id`,
3583
+ [taskId, actor.collaboratorId ?? null, normalizedContent]
3584
+ );
3585
+
3586
+ const commentId = inserted[0]?.id;
3587
+ const comments = await this.listTaskComments(userId, taskId);
3588
+ const createdComment = comments.find((comment) => comment.id === commentId);
3589
+
3590
+ if (!createdComment) {
3591
+ throw new NotFoundException('Task comment could not be loaded.');
3592
+ }
3593
+
3594
+ return createdComment;
3595
+ }
3596
+
3597
+ async updateTaskComment(
3598
+ userId: number,
3599
+ taskId: number,
3600
+ commentId: number,
3601
+ content: string
3602
+ ) {
3603
+ const actor = await this.getActorContext(userId);
3604
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3605
+ throw new ForbiddenException(
3606
+ 'Operations collaborator access is required.'
3607
+ );
3608
+ }
3609
+
3610
+ const current = await this.getTaskRecordForActor(
3611
+ this.prisma,
3612
+ actor,
3613
+ taskId
3614
+ );
3615
+ await this.assertProjectAccess(actor, current.projectId);
3616
+
3617
+ const normalizedContent = this.normalizeOptionalText(content);
3618
+ if (!normalizedContent) {
3619
+ throw new BadRequestException('Comment content is required.');
3620
+ }
3621
+
3622
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
3623
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
3624
+ FROM operations_task_comment
3625
+ WHERE id = $1 AND task_id = $2`,
3626
+ [commentId, taskId]
3627
+ );
3628
+
3629
+ const row = rows[0];
3630
+ if (!row) {
3631
+ throw new NotFoundException('Comment not found.');
3632
+ }
3633
+
3634
+ if (row.actorCollaboratorId !== actor.collaboratorId) {
3635
+ throw new ForbiddenException('You can only edit your own comments.');
3636
+ }
3637
+
3638
+ const editSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
3639
+ const editWindowMinutes = Number(editSettings['operations.comment-edit-window'] ?? 5);
3640
+ if (editWindowMinutes > 0) {
3641
+ const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
3642
+ if (diffMinutes > editWindowMinutes) {
3643
+ throw new ForbiddenException(
3644
+ `Comments can only be edited within ${editWindowMinutes} minute(s) of posting.`
3645
+ );
3646
+ }
3647
+ }
3648
+
3649
+ await this.queryRows(
3650
+ `UPDATE operations_task_comment
3651
+ SET content = $1, updated_at = NOW()
3652
+ WHERE id = $2`,
3653
+ [normalizedContent, commentId]
3654
+ );
3655
+
3656
+ const comments = await this.listTaskComments(userId, taskId);
3657
+ return comments.find((c) => c.id === commentId) ?? null;
3658
+ }
3659
+
3660
+ async removeTaskComment(
3661
+ userId: number,
3662
+ taskId: number,
3663
+ commentId: number
3664
+ ) {
3665
+ const actor = await this.getActorContext(userId);
3666
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3667
+ throw new ForbiddenException(
3668
+ 'Operations collaborator access is required.'
3669
+ );
3670
+ }
3671
+
3672
+ const current = await this.getTaskRecordForActor(
3673
+ this.prisma,
3674
+ actor,
3675
+ taskId
3676
+ );
3677
+ await this.assertProjectAccess(actor, current.projectId);
3678
+
3679
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
3680
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
3681
+ FROM operations_task_comment
3682
+ WHERE id = $1 AND task_id = $2`,
3683
+ [commentId, taskId]
3684
+ );
3685
+
3686
+ const row = rows[0];
3687
+ if (!row) {
3688
+ throw new NotFoundException('Comment not found.');
3689
+ }
3690
+
3691
+ if (row.actorCollaboratorId !== actor.collaboratorId) {
3692
+ throw new ForbiddenException('You can only delete your own comments.');
3693
+ }
3694
+
3695
+ const deleteSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
3696
+ const deleteWindowMinutes = Number(deleteSettings['operations.comment-edit-window'] ?? 5);
3697
+ if (deleteWindowMinutes > 0) {
3698
+ const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
3699
+ if (diffMinutes > deleteWindowMinutes) {
3700
+ throw new ForbiddenException(
3701
+ `Comments can only be deleted within ${deleteWindowMinutes} minute(s) of posting.`
3702
+ );
3703
+ }
3704
+ }
3705
+
3706
+ await this.queryRows(
3707
+ `DELETE FROM operations_task_comment WHERE id = $1`,
3708
+ [commentId]
3709
+ );
3710
+
3711
+ return { success: true };
3712
+ }
3713
+
3265
3714
  async listTimesheetEntries(
3266
3715
  userId: number,
3267
3716
  paginationParams: {
@@ -3761,7 +4210,17 @@ export class OperationsService {
3761
4210
  return projectId;
3762
4211
  });
3763
4212
 
3764
- return this.getProjectById(userId, createdProjectId);
4213
+ const result = await this.getProjectById(userId, createdProjectId);
4214
+
4215
+ await this.integrationApi.publishEvent({
4216
+ eventName: 'operations.project.created',
4217
+ sourceModule: 'operations',
4218
+ aggregateType: 'project',
4219
+ aggregateId: String(createdProjectId),
4220
+ payload: { id: createdProjectId, code: data.code, name: data.name, status: data.status ?? 'planning' },
4221
+ }).catch(() => null);
4222
+
4223
+ return result;
3765
4224
  }
3766
4225
 
3767
4226
  async updateProject(userId: number, projectId: number, data: Partial<ProjectPayload>) {
@@ -3858,7 +4317,17 @@ export class OperationsService {
3858
4317
  }
3859
4318
  });
3860
4319
 
3861
- return this.getProjectById(userId, projectId);
4320
+ const projectResult = await this.getProjectById(userId, projectId);
4321
+
4322
+ await this.integrationApi.publishEvent({
4323
+ eventName: 'operations.project.updated',
4324
+ sourceModule: 'operations',
4325
+ aggregateType: 'project',
4326
+ aggregateId: String(projectId),
4327
+ payload: { id: projectId, name: data.name, status: data.status },
4328
+ }).catch(() => null);
4329
+
4330
+ return projectResult;
3862
4331
  }
3863
4332
 
3864
4333
  async listContracts(
@@ -5099,6 +5568,7 @@ export class OperationsService {
5099
5568
  status?: string;
5100
5569
  dateFrom?: string;
5101
5570
  dateTo?: string;
5571
+ collaboratorId?: number;
5102
5572
  } = {}
5103
5573
  ) {
5104
5574
  const actor = await this.getActorContext(userId);
@@ -5125,6 +5595,10 @@ export class OperationsService {
5125
5595
  where.push(`t.week_start_date <= ${this.param(params, filters.dateTo)}::date`);
5126
5596
  }
5127
5597
 
5598
+ if (filters.collaboratorId) {
5599
+ where.push(`t.collaborator_id = ${this.param(params, filters.collaboratorId)}`);
5600
+ }
5601
+
5128
5602
  if (pagination?.search) {
5129
5603
  const searchPlaceholder = this.param(params, `%${pagination.search}%`);
5130
5604
  where.push(`(
@@ -5164,6 +5638,7 @@ export class OperationsService {
5164
5638
  reviewedAt: string | null;
5165
5639
  notes: string | null;
5166
5640
  decisionNote: string | null;
5641
+ approvalId: number | null;
5167
5642
  }>(
5168
5643
  `SELECT t.id,
5169
5644
  t.collaborator_id AS "collaboratorId",
@@ -5177,7 +5652,8 @@ export class OperationsService {
5177
5652
  t.submitted_at AS "submittedAt",
5178
5653
  t.reviewed_at AS "reviewedAt",
5179
5654
  t.notes,
5180
- approval.decision_note AS "decisionNote"
5655
+ approval.decision_note AS "decisionNote",
5656
+ approval.id AS "approvalId"
5181
5657
  FROM operations_timesheet t
5182
5658
  JOIN operations_collaborator c ON c.id = t.collaborator_id
5183
5659
  LEFT JOIN operations_collaborator a ON a.id = t.approver_collaborator_id
@@ -7545,6 +8021,7 @@ export class OperationsService {
7545
8021
  title: string | null;
7546
8022
  levelLabel: string | null;
7547
8023
  weeklyCapacityHours: number | null;
8024
+ hourlyRate: number | null;
7548
8025
  status: string;
7549
8026
  joinedAt: string | null;
7550
8027
  leftAt: string | null;
@@ -7572,6 +8049,7 @@ export class OperationsService {
7572
8049
  COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
7573
8050
  c.level_label AS "levelLabel",
7574
8051
  c.weekly_capacity_hours AS "weeklyCapacityHours",
8052
+ c.hourly_rate AS "hourlyRate",
7575
8053
  c.status,
7576
8054
  c.joined_at AS "joinedAt",
7577
8055
  c.left_at AS "leftAt",
@@ -8026,6 +8504,7 @@ export class OperationsService {
8026
8504
  assigneeUserPhotoId: number | null;
8027
8505
  assigneePersonAvatarId: number | null;
8028
8506
  projectAssignmentId: number | null;
8507
+ commentCount: number;
8029
8508
  createdAt: string;
8030
8509
  }>(
8031
8510
  `SELECT t.id,
@@ -8042,6 +8521,7 @@ export class OperationsService {
8042
8521
  au.photo_id AS "assigneeUserPhotoId",
8043
8522
  ap.avatar_id AS "assigneePersonAvatarId",
8044
8523
  t.project_assignment_id AS "projectAssignmentId",
8524
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
8045
8525
  t.created_at AS "createdAt"
8046
8526
  FROM operations_task t
8047
8527
  LEFT JOIN operations_collaborator ac
@@ -8050,6 +8530,11 @@ export class OperationsService {
8050
8530
  ON au.id = ac.user_id
8051
8531
  LEFT JOIN person ap
8052
8532
  ON ap.id = ac.person_id
8533
+ LEFT JOIN LATERAL (
8534
+ SELECT COUNT(*) AS count
8535
+ FROM operations_task_comment tc
8536
+ WHERE tc.task_id = t.id
8537
+ ) task_comments ON TRUE
8053
8538
  WHERE COALESCE(t.project_id, (
8054
8539
  SELECT pa.project_id FROM operations_project_assignment pa
8055
8540
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -8080,6 +8565,7 @@ export class OperationsService {
8080
8565
  assigneePersonAvatarId: number | null;
8081
8566
  projectAssignmentId: number | null;
8082
8567
  projectId: number | null;
8568
+ commentCount: number;
8083
8569
  createdAt: string;
8084
8570
  deletedAt: string | null;
8085
8571
  }>(
@@ -8098,6 +8584,7 @@ export class OperationsService {
8098
8584
  ap.avatar_id AS "assigneePersonAvatarId",
8099
8585
  t.project_assignment_id AS "projectAssignmentId",
8100
8586
  COALESCE(t.project_id, pa.project_id) AS "projectId",
8587
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
8101
8588
  t.created_at AS "createdAt",
8102
8589
  t.deleted_at AS "deletedAt"
8103
8590
  FROM operations_task t
@@ -8107,6 +8594,11 @@ export class OperationsService {
8107
8594
  LEFT JOIN person ap ON ap.id = ac.person_id
8108
8595
  LEFT JOIN operations_project_assignment pa
8109
8596
  ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
8597
+ LEFT JOIN LATERAL (
8598
+ SELECT COUNT(*) AS count
8599
+ FROM operations_task_comment tc
8600
+ WHERE tc.task_id = t.id
8601
+ ) task_comments ON TRUE
8110
8602
  WHERE t.id = $1`,
8111
8603
  [taskId]
8112
8604
  );
@@ -9824,7 +10316,9 @@ export class OperationsService {
9824
10316
  collaboratorId: number,
9825
10317
  amount: number,
9826
10318
  actorUserId: number | null,
9827
- notes: string | null
10319
+ notes: string | null,
10320
+ effectiveDate?: string | null,
10321
+ amountType: 'salary' | 'hourly_rate' = 'salary'
9828
10322
  ) {
9829
10323
  await client.$executeRawUnsafe(
9830
10324
  `INSERT INTO operations_collaborator_compensation_history (
@@ -9832,14 +10326,18 @@ export class OperationsService {
9832
10326
  amount,
9833
10327
  actor_user_id,
9834
10328
  notes,
10329
+ effective_date,
10330
+ amount_type,
9835
10331
  created_at
9836
10332
  ) VALUES (
9837
- $1, $2, $3, $4, NOW()
10333
+ $1, $2, $3, $4, $5::date, $6::operations_collaborator_compensation_history_am_f803c4196e_enum, NOW()
9838
10334
  )`,
9839
10335
  collaboratorId,
9840
10336
  amount,
9841
10337
  actorUserId,
9842
- notes ?? null
10338
+ notes ?? null,
10339
+ effectiveDate ?? null,
10340
+ amountType
9843
10341
  );
9844
10342
  }
9845
10343
 
@@ -11052,12 +11550,18 @@ export class OperationsService {
11052
11550
  au.photo_id AS "assigneeUserPhotoId",
11053
11551
  ap.avatar_id AS "assigneePersonAvatarId",
11054
11552
  t.project_assignment_id AS "projectAssignmentId",
11553
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
11055
11554
  t.created_at AS "createdAt"
11056
11555
  FROM operations_task t
11057
11556
  LEFT JOIN operations_collaborator ac
11058
11557
  ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
11059
11558
  LEFT JOIN "user" au ON au.id = ac.user_id
11060
11559
  LEFT JOIN person ap ON ap.id = ac.person_id
11560
+ LEFT JOIN LATERAL (
11561
+ SELECT COUNT(*) AS count
11562
+ FROM operations_task_comment tc
11563
+ WHERE tc.task_id = t.id
11564
+ ) task_comments ON TRUE
11061
11565
  WHERE COALESCE(t.project_id, (
11062
11566
  SELECT pa.project_id FROM operations_project_assignment pa
11063
11567
  WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
@@ -11121,7 +11625,9 @@ export class OperationsService {
11121
11625
  ];
11122
11626
 
11123
11627
  if (actor.collaboratorId) {
11124
- filters.push(`pa.collaborator_id = ${this.param(params, actor.collaboratorId)}`);
11628
+ const p1 = this.param(params, actor.collaboratorId);
11629
+ const p2 = this.param(params, actor.collaboratorId);
11630
+ filters.push(`(pa.collaborator_id = ${p1} OR t.assignee_collaborator_id = ${p2})`);
11125
11631
  }
11126
11632
 
11127
11633
  if (pagination.search) {
@@ -11178,6 +11684,7 @@ export class OperationsService {
11178
11684
  assigneeName: string | null;
11179
11685
  assigneeUserPhotoId: number | null;
11180
11686
  assigneePersonAvatarId: number | null;
11687
+ commentCount: number;
11181
11688
  createdAt: string;
11182
11689
  deletedAt: string | null;
11183
11690
  }>(
@@ -11196,6 +11703,7 @@ export class OperationsService {
11196
11703
  ac.display_name AS "assigneeName",
11197
11704
  au.photo_id AS "assigneeUserPhotoId",
11198
11705
  ap.avatar_id AS "assigneePersonAvatarId",
11706
+ COALESCE(task_comments.count, 0)::int AS "commentCount",
11199
11707
  t.created_at AS "createdAt",
11200
11708
  t.deleted_at AS "deletedAt"
11201
11709
  FROM operations_task t
@@ -11208,6 +11716,11 @@ export class OperationsService {
11208
11716
  ON au.id = ac.user_id
11209
11717
  LEFT JOIN person ap
11210
11718
  ON ap.id = ac.person_id
11719
+ LEFT JOIN LATERAL (
11720
+ SELECT COUNT(*) AS count
11721
+ FROM operations_task_comment tc
11722
+ WHERE tc.task_id = t.id
11723
+ ) task_comments ON TRUE
11211
11724
  JOIN operations_project p
11212
11725
  ON p.id = COALESCE(t.project_id, pa.project_id)
11213
11726
  WHERE ${whereClause}
@@ -11442,7 +11955,16 @@ export class OperationsService {
11442
11955
  ? { revenue: 0.9, cost: 0.96, backlog: 0.82 }
11443
11956
  : { revenue: 1, cost: 1, backlog: 1 };
11444
11957
 
11445
- const params: unknown[] = [from, to];
11958
+ const fromDate = new Date(`${from}T00:00:00`);
11959
+ const toDate = new Date(`${to}T00:00:00`);
11960
+ const periodDays = Math.max(
11961
+ 1,
11962
+ Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
11963
+ );
11964
+ const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
11965
+ const periodMonths = periodDays / 30.4375;
11966
+
11967
+ const params: unknown[] = [from, to, periodMonths, periodWeeks];
11446
11968
  const where = [
11447
11969
  'p.deleted_at IS NULL',
11448
11970
  '(p.end_date IS NULL OR p.end_date >= $1::date)',
@@ -11469,6 +11991,8 @@ export class OperationsService {
11469
11991
  weeklyHours: string | null;
11470
11992
  actualHours: string | null;
11471
11993
  billableHours: string | null;
11994
+ realizedCost: string | null;
11995
+ allocatedCost: string | null;
11472
11996
  openTasks: string | null;
11473
11997
  backlogHours: string | null;
11474
11998
  futureDeliveries: string | null;
@@ -11487,6 +12011,8 @@ export class OperationsService {
11487
12011
  COALESCE(assignment_stats.weekly_hours, 0)::text AS "weeklyHours",
11488
12012
  COALESCE(time_stats.actual_hours, 0)::text AS "actualHours",
11489
12013
  COALESCE(time_stats.billable_hours, 0)::text AS "billableHours",
12014
+ COALESCE(cost_stats.realized_cost, 0)::text AS "realizedCost",
12015
+ COALESCE(alloc_cost_stats.allocated_cost, 0)::text AS "allocatedCost",
11490
12016
  COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
11491
12017
  COALESCE(task_stats.backlog_hours, 0)::text AS "backlogHours",
11492
12018
  COALESCE(task_stats.future_deliveries, 0)::text AS "futureDeliveries"
@@ -11515,6 +12041,146 @@ export class OperationsService {
11515
12041
  AND entry.deleted_at IS NULL
11516
12042
  AND entry.work_date BETWEEN $1::date AND $2::date
11517
12043
  ) time_stats ON TRUE
12044
+ LEFT JOIN LATERAL (
12045
+ SELECT COALESCE(
12046
+ SUM(
12047
+ entry.hours
12048
+ * (
12049
+ (
12050
+ COALESCE(collaborator_costs.salary_cost, 0)
12051
+ + COALESCE(collaborator_costs.benefits_cost, 0)
12052
+ + COALESCE(collaborator_costs.taxes_cost, 0)
12053
+ + COALESCE(collaborator_costs.tools_cost, 0)
12054
+ )
12055
+ * $3::numeric
12056
+ / GREATEST(
12057
+ COALESCE(collaborator_record.weekly_capacity_hours, 40)::numeric * $4::numeric,
12058
+ COALESCE(collaborator_hours.total_hours, 0),
12059
+ 1
12060
+ )
12061
+ )
12062
+ ),
12063
+ 0
12064
+ ) AS realized_cost
12065
+ FROM operations_timesheet_entry entry
12066
+ JOIN operations_project_assignment pa
12067
+ ON pa.id = entry.project_assignment_id
12068
+ AND pa.deleted_at IS NULL
12069
+ JOIN operations_collaborator collaborator_record
12070
+ ON collaborator_record.id = pa.collaborator_id
12071
+ AND collaborator_record.deleted_at IS NULL
12072
+ LEFT JOIN LATERAL (
12073
+ SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
12074
+ cost_totals.benefits_cost,
12075
+ cost_totals.taxes_cost,
12076
+ cost_totals.tools_cost
12077
+ FROM (
12078
+ SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
12079
+ 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,
12080
+ 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,
12081
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
12082
+ FROM operations_collaborator_cost cost
12083
+ LEFT JOIN operations_cost_type cost_type
12084
+ ON cost_type.id = cost.cost_type_id
12085
+ WHERE cost.collaborator_id = collaborator_record.id
12086
+ AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
12087
+ AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
12088
+ ) cost_totals
12089
+ LEFT JOIN LATERAL (
12090
+ SELECT h.amount
12091
+ FROM operations_collaborator_compensation_history h
12092
+ WHERE h.collaborator_id = collaborator_record.id
12093
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
12094
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
12095
+ LIMIT 1
12096
+ ) compensation_history ON TRUE
12097
+ LEFT JOIN LATERAL (
12098
+ SELECT oc.budget_amount
12099
+ FROM operations_contract oc
12100
+ WHERE oc.related_collaborator_id = collaborator_record.id
12101
+ AND oc.deleted_at IS NULL
12102
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
12103
+ oc.created_at DESC
12104
+ LIMIT 1
12105
+ ) hiring_contract ON TRUE
12106
+ ) collaborator_costs ON TRUE
12107
+ LEFT JOIN LATERAL (
12108
+ SELECT COALESCE(SUM(entry2.hours), 0) AS total_hours
12109
+ FROM operations_timesheet_entry entry2
12110
+ JOIN operations_project_assignment pa2
12111
+ ON pa2.id = entry2.project_assignment_id
12112
+ AND pa2.deleted_at IS NULL
12113
+ WHERE pa2.collaborator_id = collaborator_record.id
12114
+ AND entry2.deleted_at IS NULL
12115
+ AND entry2.work_date BETWEEN $1::date AND $2::date
12116
+ ) collaborator_hours ON TRUE
12117
+ WHERE pa.project_id = p.id
12118
+ AND entry.deleted_at IS NULL
12119
+ AND entry.work_date BETWEEN $1::date AND $2::date
12120
+ ) cost_stats ON TRUE
12121
+ LEFT JOIN LATERAL (
12122
+ SELECT COALESCE(
12123
+ SUM(
12124
+ pa.weekly_hours
12125
+ * (
12126
+ (
12127
+ COALESCE(alloc_costs.salary_cost, 0)
12128
+ + COALESCE(alloc_costs.benefits_cost, 0)
12129
+ + COALESCE(alloc_costs.taxes_cost, 0)
12130
+ + COALESCE(alloc_costs.tools_cost, 0)
12131
+ )
12132
+ * $3::numeric
12133
+ / GREATEST(
12134
+ COALESCE(alloc_col.weekly_capacity_hours, 40)::numeric,
12135
+ 1
12136
+ )
12137
+ )
12138
+ ),
12139
+ 0
12140
+ ) AS allocated_cost
12141
+ FROM operations_project_assignment pa
12142
+ JOIN operations_collaborator alloc_col
12143
+ ON alloc_col.id = pa.collaborator_id
12144
+ AND alloc_col.deleted_at IS NULL
12145
+ LEFT JOIN LATERAL (
12146
+ SELECT COALESCE(NULLIF(ct.salary_cost, 0), ch.amount, hc.budget_amount, 0) AS salary_cost,
12147
+ ct.benefits_cost,
12148
+ ct.taxes_cost,
12149
+ ct.tools_cost
12150
+ FROM (
12151
+ SELECT COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
12152
+ 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,
12153
+ 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,
12154
+ COALESCE(SUM(c.amount) FILTER (WHERE c.recurrence::text = 'monthly' AND ct2.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
12155
+ FROM operations_collaborator_cost c
12156
+ LEFT JOIN operations_cost_type ct2
12157
+ ON ct2.id = c.cost_type_id
12158
+ WHERE c.collaborator_id = alloc_col.id
12159
+ AND (c.start_date IS NULL OR c.start_date <= $2::date)
12160
+ AND (c.end_date IS NULL OR c.end_date >= $1::date)
12161
+ ) ct
12162
+ LEFT JOIN LATERAL (
12163
+ SELECT h.amount
12164
+ FROM operations_collaborator_compensation_history h
12165
+ WHERE h.collaborator_id = alloc_col.id
12166
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
12167
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
12168
+ LIMIT 1
12169
+ ) ch ON TRUE
12170
+ LEFT JOIN LATERAL (
12171
+ SELECT oc.budget_amount
12172
+ FROM operations_contract oc
12173
+ WHERE oc.related_collaborator_id = alloc_col.id
12174
+ AND oc.deleted_at IS NULL
12175
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
12176
+ oc.created_at DESC
12177
+ LIMIT 1
12178
+ ) hc ON TRUE
12179
+ ) alloc_costs ON TRUE
12180
+ WHERE pa.project_id = p.id
12181
+ AND pa.deleted_at IS NULL
12182
+ AND pa.status IN ('planned', 'active')
12183
+ ) alloc_cost_stats ON TRUE
11518
12184
  LEFT JOIN LATERAL (
11519
12185
  SELECT COUNT(*) FILTER (WHERE task.status IN ('todo', 'doing', 'review')) AS open_tasks,
11520
12186
  COALESCE(SUM(task.estimate_hours) FILTER (WHERE task.status IN ('todo', 'doing', 'review')), 0) AS backlog_hours,
@@ -11528,12 +12194,6 @@ export class OperationsService {
11528
12194
  params
11529
12195
  );
11530
12196
 
11531
- const fromDate = new Date(`${from}T00:00:00`);
11532
- const toDate = new Date(`${to}T00:00:00`);
11533
- const periodWeeks = Math.max(
11534
- 1,
11535
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11536
- );
11537
12197
  const rows = dbRows
11538
12198
  .map((row) => {
11539
12199
  const progress = Number(row.progressPercent ?? 0);
@@ -11541,7 +12201,12 @@ export class OperationsService {
11541
12201
  const recognizedRevenue = contractedRevenue * (progress / 100);
11542
12202
  const actualHours = Number(row.actualHours ?? 0);
11543
12203
  const plannedHours = Math.max(Number(row.weeklyHours ?? 0) * periodWeeks, actualHours);
11544
- const realizedCost = 0;
12204
+ const realizedCost = Number(row.realizedCost ?? 0);
12205
+ const allocatedCost = Number(row.allocatedCost ?? 0);
12206
+ const consumedHoursCost = realizedCost;
12207
+ const idlenessHours = Math.max(plannedHours - actualHours, 0);
12208
+ const idlenessRate = plannedHours > 0 ? (idlenessHours / plannedHours) * 100 : 0;
12209
+ const idlenessCost = Math.max(allocatedCost - consumedHoursCost, 0);
11545
12210
  const reportStatus =
11546
12211
  row.status === 'paused'
11547
12212
  ? 'paused'
@@ -11576,7 +12241,7 @@ export class OperationsService {
11576
12241
  contractedRevenue,
11577
12242
  recognizedRevenue,
11578
12243
  realizedCost,
11579
- forecastCost: realizedCost,
12244
+ forecastCost: realizedCost * multiplier.cost,
11580
12245
  teamCost: realizedCost,
11581
12246
  infraCost: 0,
11582
12247
  licenseCost: 0,
@@ -11592,6 +12257,10 @@ export class OperationsService {
11592
12257
  financialProgress: contractedRevenue ? (recognizedRevenue / contractedRevenue) * 100 : 0,
11593
12258
  backlogValue: Math.max(contractedRevenue - recognizedRevenue, 0),
11594
12259
  futureDeliveries: Number(row.futureDeliveries ?? 0),
12260
+ allocatedCost,
12261
+ consumedHoursCost,
12262
+ idlenessRate,
12263
+ idlenessCost,
11595
12264
  risk,
11596
12265
  recommendation:
11597
12266
  risk === 'alto'
@@ -11617,6 +12286,9 @@ export class OperationsService {
11617
12286
  acc.avgDeadline += row.physicalProgress;
11618
12287
  acc.avgAllocation += row.allocatedCapacity;
11619
12288
  acc.atRisk += row.risk === 'alto' ? 1 : 0;
12289
+ acc.allocatedCost += row.allocatedCost;
12290
+ acc.consumedHoursCost += row.consumedHoursCost;
12291
+ acc.idlenessCost += row.idlenessCost;
11620
12292
  return acc;
11621
12293
  },
11622
12294
  {
@@ -11635,6 +12307,11 @@ export class OperationsService {
11635
12307
  avgAllocation: 0,
11636
12308
  atRisk: 0,
11637
12309
  burnRate: 0,
12310
+ allocatedCost: 0,
12311
+ consumedHoursCost: 0,
12312
+ idlenessCost: 0,
12313
+ idlenessRate: 0,
12314
+ plannedProfit: 0,
11638
12315
  }
11639
12316
  );
11640
12317
  summary.profit = summary.recognizedRevenue - summary.realizedCost;
@@ -11642,6 +12319,10 @@ export class OperationsService {
11642
12319
  summary.avgDeadline = rows.length ? summary.avgDeadline / rows.length : 0;
11643
12320
  summary.avgAllocation = rows.length ? summary.avgAllocation / rows.length : 0;
11644
12321
  summary.burnRate = summary.plannedHours ? (summary.actualHours / summary.plannedHours) * 100 : 0;
12322
+ summary.plannedProfit = summary.contractedRevenue - summary.allocatedCost;
12323
+ summary.idlenessRate = summary.plannedHours > 0
12324
+ ? Math.max(0, (summary.plannedHours - summary.actualHours) / summary.plannedHours * 100)
12325
+ : 0;
11645
12326
 
11646
12327
  const forecast = Array.from({ length: 12 }, (_, index) => {
11647
12328
  const monthDate = new Date(fromDate);
@@ -11744,6 +12425,15 @@ export class OperationsService {
11744
12425
  : scenario === 'conservative'
11745
12426
  ? { revenue: 0.9, cost: 0.96, capacity: 0.94 }
11746
12427
  : { revenue: 1, cost: 1, capacity: 1 };
12428
+ const fromDate = new Date(`${from}T00:00:00`);
12429
+ const toDate = new Date(`${to}T00:00:00`);
12430
+ const periodDays = Math.max(
12431
+ 1,
12432
+ Math.floor((toDate.getTime() - fromDate.getTime()) / 86400000) + 1
12433
+ );
12434
+ const periodWeeks = Math.max(1, Math.ceil(periodDays / 7));
12435
+ const periodMonths = periodDays / 30.4375;
12436
+
11747
12437
  const params: unknown[] = [from, to];
11748
12438
  const where = [
11749
12439
  'c.deleted_at IS NULL',
@@ -11774,6 +12464,11 @@ export class OperationsService {
11774
12464
  taxesCost: string | null;
11775
12465
  toolsCost: string | null;
11776
12466
  billableValue: string | null;
12467
+ plannedAllocatedHours: string | null;
12468
+ plannedBillableHours: string | null;
12469
+ openTaskHours: string | null;
12470
+ openTaskBillableHours: string | null;
12471
+ openTasks: string | null;
11777
12472
  allocatedHours: string | null;
11778
12473
  billableHours: string | null;
11779
12474
  projects: string | null;
@@ -11792,9 +12487,14 @@ export class OperationsService {
11792
12487
  COALESCE(cost_stats.taxes_cost, 0)::text AS "taxesCost",
11793
12488
  COALESCE(cost_stats.tools_cost, 0)::text AS "toolsCost",
11794
12489
  COALESCE(value_stats.billable_value, 0)::text AS "billableValue",
12490
+ COALESCE(assignment_stats.planned_allocated_hours, 0)::text AS "plannedAllocatedHours",
12491
+ COALESCE(assignment_stats.planned_billable_hours, 0)::text AS "plannedBillableHours",
12492
+ COALESCE(task_stats.open_task_hours, 0)::text AS "openTaskHours",
12493
+ COALESCE(task_stats.open_task_billable_hours, 0)::text AS "openTaskBillableHours",
12494
+ COALESCE(task_stats.open_tasks, 0)::text AS "openTasks",
11795
12495
  COALESCE(value_stats.allocated_hours, 0)::text AS "allocatedHours",
11796
12496
  COALESCE(value_stats.billable_hours, 0)::text AS "billableHours",
11797
- COALESCE(project_stats.projects, 0)::text AS projects
12497
+ COALESCE(assignment_stats.projects, 0)::text AS projects
11798
12498
  FROM operations_collaborator c
11799
12499
  LEFT JOIN person person_record ON person_record.id = c.person_id
11800
12500
  LEFT JOIN operations_department department_record
@@ -11807,16 +12507,85 @@ export class OperationsService {
11807
12507
  ON collaborator_type.id = c.collaborator_type_id
11808
12508
  AND collaborator_type.deleted_at IS NULL
11809
12509
  LEFT JOIN LATERAL (
11810
- SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
11811
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('vale-refeicao', 'vale-alimentacao', 'vale-transporte', 'plano-saude', 'plano-odontologico', 'seguro-vida')), 0) AS benefits_cost,
11812
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('inss-patronal', 'fgts', 'rat-fap', 'terceiros-sistema-s', 'provisao-decimo-terceiro', 'provisao-ferias')), 0) AS taxes_cost,
11813
- COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
11814
- FROM operations_collaborator_cost cost
11815
- LEFT JOIN operations_cost_type cost_type ON cost_type.id = cost.cost_type_id
11816
- WHERE cost.collaborator_id = c.id
11817
- AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
11818
- AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
12510
+ SELECT COALESCE(NULLIF(cost_totals.salary_cost, 0), compensation_history.amount, hiring_contract.budget_amount, 0) AS salary_cost,
12511
+ cost_totals.benefits_cost,
12512
+ cost_totals.taxes_cost,
12513
+ cost_totals.tools_cost
12514
+ FROM (
12515
+ SELECT COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('salario-base', 'pro-labore')), 0) AS salary_cost,
12516
+ 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,
12517
+ 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,
12518
+ COALESCE(SUM(cost.amount) FILTER (WHERE cost.recurrence::text = 'monthly' AND cost_type.slug IN ('software-licenca', 'equipamento')), 0) AS tools_cost
12519
+ FROM operations_collaborator_cost cost
12520
+ LEFT JOIN operations_cost_type cost_type
12521
+ ON cost_type.id = cost.cost_type_id
12522
+ WHERE cost.collaborator_id = c.id
12523
+ AND (cost.start_date IS NULL OR cost.start_date <= $2::date)
12524
+ AND (cost.end_date IS NULL OR cost.end_date >= $1::date)
12525
+ ) cost_totals
12526
+ LEFT JOIN LATERAL (
12527
+ SELECT h.amount
12528
+ FROM operations_collaborator_compensation_history h
12529
+ WHERE h.collaborator_id = c.id
12530
+ AND (h.effective_date IS NULL OR h.effective_date <= $2::date)
12531
+ ORDER BY h.effective_date DESC NULLS LAST, h.created_at DESC
12532
+ LIMIT 1
12533
+ ) compensation_history ON TRUE
12534
+ LEFT JOIN LATERAL (
12535
+ SELECT oc.budget_amount
12536
+ FROM operations_contract oc
12537
+ WHERE oc.related_collaborator_id = c.id
12538
+ AND oc.deleted_at IS NULL
12539
+ ORDER BY CASE WHEN oc.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
12540
+ oc.created_at DESC
12541
+ LIMIT 1
12542
+ ) hiring_contract ON TRUE
11819
12543
  ) cost_stats ON TRUE
12544
+ LEFT JOIN LATERAL (
12545
+ SELECT COALESCE(
12546
+ SUM(
12547
+ COALESCE(
12548
+ pa.weekly_hours,
12549
+ COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
12550
+ ) * GREATEST(
12551
+ CEIL(
12552
+ (
12553
+ LEAST(COALESCE(pa.end_date, $2::date), $2::date)
12554
+ - GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
12555
+ + 1
12556
+ ) / 7.0
12557
+ ),
12558
+ 0
12559
+ )
12560
+ ),
12561
+ 0
12562
+ ) AS planned_allocated_hours,
12563
+ COALESCE(
12564
+ SUM(
12565
+ COALESCE(
12566
+ pa.weekly_hours,
12567
+ COALESCE(c.weekly_capacity_hours, 40) * COALESCE(pa.allocation_percent, 0) / 100
12568
+ ) * GREATEST(
12569
+ CEIL(
12570
+ (
12571
+ LEAST(COALESCE(pa.end_date, $2::date), $2::date)
12572
+ - GREATEST(COALESCE(pa.start_date, $1::date), $1::date)
12573
+ + 1
12574
+ ) / 7.0
12575
+ ),
12576
+ 0
12577
+ )
12578
+ ) FILTER (WHERE pa.is_billable = true),
12579
+ 0
12580
+ ) AS planned_billable_hours,
12581
+ COUNT(DISTINCT pa.project_id) AS projects
12582
+ FROM operations_project_assignment pa
12583
+ WHERE pa.collaborator_id = c.id
12584
+ AND pa.deleted_at IS NULL
12585
+ AND pa.status IN ('planned', 'active')
12586
+ AND (pa.start_date IS NULL OR pa.start_date <= $2::date)
12587
+ AND (pa.end_date IS NULL OR pa.end_date >= $1::date)
12588
+ ) assignment_stats ON TRUE
11820
12589
  LEFT JOIN LATERAL (
11821
12590
  SELECT COALESCE(SUM(entry.hours), 0) AS allocated_hours,
11822
12591
  COALESCE(SUM(entry.hours) FILTER (WHERE pa.is_billable = true), 0) AS billable_hours,
@@ -11830,31 +12599,50 @@ export class OperationsService {
11830
12599
  AND entry.work_date BETWEEN $1::date AND $2::date
11831
12600
  ) value_stats ON TRUE
11832
12601
  LEFT JOIN LATERAL (
11833
- SELECT COUNT(DISTINCT pa.project_id) AS projects
11834
- FROM operations_project_assignment pa
11835
- WHERE pa.collaborator_id = c.id
12602
+ SELECT COUNT(*) AS open_tasks,
12603
+ COALESCE(SUM(COALESCE(task.estimate_hours, 0)), 0) AS open_task_hours,
12604
+ COALESCE(
12605
+ SUM(COALESCE(task.estimate_hours, 0)) FILTER (WHERE pa.is_billable = true),
12606
+ 0
12607
+ ) AS open_task_billable_hours
12608
+ FROM operations_task task
12609
+ LEFT JOIN operations_project_assignment pa
12610
+ ON pa.id = task.project_assignment_id
11836
12611
  AND pa.deleted_at IS NULL
11837
- AND pa.status IN ('planned', 'active')
11838
- ) project_stats ON TRUE
12612
+ WHERE task.deleted_at IS NULL
12613
+ AND task.status IN ('todo', 'doing', 'review')
12614
+ AND (
12615
+ task.assignee_collaborator_id = c.id
12616
+ OR pa.collaborator_id = c.id
12617
+ )
12618
+ ) task_stats ON TRUE
11839
12619
  WHERE ${where.join(' AND ')}
11840
12620
  ORDER BY name ASC`,
11841
12621
  params
11842
12622
  );
11843
12623
 
11844
- const fromDate = new Date(`${from}T00:00:00`);
11845
- const toDate = new Date(`${to}T00:00:00`);
11846
- const periodWeeks = Math.max(
11847
- 1,
11848
- Math.ceil((toDate.getTime() - fromDate.getTime()) / 604800000)
11849
- );
11850
12624
  const rows = dbRows.map((row) => {
11851
- const salaryCost = Number(row.salaryCost ?? 0);
11852
- const benefitsCost = Number(row.benefitsCost ?? 0);
11853
- const taxesCost = Number(row.taxesCost ?? 0);
11854
- const toolsCost = Number(row.toolsCost ?? 0);
12625
+ const salaryCost = Number(row.salaryCost ?? 0) * periodMonths;
12626
+ const benefitsCost = Number(row.benefitsCost ?? 0) * periodMonths;
12627
+ const taxesCost = Number(row.taxesCost ?? 0) * periodMonths;
12628
+ const toolsCost = Number(row.toolsCost ?? 0) * periodMonths;
11855
12629
  const availableHours = Number(row.weeklyCapacityHours ?? 40) * periodWeeks;
11856
- const allocatedHours = Number(row.allocatedHours ?? 0);
11857
- const billableHours = Number(row.billableHours ?? 0);
12630
+ const plannedAllocatedHours = Number(row.plannedAllocatedHours ?? 0);
12631
+ const plannedBillableHours = Number(row.plannedBillableHours ?? 0);
12632
+ const openTaskHours = Number(row.openTaskHours ?? 0);
12633
+ const openTaskBillableHours = Number(row.openTaskBillableHours ?? 0);
12634
+ const actualAllocatedHours = Number(row.allocatedHours ?? 0);
12635
+ const actualBillableHours = Number(row.billableHours ?? 0);
12636
+ const allocatedHours = Math.max(
12637
+ actualAllocatedHours,
12638
+ plannedAllocatedHours,
12639
+ openTaskHours
12640
+ );
12641
+ const billableHours = Math.max(
12642
+ actualBillableHours,
12643
+ plannedBillableHours,
12644
+ openTaskBillableHours
12645
+ );
11858
12646
  const allocation = availableHours ? (allocatedHours / availableHours) * 100 : 0;
11859
12647
  const risk = allocation >= 98 ? 'alto' : allocation < 75 ? 'médio' : 'baixo';
11860
12648
  return {
@@ -11929,7 +12717,11 @@ export class OperationsService {
11929
12717
  summary.freeHours = Math.max(summary.availableHours - summary.allocatedHours, 0);
11930
12718
  summary.allocation = summary.availableHours ? (summary.allocatedHours / summary.availableHours) * 100 : 0;
11931
12719
  summary.utilization = summary.availableHours ? (summary.billableHours / summary.availableHours) * 100 : 0;
11932
- summary.hourlyCost = summary.allocatedHours ? summary.cost / summary.allocatedHours : 0;
12720
+ summary.hourlyCost = summary.allocatedHours
12721
+ ? summary.cost / summary.allocatedHours
12722
+ : summary.availableHours
12723
+ ? summary.cost / summary.availableHours
12724
+ : 0;
11933
12725
 
11934
12726
  const forecast = Array.from({ length: 12 }, (_, index) => {
11935
12727
  const monthDate = new Date(fromDate);
@@ -12173,4 +12965,1623 @@ export class OperationsService {
12173
12965
 
12174
12966
  return { success: true };
12175
12967
  }
12968
+
12969
+ // ──────────────────────────────────────────────────────────────────────────
12970
+ // Project Cost Categories
12971
+ // ──────────────────────────────────────────────────────────────────────────
12972
+
12973
+ async listProjectCostCategories(userId: number, filters: { search?: string; is_active?: boolean; pageSize?: number; page?: number } = {}) {
12974
+ await this.getActorContext(userId);
12975
+ const localeId = await this.resolvePreferredLocaleId();
12976
+
12977
+ const params: unknown[] = [localeId];
12978
+ const where: string[] = ['pcc.deleted_at IS NULL'];
12979
+
12980
+ if (filters.is_active === true) {
12981
+ where.push('pcc.is_active = true');
12982
+ }
12983
+
12984
+ if (filters.search?.trim()) {
12985
+ const p = this.param(params, `%${filters.search.trim()}%`);
12986
+ where.push(`(COALESCE(pccl.name, pcc.slug) ILIKE ${p} OR COALESCE(pcc.slug, '') ILIKE ${p})`);
12987
+ }
12988
+
12989
+ const whereClause = `WHERE ${where.join(' AND ')}`;
12990
+
12991
+ return this.queryRows<{
12992
+ id: number;
12993
+ slug: string;
12994
+ name: string | null;
12995
+ description: string | null;
12996
+ icon: string | null;
12997
+ color: string | null;
12998
+ isActive: boolean;
12999
+ sortOrder: number;
13000
+ createdAt: string;
13001
+ }>(
13002
+ `SELECT pcc.id,
13003
+ pcc.slug,
13004
+ COALESCE(pccl.name, pcc.slug) AS name,
13005
+ pccl.description,
13006
+ pcc.icon,
13007
+ pcc.color,
13008
+ pcc.is_active AS "isActive",
13009
+ pcc.sort_order AS "sortOrder",
13010
+ pcc.created_at AS "createdAt"
13011
+ FROM operations_project_cost_category pcc
13012
+ LEFT JOIN LATERAL (
13013
+ SELECT l.name, l.description
13014
+ FROM operations_project_cost_category_locale l
13015
+ WHERE l.operations_project_cost_category_id = pcc.id
13016
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13017
+ l.id ASC
13018
+ LIMIT 1
13019
+ ) pccl ON TRUE
13020
+ ${whereClause}
13021
+ ORDER BY pcc.sort_order ASC, COALESCE(pccl.name, pcc.slug) ASC`,
13022
+ params
13023
+ );
13024
+ }
13025
+
13026
+ async createProjectCostCategory(userId: number, data: { slug: string; name?: any; description?: any; icon?: string | null; color?: string | null; is_active?: boolean; sort_order?: number }) {
13027
+ const actor = await this.getActorContext(userId);
13028
+ this.ensureDirector(actor);
13029
+
13030
+ const slug = data.slug?.trim();
13031
+ if (!slug) {
13032
+ throw new BadRequestException('Cost category slug is required.');
13033
+ }
13034
+
13035
+ return this.prisma.$transaction(async (tx) => {
13036
+ const localeId = await this.resolvePreferredLocaleId(tx as any);
13037
+
13038
+ const created = (await (tx as any).$queryRawUnsafe(
13039
+ `INSERT INTO operations_project_cost_category (slug, icon, color, is_active, sort_order, created_at, updated_at)
13040
+ VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
13041
+ RETURNING id`,
13042
+ slug,
13043
+ data.icon ?? null,
13044
+ data.color ?? null,
13045
+ data.is_active ?? true,
13046
+ data.sort_order ?? 0,
13047
+ )) as { id: number }[];
13048
+
13049
+ const createdId = created[0]?.id;
13050
+ if (!createdId) {
13051
+ throw new BadRequestException('Unable to create project cost category.');
13052
+ }
13053
+
13054
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
13055
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13056
+
13057
+ if (localeId && name) {
13058
+ await (tx as any).$executeRawUnsafe(
13059
+ `INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description)
13060
+ VALUES ($1, $2, $3, $4)`,
13061
+ createdId,
13062
+ localeId,
13063
+ name,
13064
+ description ?? null,
13065
+ );
13066
+ }
13067
+
13068
+ const rows = (await (tx as any).$queryRawUnsafe(
13069
+ `SELECT pcc.id,
13070
+ pcc.slug,
13071
+ COALESCE(pccl.name, pcc.slug) AS name,
13072
+ pccl.description,
13073
+ pcc.icon,
13074
+ pcc.color,
13075
+ pcc.is_active AS "isActive",
13076
+ pcc.sort_order AS "sortOrder",
13077
+ pcc.created_at AS "createdAt"
13078
+ FROM operations_project_cost_category pcc
13079
+ LEFT JOIN operations_project_cost_category_locale pccl
13080
+ ON pccl.operations_project_cost_category_id = pcc.id AND pccl.locale_id = $2
13081
+ WHERE pcc.id = $1`,
13082
+ createdId,
13083
+ localeId,
13084
+ )) as { id: number; slug: string; name: string; description: string | null; icon: string | null; color: string | null; isActive: boolean; sortOrder: number; createdAt: string }[];
13085
+
13086
+ return rows[0] ?? null;
13087
+ });
13088
+ }
13089
+
13090
+ 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 }>) {
13091
+ const actor = await this.getActorContext(userId);
13092
+ this.ensureDirector(actor);
13093
+
13094
+ const category = await this.querySingle<{ id: number }>(
13095
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13096
+ [id]
13097
+ );
13098
+ if (!category) {
13099
+ throw new NotFoundException('Project cost category not found.');
13100
+ }
13101
+
13102
+ const sets: string[] = [];
13103
+ const params: unknown[] = [];
13104
+
13105
+ if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
13106
+ if (data.icon !== undefined) sets.push(`icon = ${this.param(params, data.icon)}`);
13107
+ if (data.color !== undefined) sets.push(`color = ${this.param(params, data.color)}`);
13108
+ if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
13109
+ if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
13110
+
13111
+ if (sets.length > 0) {
13112
+ sets.push(`updated_at = NOW()`);
13113
+ await this.prisma.$queryRawUnsafe(
13114
+ `UPDATE operations_project_cost_category SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
13115
+ ...params
13116
+ );
13117
+ }
13118
+
13119
+ if (data.name !== undefined || data.description !== undefined) {
13120
+ const localeId = await this.resolvePreferredLocaleId();
13121
+ if (localeId) {
13122
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
13123
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13124
+ const existing = await this.querySingle<{ id: number }>(
13125
+ `SELECT id FROM operations_project_cost_category_locale WHERE operations_project_cost_category_id = $1 AND locale_id = $2 LIMIT 1`,
13126
+ [id, localeId]
13127
+ );
13128
+ if (existing) {
13129
+ const localeSets: string[] = [];
13130
+ const localeParams: unknown[] = [];
13131
+ if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
13132
+ if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
13133
+ if (localeSets.length > 0) {
13134
+ await this.prisma.$queryRawUnsafe(
13135
+ `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)}`,
13136
+ ...localeParams
13137
+ );
13138
+ }
13139
+ } else if (name) {
13140
+ await this.prisma.$queryRawUnsafe(
13141
+ `INSERT INTO operations_project_cost_category_locale (operations_project_cost_category_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
13142
+ id, localeId, name, description ?? null
13143
+ );
13144
+ }
13145
+ }
13146
+ }
13147
+
13148
+ return this.querySingle<{ id: number; slug: string }>(
13149
+ `SELECT id, slug FROM operations_project_cost_category WHERE id = $1`,
13150
+ [id]
13151
+ );
13152
+ }
13153
+
13154
+ async deleteProjectCostCategory(userId: number, id: number) {
13155
+ const actor = await this.getActorContext(userId);
13156
+ this.ensureDirector(actor);
13157
+
13158
+ const category = await this.querySingle<{ id: number }>(
13159
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13160
+ [id]
13161
+ );
13162
+ if (!category) {
13163
+ throw new NotFoundException('Project cost category not found.');
13164
+ }
13165
+
13166
+ await this.prisma.$queryRawUnsafe(
13167
+ `UPDATE operations_project_cost_category SET deleted_at = NOW() WHERE id = $1`,
13168
+ id
13169
+ );
13170
+
13171
+ return { success: true };
13172
+ }
13173
+
13174
+ async getProjectCostCategory(userId: number, id: number) {
13175
+ await this.getActorContext(userId);
13176
+ const localeId = await this.resolvePreferredLocaleId();
13177
+
13178
+ const row = await this.querySingle<{
13179
+ id: number;
13180
+ slug: string;
13181
+ name: string | null;
13182
+ description: string | null;
13183
+ icon: string | null;
13184
+ color: string | null;
13185
+ isActive: boolean;
13186
+ sortOrder: number;
13187
+ createdAt: string;
13188
+ }>(
13189
+ `SELECT pcc.id,
13190
+ pcc.slug,
13191
+ COALESCE(pccl.name, pcc.slug) AS name,
13192
+ pccl.description,
13193
+ pcc.icon,
13194
+ pcc.color,
13195
+ pcc.is_active AS "isActive",
13196
+ pcc.sort_order AS "sortOrder",
13197
+ pcc.created_at AS "createdAt"
13198
+ FROM operations_project_cost_category pcc
13199
+ LEFT JOIN LATERAL (
13200
+ SELECT l.name, l.description
13201
+ FROM operations_project_cost_category_locale l
13202
+ WHERE l.operations_project_cost_category_id = pcc.id
13203
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13204
+ l.id ASC
13205
+ LIMIT 1
13206
+ ) pccl ON TRUE
13207
+ WHERE pcc.id = $2 AND pcc.deleted_at IS NULL`,
13208
+ [localeId, id]
13209
+ );
13210
+
13211
+ if (!row) {
13212
+ throw new NotFoundException('Project cost category not found.');
13213
+ }
13214
+
13215
+ return row;
13216
+ }
13217
+
13218
+ // ──────────────────────────────────────────────────────────────────────────
13219
+ // Project Cost Types
13220
+ // ──────────────────────────────────────────────────────────────────────────
13221
+
13222
+ async listProjectCostTypes(userId: number, filters: { search?: string; category_id?: number; is_active?: boolean; default_calculation_type?: string; pageSize?: number; page?: number } = {}) {
13223
+ await this.getActorContext(userId);
13224
+ const localeId = await this.resolvePreferredLocaleId();
13225
+
13226
+ const params: unknown[] = [localeId];
13227
+ const where: string[] = ['pct.deleted_at IS NULL'];
13228
+
13229
+ if (filters.is_active === true) {
13230
+ where.push('pct.is_active = true');
13231
+ }
13232
+
13233
+ if (filters.category_id) {
13234
+ where.push(`pct.category_id = ${this.param(params, filters.category_id)}`);
13235
+ }
13236
+
13237
+ if (filters.default_calculation_type) {
13238
+ where.push(`pct.default_calculation_type = ${this.param(params, filters.default_calculation_type)}`);
13239
+ }
13240
+
13241
+ if (filters.search?.trim()) {
13242
+ const p = this.param(params, `%${filters.search.trim()}%`);
13243
+ where.push(`(COALESCE(pctl.name, pct.slug) ILIKE ${p} OR COALESCE(pct.code, '') ILIKE ${p} OR COALESCE(pct.slug, '') ILIKE ${p})`);
13244
+ }
13245
+
13246
+ const whereClause = `WHERE ${where.join(' AND ')}`;
13247
+
13248
+ return this.queryRows<{
13249
+ id: number;
13250
+ slug: string;
13251
+ code: string;
13252
+ name: string | null;
13253
+ description: string | null;
13254
+ categoryId: number | null;
13255
+ categorySlug: string | null;
13256
+ categoryName: string | null;
13257
+ defaultUnit: string | null;
13258
+ defaultCalculationType: string | null;
13259
+ isRecurringAllowed: boolean;
13260
+ isActive: boolean;
13261
+ sortOrder: number;
13262
+ createdAt: string;
13263
+ }>(
13264
+ `SELECT pct.id,
13265
+ pct.slug,
13266
+ pct.code,
13267
+ COALESCE(pctl.name, pct.slug) AS name,
13268
+ pctl.description,
13269
+ pct.category_id AS "categoryId",
13270
+ pcc.slug AS "categorySlug",
13271
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13272
+ pct.default_unit AS "defaultUnit",
13273
+ pct.default_calculation_type AS "defaultCalculationType",
13274
+ pct.is_recurring_allowed AS "isRecurringAllowed",
13275
+ pct.is_active AS "isActive",
13276
+ pct.sort_order AS "sortOrder",
13277
+ pct.created_at AS "createdAt"
13278
+ FROM operations_project_cost_type pct
13279
+ LEFT JOIN operations_project_cost_category pcc
13280
+ ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
13281
+ LEFT JOIN LATERAL (
13282
+ SELECT l.name, l.description
13283
+ FROM operations_project_cost_category_locale l
13284
+ WHERE l.operations_project_cost_category_id = pcc.id
13285
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13286
+ l.id ASC
13287
+ LIMIT 1
13288
+ ) pccl ON TRUE
13289
+ LEFT JOIN LATERAL (
13290
+ SELECT l.name, l.description
13291
+ FROM operations_project_cost_type_locale l
13292
+ WHERE l.operations_project_cost_type_id = pct.id
13293
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13294
+ l.id ASC
13295
+ LIMIT 1
13296
+ ) pctl ON TRUE
13297
+ ${whereClause}
13298
+ ORDER BY pct.sort_order ASC, COALESCE(pctl.name, pct.slug) ASC`,
13299
+ params
13300
+ );
13301
+ }
13302
+
13303
+ 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 }) {
13304
+ const actor = await this.getActorContext(userId);
13305
+ this.ensureDirector(actor);
13306
+
13307
+ const slug = data.slug?.trim();
13308
+ if (!slug) {
13309
+ throw new BadRequestException('Cost type slug is required.');
13310
+ }
13311
+
13312
+ if (data.category_id) {
13313
+ const category = await this.querySingle<{ id: number }>(
13314
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13315
+ [data.category_id]
13316
+ );
13317
+ if (!category) {
13318
+ throw new BadRequestException(`Category with id ${data.category_id} not found.`);
13319
+ }
13320
+ }
13321
+
13322
+ const existingSlug = await this.querySingle<{ id: number }>(
13323
+ `SELECT id FROM operations_project_cost_type WHERE slug = $1 AND deleted_at IS NULL LIMIT 1`,
13324
+ [slug]
13325
+ );
13326
+ if (existingSlug) {
13327
+ throw new ConflictException(`A cost type with slug '${slug}' already exists.`);
13328
+ }
13329
+
13330
+ const code = data.code?.trim() ?? slug;
13331
+ const existingCode = await this.querySingle<{ id: number }>(
13332
+ `SELECT id FROM operations_project_cost_type WHERE code = $1 AND deleted_at IS NULL LIMIT 1`,
13333
+ [code]
13334
+ );
13335
+ if (existingCode) {
13336
+ throw new ConflictException(`A cost type with code '${code}' already exists.`);
13337
+ }
13338
+
13339
+ return this.prisma.$transaction(async (tx) => {
13340
+ const localeId = await this.resolvePreferredLocaleId(tx as any);
13341
+
13342
+ const created = (await (tx as any).$queryRawUnsafe(
13343
+ `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)
13344
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
13345
+ RETURNING id`,
13346
+ data.category_id ?? null,
13347
+ slug,
13348
+ data.code?.trim() ?? slug,
13349
+ data.default_unit ?? null,
13350
+ data.default_calculation_type ?? 'fixed',
13351
+ data.is_recurring_allowed ?? true,
13352
+ data.is_active ?? true,
13353
+ data.sort_order ?? 0,
13354
+ )) as { id: number }[];
13355
+
13356
+ const createdId = created[0]?.id;
13357
+ if (!createdId) {
13358
+ throw new BadRequestException('Unable to create project cost type.');
13359
+ }
13360
+
13361
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : slug);
13362
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13363
+
13364
+ if (localeId && name) {
13365
+ await (tx as any).$executeRawUnsafe(
13366
+ `INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description)
13367
+ VALUES ($1, $2, $3, $4)`,
13368
+ createdId,
13369
+ localeId,
13370
+ name,
13371
+ description ?? null,
13372
+ );
13373
+ }
13374
+
13375
+ const rows = (await (tx as any).$queryRawUnsafe(
13376
+ `SELECT pct.id,
13377
+ pct.slug,
13378
+ pct.code,
13379
+ COALESCE(pctl.name, pct.slug) AS name,
13380
+ pctl.description,
13381
+ pct.category_id AS "categoryId",
13382
+ pct.default_unit AS "defaultUnit",
13383
+ pct.default_calculation_type AS "defaultCalculationType",
13384
+ pct.is_recurring_allowed AS "isRecurringAllowed",
13385
+ pct.is_active AS "isActive",
13386
+ pct.sort_order AS "sortOrder",
13387
+ pct.created_at AS "createdAt"
13388
+ FROM operations_project_cost_type pct
13389
+ LEFT JOIN operations_project_cost_type_locale pctl
13390
+ ON pctl.operations_project_cost_type_id = pct.id AND pctl.locale_id = $2
13391
+ WHERE pct.id = $1`,
13392
+ createdId,
13393
+ localeId,
13394
+ )) as any[];
13395
+
13396
+ return rows[0] ?? null;
13397
+ });
13398
+ }
13399
+
13400
+ 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 }>) {
13401
+ const actor = await this.getActorContext(userId);
13402
+ this.ensureDirector(actor);
13403
+
13404
+ const costType = await this.querySingle<{ id: number }>(
13405
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13406
+ [id]
13407
+ );
13408
+ if (!costType) {
13409
+ throw new NotFoundException('Project cost type not found.');
13410
+ }
13411
+
13412
+ if (data.category_id !== undefined && data.category_id !== null) {
13413
+ const category = await this.querySingle<{ id: number }>(
13414
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13415
+ [data.category_id]
13416
+ );
13417
+ if (!category) {
13418
+ throw new BadRequestException(`Category with id ${data.category_id} not found.`);
13419
+ }
13420
+ }
13421
+
13422
+ if (data.slug !== undefined) {
13423
+ const existingSlug = await this.querySingle<{ id: number }>(
13424
+ `SELECT id FROM operations_project_cost_type WHERE slug = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
13425
+ [data.slug, id]
13426
+ );
13427
+ if (existingSlug) {
13428
+ throw new ConflictException(`A cost type with slug '${data.slug}' already exists.`);
13429
+ }
13430
+ }
13431
+
13432
+ if (data.code !== undefined) {
13433
+ const existingCode = await this.querySingle<{ id: number }>(
13434
+ `SELECT id FROM operations_project_cost_type WHERE code = $1 AND id != $2 AND deleted_at IS NULL LIMIT 1`,
13435
+ [data.code, id]
13436
+ );
13437
+ if (existingCode) {
13438
+ throw new ConflictException(`A cost type with code '${data.code}' already exists.`);
13439
+ }
13440
+ }
13441
+
13442
+ const sets: string[] = [];
13443
+ const params: unknown[] = [];
13444
+
13445
+ if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
13446
+ if (data.slug !== undefined) sets.push(`slug = ${this.param(params, data.slug)}`);
13447
+ if (data.code !== undefined) sets.push(`code = ${this.param(params, data.code)}`);
13448
+ if (data.default_unit !== undefined) sets.push(`default_unit = ${this.param(params, data.default_unit)}`);
13449
+ if (data.default_calculation_type !== undefined) sets.push(`default_calculation_type = ${this.param(params, data.default_calculation_type)}`);
13450
+ if (data.is_recurring_allowed !== undefined) sets.push(`is_recurring_allowed = ${this.param(params, data.is_recurring_allowed)}`);
13451
+ if (data.is_active !== undefined) sets.push(`is_active = ${this.param(params, data.is_active)}`);
13452
+ if (data.sort_order !== undefined) sets.push(`sort_order = ${this.param(params, data.sort_order)}`);
13453
+
13454
+ if (sets.length > 0) {
13455
+ sets.push(`updated_at = NOW()`);
13456
+ await this.prisma.$queryRawUnsafe(
13457
+ `UPDATE operations_project_cost_type SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
13458
+ ...params
13459
+ );
13460
+ }
13461
+
13462
+ if (data.name !== undefined || data.description !== undefined) {
13463
+ const localeId = await this.resolvePreferredLocaleId();
13464
+ if (localeId) {
13465
+ const name = typeof data.name === 'string' ? data.name : (data.name ? JSON.stringify(data.name) : undefined);
13466
+ const description = typeof data.description === 'string' ? data.description : (data.description ? JSON.stringify(data.description) : null);
13467
+ const existing = await this.querySingle<{ id: number }>(
13468
+ `SELECT id FROM operations_project_cost_type_locale WHERE operations_project_cost_type_id = $1 AND locale_id = $2 LIMIT 1`,
13469
+ [id, localeId]
13470
+ );
13471
+ if (existing) {
13472
+ const localeSets: string[] = [];
13473
+ const localeParams: unknown[] = [];
13474
+ if (name !== undefined) localeSets.push(`name = ${this.param(localeParams, name)}`);
13475
+ if (description !== undefined) localeSets.push(`description = ${this.param(localeParams, description)}`);
13476
+ if (localeSets.length > 0) {
13477
+ await this.prisma.$queryRawUnsafe(
13478
+ `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)}`,
13479
+ ...localeParams
13480
+ );
13481
+ }
13482
+ } else if (name) {
13483
+ await this.prisma.$queryRawUnsafe(
13484
+ `INSERT INTO operations_project_cost_type_locale (operations_project_cost_type_id, locale_id, name, description) VALUES ($1, $2, $3, $4)`,
13485
+ id, localeId, name, description ?? null
13486
+ );
13487
+ }
13488
+ }
13489
+ }
13490
+
13491
+ return this.querySingle<{ id: number; slug: string }>(
13492
+ `SELECT id, slug FROM operations_project_cost_type WHERE id = $1`,
13493
+ [id]
13494
+ );
13495
+ }
13496
+
13497
+ async deleteProjectCostType(userId: number, id: number) {
13498
+ const actor = await this.getActorContext(userId);
13499
+ this.ensureDirector(actor);
13500
+
13501
+ const costType = await this.querySingle<{ id: number }>(
13502
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
13503
+ [id]
13504
+ );
13505
+ if (!costType) {
13506
+ throw new NotFoundException('Project cost type not found.');
13507
+ }
13508
+
13509
+ await this.prisma.$queryRawUnsafe(
13510
+ `UPDATE operations_project_cost_type SET deleted_at = NOW() WHERE id = $1`,
13511
+ id
13512
+ );
13513
+
13514
+ return { success: true };
13515
+ }
13516
+
13517
+ async getProjectCostType(userId: number, id: number) {
13518
+ await this.getActorContext(userId);
13519
+ const localeId = await this.resolvePreferredLocaleId();
13520
+
13521
+ const row = await this.querySingle<{
13522
+ id: number;
13523
+ slug: string;
13524
+ code: string;
13525
+ name: string | null;
13526
+ description: string | null;
13527
+ default_unit: string | null;
13528
+ default_calculation_type: string | null;
13529
+ is_recurring_allowed: boolean;
13530
+ is_active: boolean;
13531
+ sort_order: number;
13532
+ category_id: number | null;
13533
+ category: { id: number; slug: string; name: string | null; color: string | null; icon: string | null } | null;
13534
+ }>(
13535
+ `SELECT pct.id,
13536
+ pct.slug,
13537
+ pct.code,
13538
+ COALESCE(pctl.name, pct.slug) AS name,
13539
+ pctl.description,
13540
+ pct.default_unit,
13541
+ pct.default_calculation_type,
13542
+ pct.is_recurring_allowed,
13543
+ pct.is_active,
13544
+ pct.sort_order,
13545
+ pct.category_id,
13546
+ CASE WHEN pcc.id IS NOT NULL THEN
13547
+ jsonb_build_object(
13548
+ 'id', pcc.id,
13549
+ 'slug', pcc.slug,
13550
+ 'name', COALESCE(pccl.name, pcc.slug),
13551
+ 'color', pcc.color,
13552
+ 'icon', pcc.icon
13553
+ )
13554
+ ELSE NULL END AS category
13555
+ FROM operations_project_cost_type pct
13556
+ LEFT JOIN operations_project_cost_category pcc
13557
+ ON pcc.id = pct.category_id AND pcc.deleted_at IS NULL
13558
+ LEFT JOIN LATERAL (
13559
+ SELECT l.name, l.description
13560
+ FROM operations_project_cost_category_locale l
13561
+ WHERE l.operations_project_cost_category_id = pcc.id
13562
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13563
+ l.id ASC
13564
+ LIMIT 1
13565
+ ) pccl ON TRUE
13566
+ LEFT JOIN LATERAL (
13567
+ SELECT l.name, l.description
13568
+ FROM operations_project_cost_type_locale l
13569
+ WHERE l.operations_project_cost_type_id = pct.id
13570
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13571
+ l.id ASC
13572
+ LIMIT 1
13573
+ ) pctl ON TRUE
13574
+ WHERE pct.id = $2 AND pct.deleted_at IS NULL`,
13575
+ [localeId, id]
13576
+ );
13577
+
13578
+ if (!row) {
13579
+ throw new NotFoundException('Project cost type not found.');
13580
+ }
13581
+
13582
+ return row;
13583
+ }
13584
+
13585
+ // ──────────────────────────────────────────────────────────────────────────
13586
+ // Project Costs
13587
+ // ──────────────────────────────────────────────────────────────────────────
13588
+
13589
+ async listProjectCosts(userId: number, projectId: number, filters: {
13590
+ search?: string;
13591
+ cost_type_id?: number;
13592
+ category_id?: number;
13593
+ recurrence_type?: string;
13594
+ calculation_type?: string;
13595
+ status?: string;
13596
+ is_billable?: boolean;
13597
+ is_reimbursable?: boolean;
13598
+ date_from?: string;
13599
+ date_to?: string;
13600
+ } = {}) {
13601
+ await this.getActorContext(userId);
13602
+ const localeId = await this.resolvePreferredLocaleId();
13603
+
13604
+ const params: unknown[] = [localeId, projectId];
13605
+ const where: string[] = ['pc.deleted_at IS NULL', 'pc.project_id = $2'];
13606
+
13607
+ if (filters.cost_type_id) {
13608
+ where.push(`pc.cost_type_id = ${this.param(params, filters.cost_type_id)}`);
13609
+ }
13610
+
13611
+ if (filters.category_id) {
13612
+ where.push(`COALESCE(pc.category_id, pct.category_id) = ${this.param(params, filters.category_id)}`);
13613
+ }
13614
+
13615
+ if (filters.recurrence_type) {
13616
+ where.push(`pc.recurrence_type = ${this.param(params, filters.recurrence_type)}`);
13617
+ }
13618
+
13619
+ if (filters.calculation_type) {
13620
+ where.push(`pc.calculation_type = ${this.param(params, filters.calculation_type)}`);
13621
+ }
13622
+
13623
+ if (filters.status) {
13624
+ where.push(`pc.status = ${this.param(params, filters.status)}`);
13625
+ }
13626
+
13627
+ if (filters.is_billable !== undefined) {
13628
+ where.push(`pc.is_billable = ${this.param(params, filters.is_billable)}`);
13629
+ }
13630
+
13631
+ if (filters.is_reimbursable !== undefined) {
13632
+ where.push(`pc.is_reimbursable = ${this.param(params, filters.is_reimbursable)}`);
13633
+ }
13634
+
13635
+ if (filters.date_from) {
13636
+ where.push(`pc.cost_date >= ${this.param(params, filters.date_from)}::date`);
13637
+ }
13638
+
13639
+ if (filters.date_to) {
13640
+ where.push(`pc.cost_date <= ${this.param(params, filters.date_to)}::date`);
13641
+ }
13642
+
13643
+ if (filters.search?.trim()) {
13644
+ const p = this.param(params, `%${filters.search.trim()}%`);
13645
+ where.push(`(COALESCE(pc.description, '') ILIKE ${p} OR COALESCE(pc.notes, '') ILIKE ${p})`);
13646
+ }
13647
+
13648
+ const whereClause = `WHERE ${where.join(' AND ')}`;
13649
+
13650
+ const rows = await this.queryRows<{
13651
+ id: number;
13652
+ projectId: number;
13653
+ costTypeId: number | null;
13654
+ costTypeSlug: string | null;
13655
+ costTypeCode: string | null;
13656
+ costTypeName: string | null;
13657
+ categoryId: number | null;
13658
+ resolvedCategoryId: number | null;
13659
+ categorySlug: string | null;
13660
+ categoryName: string | null;
13661
+ categoryColor: string | null;
13662
+ categoryIcon: string | null;
13663
+ description: string | null;
13664
+ amount: string;
13665
+ quantity: string;
13666
+ unitAmount: string | null;
13667
+ currency: string;
13668
+ costDate: string | null;
13669
+ periodStart: string | null;
13670
+ periodEnd: string | null;
13671
+ calculationType: string;
13672
+ recurrenceType: string;
13673
+ isBillable: boolean;
13674
+ isReimbursable: boolean;
13675
+ notes: string | null;
13676
+ status: string;
13677
+ createdAt: string;
13678
+ }>(
13679
+ `SELECT pc.id,
13680
+ pc.project_id AS "projectId",
13681
+ pc.cost_type_id AS "costTypeId",
13682
+ pct.slug AS "costTypeSlug",
13683
+ pct.code AS "costTypeCode",
13684
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
13685
+ pc.category_id AS "categoryId",
13686
+ COALESCE(pc.category_id, pct.category_id) AS "resolvedCategoryId",
13687
+ pcc.slug AS "categorySlug",
13688
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13689
+ pcc.color AS "categoryColor",
13690
+ pcc.icon AS "categoryIcon",
13691
+ pc.description,
13692
+ pc.amount::text AS amount,
13693
+ pc.quantity::text AS quantity,
13694
+ pc.unit_amount::text AS "unitAmount",
13695
+ pc.currency,
13696
+ TO_CHAR(pc.cost_date, 'YYYY-MM-DD') AS "costDate",
13697
+ TO_CHAR(pc.period_start, 'YYYY-MM-DD') AS "periodStart",
13698
+ TO_CHAR(pc.period_end, 'YYYY-MM-DD') AS "periodEnd",
13699
+ pc.calculation_type AS "calculationType",
13700
+ pc.recurrence_type AS "recurrenceType",
13701
+ pc.is_billable AS "isBillable",
13702
+ pc.is_reimbursable AS "isReimbursable",
13703
+ pc.notes,
13704
+ pc.status,
13705
+ pc.created_at AS "createdAt"
13706
+ FROM operations_project_cost pc
13707
+ LEFT JOIN operations_project_cost_type pct
13708
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13709
+ LEFT JOIN operations_project_cost_category pcc
13710
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
13711
+ LEFT JOIN LATERAL (
13712
+ SELECT l.name
13713
+ FROM operations_project_cost_type_locale l
13714
+ WHERE l.operations_project_cost_type_id = pct.id
13715
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13716
+ l.id ASC
13717
+ LIMIT 1
13718
+ ) pctl ON TRUE
13719
+ LEFT JOIN LATERAL (
13720
+ SELECT l.name
13721
+ FROM operations_project_cost_category_locale l
13722
+ WHERE l.operations_project_cost_category_id = pcc.id
13723
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13724
+ l.id ASC
13725
+ LIMIT 1
13726
+ ) pccl ON TRUE
13727
+ ${whereClause}
13728
+ ORDER BY pc.created_at DESC`,
13729
+ params
13730
+ );
13731
+
13732
+ return rows.map((row) => ({
13733
+ id: row.id,
13734
+ project_id: row.projectId,
13735
+ cost_type_id: row.costTypeId,
13736
+ category_id: row.categoryId,
13737
+ description: row.description,
13738
+ amount: row.amount,
13739
+ quantity: row.quantity,
13740
+ unit_amount: row.unitAmount,
13741
+ currency: row.currency,
13742
+ cost_date: row.costDate,
13743
+ period_start: row.periodStart,
13744
+ period_end: row.periodEnd,
13745
+ calculation_type: row.calculationType,
13746
+ recurrence_type: row.recurrenceType,
13747
+ is_billable: row.isBillable,
13748
+ is_reimbursable: row.isReimbursable,
13749
+ notes: row.notes,
13750
+ status: row.status,
13751
+ created_at: row.createdAt,
13752
+ cost_type: row.costTypeId
13753
+ ? { id: row.costTypeId, slug: row.costTypeSlug, name: row.costTypeName, code: row.costTypeCode }
13754
+ : null,
13755
+ category: row.resolvedCategoryId
13756
+ ? { id: row.resolvedCategoryId, slug: row.categorySlug, name: row.categoryName, color: row.categoryColor, icon: row.categoryIcon }
13757
+ : null,
13758
+ }));
13759
+ }
13760
+
13761
+ async getProjectCostsSummaryGrouped(userId: number, projectId: number) {
13762
+ const items = await this.listProjectCosts(userId, projectId, {});
13763
+
13764
+ // Group by resolved category
13765
+ const categoryMap = new Map<
13766
+ number | null,
13767
+ {
13768
+ category: { id: number; slug: string | null; name: string | null; color: string | null; icon: string | null } | null;
13769
+ items: typeof items;
13770
+ total_amount: number;
13771
+ }
13772
+ >();
13773
+
13774
+ for (const cost of items) {
13775
+ const cat = cost.category ?? null;
13776
+ const key = cat?.id ?? null;
13777
+ if (!categoryMap.has(key)) {
13778
+ categoryMap.set(key, { category: cat, items: [], total_amount: 0 });
13779
+ }
13780
+ const group = categoryMap.get(key)!;
13781
+ group.items.push(cost);
13782
+ group.total_amount += (parseFloat(String(cost.amount)) || 0) * (parseFloat(String(cost.quantity)) || 1);
13783
+ }
13784
+
13785
+ const grand_total = Array.from(categoryMap.values()).reduce(
13786
+ (sum, g) => sum + g.total_amount,
13787
+ 0,
13788
+ );
13789
+
13790
+ return {
13791
+ categories: Array.from(categoryMap.values()).map((g) => ({
13792
+ category: g.category,
13793
+ items: g.items,
13794
+ total_amount: Math.round(g.total_amount * 100) / 100,
13795
+ count: g.items.length,
13796
+ })),
13797
+ grand_total: Math.round(grand_total * 100) / 100,
13798
+ };
13799
+ }
13800
+
13801
+ async getProjectCost(userId: number, projectId: number, id: number) {
13802
+ const rows = await this.listProjectCosts(userId, projectId, {});
13803
+ const cost = rows.find((r) => r.id === id);
13804
+ if (!cost) {
13805
+ throw new NotFoundException('Project cost not found.');
13806
+ }
13807
+ return cost;
13808
+ }
13809
+
13810
+ async getProjectCostsSummary(userId: number, projectId: number) {
13811
+ await this.getActorContext(userId);
13812
+ const localeId = await this.resolvePreferredLocaleId();
13813
+
13814
+ // ── 1. Verify project exists and fetch budget_amount ──────────────────
13815
+ const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
13816
+ `SELECT id, budget_amount::text AS "budgetAmount"
13817
+ FROM operations_project
13818
+ WHERE id = $1 AND deleted_at IS NULL
13819
+ LIMIT 1`,
13820
+ [projectId]
13821
+ );
13822
+ if (!project) {
13823
+ throw new NotFoundException('Project not found.');
13824
+ }
13825
+
13826
+ const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
13827
+
13828
+ // ── 2. Aggregated cost totals ─────────────────────────────────────────
13829
+ const totals = await this.querySingle<{
13830
+ extraCostTotal: string;
13831
+ plannedTotal: string;
13832
+ approvedTotal: string;
13833
+ realizedTotal: string;
13834
+ cancelledTotal: string;
13835
+ billableTotal: string;
13836
+ nonBillableTotal: string;
13837
+ reimbursableTotal: string;
13838
+ }>(
13839
+ `SELECT
13840
+ COALESCE(SUM(CASE WHEN status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "extraCostTotal",
13841
+ COALESCE(SUM(CASE WHEN status = 'planned' THEN amount * quantity ELSE 0 END), 0)::text AS "plannedTotal",
13842
+ COALESCE(SUM(CASE WHEN status = 'approved' THEN amount * quantity ELSE 0 END), 0)::text AS "approvedTotal",
13843
+ COALESCE(SUM(CASE WHEN status = 'realized' THEN amount * quantity ELSE 0 END), 0)::text AS "realizedTotal",
13844
+ COALESCE(SUM(CASE WHEN status = 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "cancelledTotal",
13845
+ COALESCE(SUM(CASE WHEN is_billable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "billableTotal",
13846
+ COALESCE(SUM(CASE WHEN is_billable = false AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
13847
+ COALESCE(SUM(CASE WHEN is_reimbursable = true AND status != 'cancelled' THEN amount * quantity ELSE 0 END), 0)::text AS "reimbursableTotal"
13848
+ FROM operations_project_cost
13849
+ WHERE deleted_at IS NULL
13850
+ AND project_id = $1`,
13851
+ [projectId]
13852
+ );
13853
+
13854
+ const extraCostTotal = Math.round((parseFloat(totals?.extraCostTotal ?? '0') || 0) * 100) / 100;
13855
+ const plannedTotal = Math.round((parseFloat(totals?.plannedTotal ?? '0') || 0) * 100) / 100;
13856
+ const approvedTotal = Math.round((parseFloat(totals?.approvedTotal ?? '0') || 0) * 100) / 100;
13857
+ const realizedTotal = Math.round((parseFloat(totals?.realizedTotal ?? '0') || 0) * 100) / 100;
13858
+ const cancelledTotal = Math.round((parseFloat(totals?.cancelledTotal ?? '0') || 0) * 100) / 100;
13859
+ const billableTotal = Math.round((parseFloat(totals?.billableTotal ?? '0') || 0) * 100) / 100;
13860
+ const nonBillableTotal = Math.round((parseFloat(totals?.nonBillableTotal ?? '0') || 0) * 100) / 100;
13861
+ const reimbursableTotal = Math.round((parseFloat(totals?.reimbursableTotal ?? '0') || 0) * 100) / 100;
13862
+
13863
+ const teamCostTotal = 0;
13864
+ const totalProjectCost = Math.round((teamCostTotal + extraCostTotal) * 100) / 100;
13865
+ const remainingBudget = Math.round((budgetAmount - totalProjectCost) * 100) / 100;
13866
+ const budgetUsagePercent = budgetAmount > 0
13867
+ ? Math.round((totalProjectCost / budgetAmount) * 10000) / 100
13868
+ : 0;
13869
+
13870
+ // ── 3. cost_by_category ───────────────────────────────────────────────
13871
+ const costByCategory = await this.queryRows<{
13872
+ categoryId: number | null;
13873
+ categorySlug: string | null;
13874
+ categoryName: string | null;
13875
+ categoryColor: string | null;
13876
+ categoryIcon: string | null;
13877
+ total: string;
13878
+ count: number;
13879
+ }>(
13880
+ `SELECT
13881
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
13882
+ pcc.slug AS "categorySlug",
13883
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
13884
+ pcc.color AS "categoryColor",
13885
+ pcc.icon AS "categoryIcon",
13886
+ SUM(pc.amount * pc.quantity)::text AS total,
13887
+ COUNT(*)::int AS count
13888
+ FROM operations_project_cost pc
13889
+ LEFT JOIN operations_project_cost_type pct
13890
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13891
+ LEFT JOIN operations_project_cost_category pcc
13892
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
13893
+ LEFT JOIN LATERAL (
13894
+ SELECT l.name
13895
+ FROM operations_project_cost_category_locale l
13896
+ WHERE l.operations_project_cost_category_id = pcc.id
13897
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13898
+ l.id ASC
13899
+ LIMIT 1
13900
+ ) pccl ON TRUE
13901
+ WHERE pc.deleted_at IS NULL
13902
+ AND pc.project_id = $2
13903
+ AND pc.status != 'cancelled'
13904
+ GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
13905
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
13906
+ [localeId, projectId]
13907
+ );
13908
+
13909
+ // ── 4. cost_by_type ───────────────────────────────────────────────────
13910
+ const costByType = await this.queryRows<{
13911
+ costTypeId: number | null;
13912
+ costTypeSlug: string | null;
13913
+ costTypeName: string | null;
13914
+ costTypeCode: string | null;
13915
+ total: string;
13916
+ count: number;
13917
+ }>(
13918
+ `SELECT
13919
+ pc.cost_type_id AS "costTypeId",
13920
+ pct.slug AS "costTypeSlug",
13921
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
13922
+ pct.code AS "costTypeCode",
13923
+ SUM(pc.amount * pc.quantity)::text AS total,
13924
+ COUNT(*)::int AS count
13925
+ FROM operations_project_cost pc
13926
+ LEFT JOIN operations_project_cost_type pct
13927
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
13928
+ LEFT JOIN LATERAL (
13929
+ SELECT l.name
13930
+ FROM operations_project_cost_type_locale l
13931
+ WHERE l.operations_project_cost_type_id = pct.id
13932
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC,
13933
+ l.id ASC
13934
+ LIMIT 1
13935
+ ) pctl ON TRUE
13936
+ WHERE pc.deleted_at IS NULL
13937
+ AND pc.project_id = $2
13938
+ AND pc.status != 'cancelled'
13939
+ GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
13940
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
13941
+ [localeId, projectId]
13942
+ );
13943
+
13944
+ // ── 5. cost_by_month ──────────────────────────────────────────────────
13945
+ const costByMonth = await this.queryRows<{
13946
+ month: string;
13947
+ total: string;
13948
+ count: number;
13949
+ }>(
13950
+ `SELECT
13951
+ TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM') AS month,
13952
+ SUM(pc.amount * pc.quantity)::text AS total,
13953
+ COUNT(*)::int AS count
13954
+ FROM operations_project_cost pc
13955
+ WHERE pc.deleted_at IS NULL
13956
+ AND pc.project_id = $1
13957
+ AND pc.status != 'cancelled'
13958
+ GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at), 'YYYY-MM')
13959
+ ORDER BY month ASC`,
13960
+ [projectId]
13961
+ );
13962
+
13963
+ // ── 6. top_cost_types (top 5) ─────────────────────────────────────────
13964
+ const topCostTypes = costByType.slice(0, 5).map((ct) => {
13965
+ const typeTotal = Math.round((parseFloat(ct.total) || 0) * 100) / 100;
13966
+ const percentage = extraCostTotal > 0
13967
+ ? Math.round((typeTotal / extraCostTotal) * 10000) / 100
13968
+ : 0;
13969
+ return {
13970
+ cost_type_id: ct.costTypeId,
13971
+ cost_type_slug: ct.costTypeSlug,
13972
+ cost_type_name: ct.costTypeName,
13973
+ cost_type_code: ct.costTypeCode,
13974
+ total: typeTotal,
13975
+ percentage,
13976
+ };
13977
+ });
13978
+
13979
+ return {
13980
+ project_id: projectId,
13981
+ budget_amount: budgetAmount,
13982
+ team_cost_total: teamCostTotal,
13983
+ extra_cost_total: extraCostTotal,
13984
+ total_project_cost: totalProjectCost,
13985
+ remaining_budget: remainingBudget,
13986
+ budget_usage_percent: budgetUsagePercent,
13987
+ planned_total: plannedTotal,
13988
+ approved_total: approvedTotal,
13989
+ realized_total: realizedTotal,
13990
+ cancelled_total: cancelledTotal,
13991
+ billable_total: billableTotal,
13992
+ non_billable_total: nonBillableTotal,
13993
+ reimbursable_total: reimbursableTotal,
13994
+ cost_by_category: costByCategory.map((c) => ({
13995
+ category_id: c.categoryId,
13996
+ category_slug: c.categorySlug,
13997
+ category_name: c.categoryName,
13998
+ category_color: c.categoryColor,
13999
+ category_icon: c.categoryIcon,
14000
+ total: Math.round((parseFloat(c.total) || 0) * 100) / 100,
14001
+ count: Number(c.count),
14002
+ })),
14003
+ cost_by_type: costByType.map((t) => ({
14004
+ cost_type_id: t.costTypeId,
14005
+ cost_type_slug: t.costTypeSlug,
14006
+ cost_type_name: t.costTypeName,
14007
+ cost_type_code: t.costTypeCode,
14008
+ total: Math.round((parseFloat(t.total) || 0) * 100) / 100,
14009
+ count: Number(t.count),
14010
+ })),
14011
+ cost_by_month: costByMonth.map((m) => ({
14012
+ month: m.month,
14013
+ total: Math.round((parseFloat(m.total) || 0) * 100) / 100,
14014
+ count: Number(m.count),
14015
+ })),
14016
+ top_cost_types: topCostTypes,
14017
+ };
14018
+ }
14019
+
14020
+ async getProjectCostReport(
14021
+ userId: number,
14022
+ projectId: number,
14023
+ filters: {
14024
+ date_from?: string;
14025
+ date_to?: string;
14026
+ category_id?: number;
14027
+ cost_type_id?: number;
14028
+ status?: string;
14029
+ is_billable?: boolean;
14030
+ is_reimbursable?: boolean;
14031
+ },
14032
+ ) {
14033
+ await this.getActorContext(userId);
14034
+ const localeId = await this.resolvePreferredLocaleId();
14035
+
14036
+ // ── Verify project ───────────────────────────────────────────────────
14037
+ const project = await this.querySingle<{ id: number; budgetAmount: string | null }>(
14038
+ `SELECT id, budget_amount::text AS "budgetAmount"
14039
+ FROM operations_project
14040
+ WHERE id = $1 AND deleted_at IS NULL
14041
+ LIMIT 1`,
14042
+ [projectId],
14043
+ );
14044
+ if (!project) {
14045
+ throw new NotFoundException('Project not found.');
14046
+ }
14047
+ const budgetAmount = parseFloat(project.budgetAmount ?? '0') || 0;
14048
+
14049
+ // ── Build dynamic WHERE clause ────────────────────────────────────────
14050
+ const conditions: string[] = [
14051
+ 'pc.deleted_at IS NULL',
14052
+ 'pc.project_id = $1',
14053
+ ];
14054
+ const params: unknown[] = [projectId];
14055
+
14056
+ if (filters.date_from) {
14057
+ params.push(filters.date_from);
14058
+ conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) >= $${params.length}::date`);
14059
+ }
14060
+ if (filters.date_to) {
14061
+ params.push(filters.date_to);
14062
+ conditions.push(`COALESCE(pc.cost_date, pc.created_at::date) <= $${params.length}::date`);
14063
+ }
14064
+ if (filters.category_id !== undefined) {
14065
+ params.push(filters.category_id);
14066
+ conditions.push(
14067
+ `(pc.category_id = $${params.length} OR (pc.category_id IS NULL AND EXISTS (
14068
+ SELECT 1 FROM operations_project_cost_type pct2
14069
+ WHERE pct2.id = pc.cost_type_id AND pct2.category_id = $${params.length} AND pct2.deleted_at IS NULL
14070
+ )))`,
14071
+ );
14072
+ }
14073
+ if (filters.cost_type_id !== undefined) {
14074
+ params.push(filters.cost_type_id);
14075
+ conditions.push(`pc.cost_type_id = $${params.length}`);
14076
+ }
14077
+ if (filters.status !== undefined) {
14078
+ params.push(filters.status);
14079
+ conditions.push(`pc.status = $${params.length}`);
14080
+ }
14081
+ if (filters.is_billable !== undefined) {
14082
+ params.push(filters.is_billable);
14083
+ conditions.push(`pc.is_billable = $${params.length}`);
14084
+ }
14085
+ if (filters.is_reimbursable !== undefined) {
14086
+ params.push(filters.is_reimbursable);
14087
+ conditions.push(`pc.is_reimbursable = $${params.length}`);
14088
+ }
14089
+
14090
+ const whereClause = conditions.join(' AND ');
14091
+
14092
+ // ── Totals ────────────────────────────────────────────────────────────
14093
+ const totals = await this.querySingle<{
14094
+ grandTotal: string;
14095
+ plannedTotal: string;
14096
+ approvedTotal: string;
14097
+ realizedTotal: string;
14098
+ cancelledTotal: string;
14099
+ billableTotal: string;
14100
+ nonBillableTotal: string;
14101
+ reimbursableTotal: string;
14102
+ totalCount: number;
14103
+ }>(
14104
+ `SELECT
14105
+ COALESCE(SUM(pc.amount * pc.quantity), 0)::text AS "grandTotal",
14106
+ COALESCE(SUM(CASE WHEN pc.status = 'planned' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "plannedTotal",
14107
+ COALESCE(SUM(CASE WHEN pc.status = 'approved' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "approvedTotal",
14108
+ COALESCE(SUM(CASE WHEN pc.status = 'realized' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "realizedTotal",
14109
+ COALESCE(SUM(CASE WHEN pc.status = 'cancelled' THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "cancelledTotal",
14110
+ COALESCE(SUM(CASE WHEN pc.is_billable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "billableTotal",
14111
+ COALESCE(SUM(CASE WHEN pc.is_billable = false THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "nonBillableTotal",
14112
+ COALESCE(SUM(CASE WHEN pc.is_reimbursable = true THEN pc.amount * pc.quantity ELSE 0 END), 0)::text AS "reimbursableTotal",
14113
+ COUNT(*)::int AS "totalCount"
14114
+ FROM operations_project_cost pc
14115
+ WHERE ${whereClause}`,
14116
+ params,
14117
+ );
14118
+
14119
+ const round2 = (v: string | null | undefined) =>
14120
+ Math.round((parseFloat(v ?? '0') || 0) * 100) / 100;
14121
+
14122
+ const grandTotal = round2(totals?.grandTotal);
14123
+ const plannedTotal = round2(totals?.plannedTotal);
14124
+ const approvedTotal = round2(totals?.approvedTotal);
14125
+ const realizedTotal = round2(totals?.realizedTotal);
14126
+ const cancelledTotal = round2(totals?.cancelledTotal);
14127
+ const billableTotal = round2(totals?.billableTotal);
14128
+ const nonBillableTotal = round2(totals?.nonBillableTotal);
14129
+ const reimbursableTotal= round2(totals?.reimbursableTotal);
14130
+
14131
+ // ── By category ───────────────────────────────────────────────────────
14132
+ const costByCategory = await this.queryRows<{
14133
+ categoryId: number | null;
14134
+ categorySlug: string | null;
14135
+ categoryName: string | null;
14136
+ categoryColor: string | null;
14137
+ categoryIcon: string | null;
14138
+ total: string;
14139
+ count: number;
14140
+ plannedSubtotal: string;
14141
+ realizedSubtotal: string;
14142
+ }>(
14143
+ `SELECT
14144
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
14145
+ pcc.slug AS "categorySlug",
14146
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
14147
+ pcc.color AS "categoryColor",
14148
+ pcc.icon AS "categoryIcon",
14149
+ SUM(pc.amount * pc.quantity)::text AS total,
14150
+ COUNT(*)::int AS count,
14151
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
14152
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
14153
+ FROM operations_project_cost pc
14154
+ LEFT JOIN operations_project_cost_type pct
14155
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14156
+ LEFT JOIN operations_project_cost_category pcc
14157
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
14158
+ LEFT JOIN LATERAL (
14159
+ SELECT l.name
14160
+ FROM operations_project_cost_category_locale l
14161
+ WHERE l.operations_project_cost_category_id = pcc.id
14162
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14163
+ LIMIT 1
14164
+ ) pccl ON TRUE
14165
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14166
+ GROUP BY COALESCE(pc.category_id, pct.category_id), pcc.slug, pcc.color, pcc.icon, pccl.name
14167
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
14168
+ [localeId, ...params],
14169
+ );
14170
+
14171
+ // ── By type ───────────────────────────────────────────────────────────
14172
+ const costByType = await this.queryRows<{
14173
+ costTypeId: number | null;
14174
+ costTypeSlug: string | null;
14175
+ costTypeName: string | null;
14176
+ costTypeCode: string | null;
14177
+ total: string;
14178
+ count: number;
14179
+ plannedSubtotal: string;
14180
+ realizedSubtotal: string;
14181
+ }>(
14182
+ `SELECT
14183
+ pc.cost_type_id AS "costTypeId",
14184
+ pct.slug AS "costTypeSlug",
14185
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14186
+ pct.code AS "costTypeCode",
14187
+ SUM(pc.amount * pc.quantity)::text AS total,
14188
+ COUNT(*)::int AS count,
14189
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
14190
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal"
14191
+ FROM operations_project_cost pc
14192
+ LEFT JOIN operations_project_cost_type pct
14193
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14194
+ LEFT JOIN LATERAL (
14195
+ SELECT l.name
14196
+ FROM operations_project_cost_type_locale l
14197
+ WHERE l.operations_project_cost_type_id = pct.id
14198
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14199
+ LIMIT 1
14200
+ ) pctl ON TRUE
14201
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14202
+ GROUP BY pc.cost_type_id, pct.slug, pct.code, pctl.name
14203
+ ORDER BY SUM(pc.amount * pc.quantity) DESC`,
14204
+ [localeId, ...params],
14205
+ );
14206
+
14207
+ // ── By month ──────────────────────────────────────────────────────────
14208
+ const costByMonth = await this.queryRows<{
14209
+ month: string;
14210
+ total: string;
14211
+ plannedSubtotal: string;
14212
+ realizedSubtotal: string;
14213
+ count: number;
14214
+ }>(
14215
+ `SELECT
14216
+ TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM') AS month,
14217
+ SUM(pc.amount * pc.quantity)::text AS total,
14218
+ COALESCE(SUM(CASE WHEN pc.status='planned' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "plannedSubtotal",
14219
+ COALESCE(SUM(CASE WHEN pc.status='realized' THEN pc.amount*pc.quantity ELSE 0 END),0)::text AS "realizedSubtotal",
14220
+ COUNT(*)::int AS count
14221
+ FROM operations_project_cost pc
14222
+ WHERE ${whereClause}
14223
+ GROUP BY TO_CHAR(COALESCE(pc.cost_date, pc.created_at::date), 'YYYY-MM')
14224
+ ORDER BY month ASC`,
14225
+ params,
14226
+ );
14227
+
14228
+ // ── Top 5 individual costs ────────────────────────────────────────────
14229
+ const top5Costs = await this.queryRows<{
14230
+ id: number;
14231
+ description: string | null;
14232
+ amount: string;
14233
+ quantity: string;
14234
+ status: string;
14235
+ costTypeName: string | null;
14236
+ categoryName: string | null;
14237
+ categoryColor: string | null;
14238
+ costDate: string | null;
14239
+ }>(
14240
+ `SELECT
14241
+ pc.id,
14242
+ pc.description,
14243
+ pc.amount::text,
14244
+ pc.quantity::text,
14245
+ pc.status,
14246
+ pc.cost_date AS "costDate",
14247
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14248
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
14249
+ pcc.color AS "categoryColor"
14250
+ FROM operations_project_cost pc
14251
+ LEFT JOIN operations_project_cost_type pct
14252
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14253
+ LEFT JOIN LATERAL (
14254
+ SELECT l.name
14255
+ FROM operations_project_cost_type_locale l
14256
+ WHERE l.operations_project_cost_type_id = pct.id
14257
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14258
+ LIMIT 1
14259
+ ) pctl ON TRUE
14260
+ LEFT JOIN operations_project_cost_category pcc
14261
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
14262
+ LEFT JOIN LATERAL (
14263
+ SELECT l.name
14264
+ FROM operations_project_cost_category_locale l
14265
+ WHERE l.operations_project_cost_category_id = pcc.id
14266
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14267
+ LIMIT 1
14268
+ ) pccl ON TRUE
14269
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14270
+ ORDER BY (pc.amount * pc.quantity) DESC
14271
+ LIMIT 5`,
14272
+ [localeId, ...params],
14273
+ );
14274
+
14275
+ // ── Detailed list ─────────────────────────────────────────────────────
14276
+ const detailedList = await this.queryRows<{
14277
+ id: number;
14278
+ description: string | null;
14279
+ amount: string;
14280
+ quantity: string;
14281
+ unitAmount: string | null;
14282
+ currency: string | null;
14283
+ calculationType: string | null;
14284
+ recurrenceType: string | null;
14285
+ status: string;
14286
+ isBillable: boolean;
14287
+ isReimbursable: boolean;
14288
+ costDate: string | null;
14289
+ periodStart: string | null;
14290
+ periodEnd: string | null;
14291
+ notes: string | null;
14292
+ costTypeId: number | null;
14293
+ costTypeName: string | null;
14294
+ costTypeCode: string | null;
14295
+ categoryId: number | null;
14296
+ categoryName: string | null;
14297
+ categoryColor: string | null;
14298
+ createdAt: string;
14299
+ }>(
14300
+ `SELECT
14301
+ pc.id,
14302
+ pc.description,
14303
+ pc.amount::text,
14304
+ pc.quantity::text,
14305
+ pc.unit_amount::text AS "unitAmount",
14306
+ pc.currency,
14307
+ pc.calculation_type AS "calculationType",
14308
+ pc.recurrence_type AS "recurrenceType",
14309
+ pc.status,
14310
+ pc.is_billable AS "isBillable",
14311
+ pc.is_reimbursable AS "isReimbursable",
14312
+ pc.cost_date AS "costDate",
14313
+ pc.period_start AS "periodStart",
14314
+ pc.period_end AS "periodEnd",
14315
+ pc.notes,
14316
+ pc.cost_type_id AS "costTypeId",
14317
+ COALESCE(pctl.name, pct.slug) AS "costTypeName",
14318
+ pct.code AS "costTypeCode",
14319
+ COALESCE(pc.category_id, pct.category_id) AS "categoryId",
14320
+ COALESCE(pccl.name, pcc.slug) AS "categoryName",
14321
+ pcc.color AS "categoryColor",
14322
+ pc.created_at::text AS "createdAt"
14323
+ FROM operations_project_cost pc
14324
+ LEFT JOIN operations_project_cost_type pct
14325
+ ON pct.id = pc.cost_type_id AND pct.deleted_at IS NULL
14326
+ LEFT JOIN LATERAL (
14327
+ SELECT l.name
14328
+ FROM operations_project_cost_type_locale l
14329
+ WHERE l.operations_project_cost_type_id = pct.id
14330
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14331
+ LIMIT 1
14332
+ ) pctl ON TRUE
14333
+ LEFT JOIN operations_project_cost_category pcc
14334
+ ON pcc.id = COALESCE(pc.category_id, pct.category_id) AND pcc.deleted_at IS NULL
14335
+ LEFT JOIN LATERAL (
14336
+ SELECT l.name
14337
+ FROM operations_project_cost_category_locale l
14338
+ WHERE l.operations_project_cost_category_id = pcc.id
14339
+ ORDER BY CASE WHEN $1::int IS NOT NULL AND l.locale_id = $1 THEN 0 ELSE 1 END ASC, l.id ASC
14340
+ LIMIT 1
14341
+ ) pccl ON TRUE
14342
+ WHERE ${whereClause.replace(/\$(\d+)/g, (m, n) => '$' + (Number(n) + 1))}
14343
+ ORDER BY (pc.amount * pc.quantity) DESC, pc.cost_date DESC NULLS LAST`,
14344
+ [localeId, ...params],
14345
+ );
14346
+
14347
+ return {
14348
+ project_id: projectId,
14349
+ budget_amount: budgetAmount,
14350
+ filters_applied: {
14351
+ date_from: filters.date_from ?? null,
14352
+ date_to: filters.date_to ?? null,
14353
+ category_id: filters.category_id ?? null,
14354
+ cost_type_id: filters.cost_type_id ?? null,
14355
+ status: filters.status ?? null,
14356
+ is_billable: filters.is_billable ?? null,
14357
+ is_reimbursable:filters.is_reimbursable ?? null,
14358
+ },
14359
+ totals: {
14360
+ grand_total: grandTotal,
14361
+ planned_total: plannedTotal,
14362
+ approved_total: approvedTotal,
14363
+ realized_total: realizedTotal,
14364
+ cancelled_total: cancelledTotal,
14365
+ billable_total: billableTotal,
14366
+ non_billable_total: nonBillableTotal,
14367
+ reimbursable_total: reimbursableTotal,
14368
+ total_count: Number(totals?.totalCount ?? 0),
14369
+ },
14370
+ cost_by_category: costByCategory.map((c) => ({
14371
+ category_id: c.categoryId,
14372
+ category_slug: c.categorySlug,
14373
+ category_name: c.categoryName,
14374
+ category_color: c.categoryColor,
14375
+ category_icon: c.categoryIcon,
14376
+ total: round2(c.total),
14377
+ count: Number(c.count),
14378
+ planned_subtotal: round2(c.plannedSubtotal),
14379
+ realized_subtotal: round2(c.realizedSubtotal),
14380
+ })),
14381
+ cost_by_type: costByType.map((t) => ({
14382
+ cost_type_id: t.costTypeId,
14383
+ cost_type_slug: t.costTypeSlug,
14384
+ cost_type_name: t.costTypeName,
14385
+ cost_type_code: t.costTypeCode,
14386
+ total: round2(t.total),
14387
+ count: Number(t.count),
14388
+ planned_subtotal: round2(t.plannedSubtotal),
14389
+ realized_subtotal: round2(t.realizedSubtotal),
14390
+ })),
14391
+ cost_by_month: costByMonth.map((m) => ({
14392
+ month: m.month,
14393
+ total: round2(m.total),
14394
+ planned_subtotal: round2(m.plannedSubtotal),
14395
+ realized_subtotal: round2(m.realizedSubtotal),
14396
+ count: Number(m.count),
14397
+ })),
14398
+ top_5_costs: top5Costs.map((c) => ({
14399
+ id: c.id,
14400
+ description: c.description,
14401
+ total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
14402
+ amount: round2(c.amount),
14403
+ quantity: parseFloat(c.quantity),
14404
+ status: c.status,
14405
+ cost_type_name: c.costTypeName,
14406
+ category_name: c.categoryName,
14407
+ category_color: c.categoryColor,
14408
+ cost_date: c.costDate,
14409
+ })),
14410
+ detailed_list: detailedList.map((c) => ({
14411
+ id: c.id,
14412
+ description: c.description,
14413
+ amount: round2(c.amount),
14414
+ quantity: parseFloat(c.quantity),
14415
+ unit_amount: c.unitAmount ? round2(c.unitAmount) : null,
14416
+ total: round2(String(parseFloat(c.amount) * parseFloat(c.quantity))),
14417
+ currency: c.currency,
14418
+ calculation_type: c.calculationType,
14419
+ recurrence_type: c.recurrenceType,
14420
+ status: c.status,
14421
+ is_billable: c.isBillable,
14422
+ is_reimbursable: c.isReimbursable,
14423
+ cost_date: c.costDate,
14424
+ period_start: c.periodStart,
14425
+ period_end: c.periodEnd,
14426
+ notes: c.notes,
14427
+ cost_type_id: c.costTypeId,
14428
+ cost_type_name: c.costTypeName,
14429
+ cost_type_code: c.costTypeCode,
14430
+ category_id: c.categoryId,
14431
+ category_name: c.categoryName,
14432
+ category_color: c.categoryColor,
14433
+ created_at: c.createdAt,
14434
+ })),
14435
+ };
14436
+ }
14437
+
14438
+ 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 }) {
14439
+ const actor = await this.getActorContext(userId);
14440
+ this.ensureSupervisor(actor);
14441
+
14442
+ const project = await this.querySingle<{ id: number }>(
14443
+ `SELECT id FROM operations_project WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14444
+ [projectId]
14445
+ );
14446
+ if (!project) {
14447
+ throw new NotFoundException('Project not found.');
14448
+ }
14449
+
14450
+ if (data.cost_type_id) {
14451
+ const costType = await this.querySingle<{ id: number }>(
14452
+ `SELECT id FROM operations_project_cost_type WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14453
+ [data.cost_type_id]
14454
+ );
14455
+ if (!costType) {
14456
+ throw new BadRequestException(`Cost type with id ${data.cost_type_id} not found.`);
14457
+ }
14458
+ }
14459
+
14460
+ if (data.category_id) {
14461
+ const category = await this.querySingle<{ id: number }>(
14462
+ `SELECT id FROM operations_project_cost_category WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14463
+ [data.category_id]
14464
+ );
14465
+ if (!category) {
14466
+ throw new BadRequestException(`Cost category with id ${data.category_id} not found.`);
14467
+ }
14468
+ }
14469
+
14470
+ const calcType = data.calculation_type ?? 'fixed';
14471
+ let effectiveAmount = data.amount;
14472
+ if (['unit', 'hourly', 'monthly'].includes(calcType) && data.unit_amount !== undefined && data.unit_amount !== null) {
14473
+ const qty = data.quantity ?? 1;
14474
+ effectiveAmount = Math.round(qty * data.unit_amount * 100) / 100;
14475
+ }
14476
+
14477
+ const created = await this.querySingle<{ id: number }>(
14478
+ `INSERT INTO operations_project_cost
14479
+ (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)
14480
+ 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())
14481
+ RETURNING id`,
14482
+ [
14483
+ projectId,
14484
+ data.cost_type_id ?? null,
14485
+ data.category_id ?? null,
14486
+ data.description ?? null,
14487
+ effectiveAmount,
14488
+ data.quantity ?? 1,
14489
+ data.unit_amount ?? null,
14490
+ data.currency ?? 'BRL',
14491
+ data.cost_date ?? null,
14492
+ data.period_start ?? null,
14493
+ data.period_end ?? null,
14494
+ calcType,
14495
+ data.recurrence_type ?? 'none',
14496
+ data.is_billable ?? false,
14497
+ data.is_reimbursable ?? false,
14498
+ data.notes ?? null,
14499
+ data.status ?? 'planned',
14500
+ ]
14501
+ );
14502
+
14503
+ if (!created?.id) {
14504
+ throw new BadRequestException('Unable to create project cost.');
14505
+ }
14506
+
14507
+ const rows = await this.listProjectCosts(userId, projectId, {});
14508
+ return rows.find((r) => r.id === created.id) ?? null;
14509
+ }
14510
+
14511
+ 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 }>) {
14512
+ const actor = await this.getActorContext(userId);
14513
+ this.ensureSupervisor(actor);
14514
+
14515
+ const cost = await this.querySingle<{ id: number; projectId: number; calculationType: string; unitAmount: string | null; quantity: string }>(
14516
+ `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`,
14517
+ [id]
14518
+ );
14519
+ if (!cost) {
14520
+ throw new NotFoundException('Project cost not found.');
14521
+ }
14522
+
14523
+ // Auto-calculate amount when applicable
14524
+ const effectiveCalcType = data.calculation_type ?? cost.calculationType;
14525
+ if (['unit', 'hourly', 'monthly'].includes(effectiveCalcType)) {
14526
+ const ua = data.unit_amount !== undefined ? data.unit_amount : (cost.unitAmount !== null ? parseFloat(cost.unitAmount) : null);
14527
+ const qty = data.quantity !== undefined ? data.quantity : parseFloat(cost.quantity);
14528
+ if (ua !== null && ua !== undefined) {
14529
+ data = { ...data, amount: Math.round(qty * ua * 100) / 100 };
14530
+ }
14531
+ }
14532
+
14533
+ const sets: string[] = [];
14534
+ const params: unknown[] = [];
14535
+
14536
+ if (data.cost_type_id !== undefined) sets.push(`cost_type_id = ${this.param(params, data.cost_type_id)}`);
14537
+ if (data.category_id !== undefined) sets.push(`category_id = ${this.param(params, data.category_id)}`);
14538
+ if (data.description !== undefined) sets.push(`description = ${this.param(params, data.description)}`);
14539
+ if (data.amount !== undefined) sets.push(`amount = ${this.param(params, data.amount)}`);
14540
+ if (data.currency !== undefined) sets.push(`currency = ${this.param(params, data.currency)}`);
14541
+ if (data.quantity !== undefined) sets.push(`quantity = ${this.param(params, data.quantity)}`);
14542
+ if (data.unit_amount !== undefined) sets.push(`unit_amount = ${this.param(params, data.unit_amount)}`);
14543
+ if (data.calculation_type !== undefined) sets.push(`calculation_type = ${this.param(params, data.calculation_type)}::operations_project_cost_calculation_type_134cdfb49c_enum`);
14544
+ if (data.recurrence_type !== undefined) sets.push(`recurrence_type = ${this.param(params, data.recurrence_type)}::operations_project_cost_recurrence_type_09baf0f043_enum`);
14545
+ if (data.is_billable !== undefined) sets.push(`is_billable = ${this.param(params, data.is_billable)}`);
14546
+ if (data.is_reimbursable !== undefined) sets.push(`is_reimbursable = ${this.param(params, data.is_reimbursable)}`);
14547
+ if (data.cost_date !== undefined) sets.push(`cost_date = ${this.param(params, data.cost_date)}::date`);
14548
+ if (data.period_start !== undefined) sets.push(`period_start = ${this.param(params, data.period_start)}::date`);
14549
+ if (data.period_end !== undefined) sets.push(`period_end = ${this.param(params, data.period_end)}::date`);
14550
+ if (data.notes !== undefined) sets.push(`notes = ${this.param(params, data.notes)}`);
14551
+ if (data.status !== undefined) sets.push(`status = ${this.param(params, data.status)}::operations_project_cost_status_153e8592ce_enum`);
14552
+
14553
+ if (sets.length === 0) {
14554
+ const rows = await this.listProjectCosts(userId, cost.projectId, {});
14555
+ return rows.find((r) => r.id === id) ?? null;
14556
+ }
14557
+
14558
+ sets.push(`updated_at = NOW()`);
14559
+ await this.prisma.$queryRawUnsafe(
14560
+ `UPDATE operations_project_cost SET ${sets.join(', ')} WHERE id = ${this.param(params, id)}`,
14561
+ ...params
14562
+ );
14563
+
14564
+ const rows = await this.listProjectCosts(userId, cost.projectId, {});
14565
+ return rows.find((r) => r.id === id) ?? null;
14566
+ }
14567
+
14568
+ async deleteProjectCost(userId: number, id: number) {
14569
+ const actor = await this.getActorContext(userId);
14570
+ this.ensureSupervisor(actor);
14571
+
14572
+ const cost = await this.querySingle<{ id: number }>(
14573
+ `SELECT id FROM operations_project_cost WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
14574
+ [id]
14575
+ );
14576
+ if (!cost) {
14577
+ throw new NotFoundException('Project cost not found.');
14578
+ }
14579
+
14580
+ await this.prisma.$queryRawUnsafe(
14581
+ `UPDATE operations_project_cost SET deleted_at = NOW() WHERE id = $1`,
14582
+ id
14583
+ );
14584
+
14585
+ return { success: true };
14586
+ }
12176
14587
  }