@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
@@ -0,0 +1,4378 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, Page } from '@/components/entity-list';
4
+ import { RichTextEditor } from '@/components/rich-text-editor';
5
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card, CardContent } from '@/components/ui/card';
8
+ import {
9
+ ChartContainer,
10
+ ChartTooltip,
11
+ ChartTooltipContent,
12
+ type ChartConfig,
13
+ } from '@/components/ui/chart';
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from '@/components/ui/dialog';
21
+ import {
22
+ DropdownMenu,
23
+ DropdownMenuContent,
24
+ DropdownMenuItem,
25
+ DropdownMenuSeparator,
26
+ DropdownMenuTrigger,
27
+ } from '@/components/ui/dropdown-menu';
28
+ import { Input } from '@/components/ui/input';
29
+ import { Label } from '@/components/ui/label';
30
+ import { Progress } from '@/components/ui/progress';
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from '@/components/ui/select';
38
+ import {
39
+ Sheet,
40
+ SheetContent,
41
+ SheetDescription,
42
+ SheetHeader,
43
+ SheetTitle,
44
+ } from '@/components/ui/sheet';
45
+ import { Skeleton } from '@/components/ui/skeleton';
46
+ import {
47
+ Table,
48
+ TableBody,
49
+ TableCell,
50
+ TableHead,
51
+ TableHeader,
52
+ TableRow,
53
+ } from '@/components/ui/table';
54
+ import {
55
+ Tooltip,
56
+ TooltipContent,
57
+ TooltipTrigger,
58
+ } from '@/components/ui/tooltip';
59
+ import {
60
+ closestCenter,
61
+ DndContext,
62
+ DragOverlay,
63
+ PointerSensor,
64
+ pointerWithin,
65
+ useDraggable,
66
+ useDroppable,
67
+ useSensor,
68
+ useSensors,
69
+ type CollisionDetection,
70
+ type DragEndEvent,
71
+ type DragStartEvent,
72
+ type UniqueIdentifier,
73
+ } from '@dnd-kit/core';
74
+ import { CSS } from '@dnd-kit/utilities';
75
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
76
+ import { AnimatePresence, motion } from 'framer-motion';
77
+ import {
78
+ AlarmClock,
79
+ AlertTriangle,
80
+ Archive,
81
+ ArchiveRestore,
82
+ BarChart2,
83
+ BarChart3,
84
+ CalendarClock,
85
+ CalendarDays,
86
+ CheckCircle2,
87
+ ChevronRight,
88
+ ClipboardList,
89
+ FileText,
90
+ FolderKanban,
91
+ Gauge,
92
+ GitCommitHorizontal,
93
+ HeartPulse,
94
+ LineChart as LineChartIcon,
95
+ Loader2,
96
+ MessageSquare,
97
+ MoreHorizontal,
98
+ Paperclip,
99
+ Pencil,
100
+ Plus,
101
+ Rocket,
102
+ Search,
103
+ SlidersHorizontal,
104
+ Timer,
105
+ Trash2,
106
+ TrendingUp,
107
+ Users,
108
+ type LucideIcon,
109
+ } from 'lucide-react';
110
+ import { useTranslations } from 'next-intl';
111
+ import Link from 'next/link';
112
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
113
+ import type { ReactNode } from 'react';
114
+ import { useCallback, useMemo, useState } from 'react';
115
+ import {
116
+ Area,
117
+ AreaChart,
118
+ Bar,
119
+ BarChart,
120
+ CartesianGrid,
121
+ Cell,
122
+ Line,
123
+ LineChart,
124
+ Pie,
125
+ PieChart,
126
+ PolarAngleAxis,
127
+ RadialBar,
128
+ RadialBarChart,
129
+ XAxis,
130
+ YAxis,
131
+ } from 'recharts';
132
+ import { fetchOperations, mutateOperations } from '../_lib/api';
133
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
134
+ import type {
135
+ OperationsProjectDetails,
136
+ OperationsTaskOption,
137
+ } from '../_lib/types';
138
+ import {
139
+ formatCurrency,
140
+ formatDate,
141
+ formatDateRange,
142
+ formatEnumLabel,
143
+ formatHours,
144
+ formatPercent,
145
+ getStatusBadgeClass,
146
+ } from '../_lib/utils/format';
147
+ import { OperationsHeader } from './operations-header';
148
+ import { ProjectCostsSection } from './project-costs-section';
149
+ import { ProjectFormScreen } from './project-form-screen';
150
+ import { SectionCard } from './section-card';
151
+ import { StatusBadge } from './status-badge';
152
+ import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
153
+ import { TaskFileAttachments } from './task-file-attachments';
154
+
155
+ type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
156
+
157
+ type BoardTask = {
158
+ id: number;
159
+ name: string;
160
+ description: string | null;
161
+ status: BoardColumnId;
162
+ priority: 'low' | 'medium' | 'high';
163
+ dueDate: string | null;
164
+ estimateHours: number | null;
165
+ tags: string | null;
166
+ assigneeCollaboratorId: number | null;
167
+ assigneeName: string | null;
168
+ assigneeUserPhotoId: number | null;
169
+ assigneePersonAvatarId: number | null;
170
+ projectAssignmentId: number | null;
171
+ createdAt: string | null;
172
+ commentCount: number;
173
+ fileCount: number;
174
+ };
175
+
176
+ type ApiBoardTask = Partial<BoardTask> & {
177
+ id: number;
178
+ name: string;
179
+ status?: string | null;
180
+ priority?: BoardTask['priority'] | null;
181
+ };
182
+
183
+ type TaskFormState = {
184
+ name: string;
185
+ description: string;
186
+ priority: 'low' | 'medium' | 'high';
187
+ status: BoardColumnId;
188
+ assigneeCollaboratorId: string;
189
+ dueDate: string;
190
+ estimateHours: string;
191
+ tags: string;
192
+ };
193
+
194
+ const EMPTY_TASK_FORM: TaskFormState = {
195
+ name: '',
196
+ description: '',
197
+ priority: 'medium',
198
+ status: 'todo',
199
+ assigneeCollaboratorId: 'none',
200
+ dueDate: '',
201
+ estimateHours: '',
202
+ tags: '',
203
+ };
204
+
205
+ type BoardColumns = Record<BoardColumnId, BoardTask[]>;
206
+
207
+ type BoardState = {
208
+ projectId: number;
209
+ columns: BoardColumns;
210
+ };
211
+
212
+ const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
213
+ { id: 'todo', label: 'Backlog' },
214
+ { id: 'doing', label: 'Em execução' },
215
+ { id: 'review', label: 'Revisão' },
216
+ { id: 'done', label: 'Concluído' },
217
+ ];
218
+
219
+ // Prefer pointer-within so any column the cursor enters triggers a drop target.
220
+ // Falls back to closestCenter for the gap between columns.
221
+ const kanbanCollision: CollisionDetection = (args) => {
222
+ const within = pointerWithin(args);
223
+ if (within.length > 0) return within;
224
+ return closestCenter(args);
225
+ };
226
+
227
+ function apiTaskToBoardTask(row: ApiBoardTask): BoardTask {
228
+ const status = KANBAN_COLUMNS.some((c) => c.id === row.status)
229
+ ? (row.status as BoardColumnId)
230
+ : 'todo';
231
+ return {
232
+ id: row.id,
233
+ name: row.name,
234
+ description: row.description ?? null,
235
+ status,
236
+ priority: row.priority ?? 'medium',
237
+ dueDate: row.dueDate ?? null,
238
+ estimateHours: row.estimateHours ?? null,
239
+ tags: row.tags ?? null,
240
+ assigneeCollaboratorId: row.assigneeCollaboratorId ?? null,
241
+ assigneeName: row.assigneeName ?? null,
242
+ assigneeUserPhotoId: row.assigneeUserPhotoId ?? null,
243
+ assigneePersonAvatarId: row.assigneePersonAvatarId ?? null,
244
+ projectAssignmentId: row.projectAssignmentId ?? null,
245
+ createdAt: row.createdAt ?? null,
246
+ commentCount: (row as BoardTask & Record<string, unknown>).commentCount as number ?? 0,
247
+ fileCount: (row as BoardTask & Record<string, unknown>).fileCount as number ?? 0,
248
+ };
249
+ }
250
+
251
+ function splitTasksByColumn(tasks: BoardTask[]): BoardColumns {
252
+ return {
253
+ todo: tasks.filter((t) => t.status === 'todo'),
254
+ doing: tasks.filter((t) => t.status === 'doing'),
255
+ review: tasks.filter((t) => t.status === 'review'),
256
+ done: tasks.filter((t) => t.status === 'done'),
257
+ };
258
+ }
259
+
260
+ const boardChartConfig = {
261
+ allocation: { label: 'Alocacao', color: 'hsl(201 96% 32%)' },
262
+ loggedHours: { label: 'Horas', color: 'hsl(166 72% 28%)' },
263
+ progress: { label: 'Progresso', color: 'hsl(262 83% 58%)' },
264
+ planned: { label: 'Planejado', color: 'hsl(215 16% 47%)' },
265
+ todo: { label: 'Backlog', color: 'hsl(215 16% 47%)' },
266
+ doing: { label: 'Em execucao', color: 'hsl(201 96% 32%)' },
267
+ review: { label: 'Revisao', color: 'hsl(38 92% 50%)' },
268
+ done: { label: 'Concluido', color: 'hsl(166 72% 28%)' },
269
+ health: { label: 'Saude', color: 'hsl(166 72% 28%)' },
270
+ } satisfies ChartConfig;
271
+
272
+ function taskDragId(taskId: number) {
273
+ return `task-${taskId}`;
274
+ }
275
+
276
+ function columnDropId(columnId: BoardColumnId) {
277
+ return `col-${columnId}`;
278
+ }
279
+
280
+ function parseTaskId(value: UniqueIdentifier | null | undefined) {
281
+ if (!value) {
282
+ return null;
283
+ }
284
+
285
+ const id = String(value);
286
+ if (!id.startsWith('task-')) {
287
+ return null;
288
+ }
289
+
290
+ const parsed = Number(id.slice(5));
291
+ return Number.isFinite(parsed) ? parsed : null;
292
+ }
293
+
294
+ function parseColumnId(value: UniqueIdentifier | null | undefined) {
295
+ if (!value) {
296
+ return null;
297
+ }
298
+
299
+ const id = String(value);
300
+ if (!id.startsWith('col-')) {
301
+ return null;
302
+ }
303
+
304
+ const column = id.slice(4);
305
+ return KANBAN_COLUMNS.some((item) => item.id === column)
306
+ ? (column as BoardColumnId)
307
+ : null;
308
+ }
309
+
310
+ function DroppableColumn({
311
+ columnId,
312
+ children,
313
+ }: {
314
+ columnId: BoardColumnId;
315
+ children: (isOver: boolean) => React.ReactNode;
316
+ }) {
317
+ const { isOver, setNodeRef } = useDroppable({ id: columnDropId(columnId) });
318
+
319
+ return <div ref={setNodeRef}>{children(isOver)}</div>;
320
+ }
321
+
322
+ function DraggableTaskCard({
323
+ task,
324
+ disabled = false,
325
+ children,
326
+ }: {
327
+ task: BoardTask;
328
+ disabled?: boolean;
329
+ children: (isDragging: boolean) => ReactNode;
330
+ }) {
331
+ const { attributes, listeners, setNodeRef, transform, isDragging } =
332
+ useDraggable({ id: taskDragId(task.id), disabled });
333
+
334
+ return (
335
+ <div
336
+ ref={setNodeRef}
337
+ style={{ transform: CSS.Translate.toString(transform) }}
338
+ {...(disabled ? {} : listeners)}
339
+ {...(disabled ? {} : attributes)}
340
+ className={isDragging ? 'z-20' : undefined}
341
+ >
342
+ {children(disabled ? false : isDragging)}
343
+ </div>
344
+ );
345
+ }
346
+
347
+ function shouldOpenEditSheet(value: string | null, projectId: number) {
348
+ if (!value) {
349
+ return false;
350
+ }
351
+
352
+ return value === '1' || value === 'true' || value === String(projectId);
353
+ }
354
+
355
+ function getInitials(value?: string | null) {
356
+ const parts = String(value ?? '')
357
+ .trim()
358
+ .split(/\s+/)
359
+ .filter(Boolean)
360
+ .slice(0, 2);
361
+
362
+ if (!parts.length) {
363
+ return '??';
364
+ }
365
+
366
+ return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
367
+ }
368
+
369
+ function getPersonAvatarUrl(avatarId?: number | null) {
370
+ return typeof avatarId === 'number' && avatarId > 0
371
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
372
+ : '/placeholder.png';
373
+ }
374
+
375
+ function getUserPhotoUrl(photoId?: number | null) {
376
+ return typeof photoId === 'number' && photoId > 0
377
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/file/open/${photoId}`
378
+ : null;
379
+ }
380
+
381
+ function normalizeDateInputValue(value?: string | null) {
382
+ if (!value) {
383
+ return '';
384
+ }
385
+
386
+ const normalizedValue = String(value).trim();
387
+ const directMatch = normalizedValue.match(/^\d{4}-\d{2}-\d{2}/);
388
+
389
+ if (directMatch?.[0]) {
390
+ return directMatch[0];
391
+ }
392
+
393
+ const parsedDate = new Date(normalizedValue);
394
+ if (Number.isNaN(parsedDate.getTime())) {
395
+ return '';
396
+ }
397
+
398
+ return parsedDate.toISOString().slice(0, 10);
399
+ }
400
+
401
+ function clampPercent(value?: number | null) {
402
+ if (typeof value !== 'number' || Number.isNaN(value)) {
403
+ return 0;
404
+ }
405
+
406
+ return Math.max(0, Math.min(100, Math.round(value)));
407
+ }
408
+
409
+ function isPastDue(value?: string | null) {
410
+ if (!value) {
411
+ return false;
412
+ }
413
+
414
+ const date = new Date(value);
415
+ if (Number.isNaN(date.getTime())) {
416
+ return false;
417
+ }
418
+
419
+ const today = new Date();
420
+ today.setHours(0, 0, 0, 0);
421
+ date.setHours(0, 0, 0, 0);
422
+
423
+ return date < today;
424
+ }
425
+
426
+ function getTaskProgress(status: BoardColumnId) {
427
+ const progressByStatus: Record<BoardColumnId, number> = {
428
+ todo: 12,
429
+ doing: 48,
430
+ review: 76,
431
+ done: 100,
432
+ };
433
+
434
+ return progressByStatus[status];
435
+ }
436
+
437
+ function getPriorityClassName(priority: BoardTask['priority']) {
438
+ if (priority === 'high') {
439
+ return 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300';
440
+ }
441
+ if (priority === 'medium') {
442
+ return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300';
443
+ }
444
+
445
+ return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
446
+ }
447
+
448
+ function getColumnClassName(columnId: BoardColumnId) {
449
+ const styles: Record<BoardColumnId, string> = {
450
+ todo: 'from-slate-500/20 via-slate-500/5 to-transparent',
451
+ doing: 'from-sky-500/20 via-cyan-500/5 to-transparent',
452
+ review: 'from-amber-500/20 via-yellow-500/5 to-transparent',
453
+ done: 'from-emerald-500/20 via-green-500/5 to-transparent',
454
+ };
455
+
456
+ return styles[columnId];
457
+ }
458
+
459
+ function getColumnDotClassName(columnId: BoardColumnId) {
460
+ const styles: Record<BoardColumnId, string> = {
461
+ todo: 'bg-slate-500',
462
+ doing: 'bg-sky-500',
463
+ review: 'bg-amber-500',
464
+ done: 'bg-emerald-500',
465
+ };
466
+
467
+ return styles[columnId];
468
+ }
469
+
470
+ function getTaskTags(task: BoardTask) {
471
+ return String(task.tags ?? '')
472
+ .split(',')
473
+ .map((tag) => tag.trim())
474
+ .filter(Boolean);
475
+ }
476
+
477
+ function getTaskCommentCount(task: BoardTask) {
478
+ return task.commentCount ?? 0;
479
+ }
480
+
481
+ function getTaskAttachmentCount(task: BoardTask) {
482
+ return task.fileCount ?? 0;
483
+ }
484
+
485
+ function getAllocationTone(allocation: number) {
486
+ if (allocation > 100) {
487
+ return {
488
+ labelKey: 'overload',
489
+ text: 'text-rose-700 dark:text-rose-300',
490
+ border: 'border-rose-500/30',
491
+ bg: 'bg-rose-500/10',
492
+ progress: '[&>div]:bg-rose-500',
493
+ icon: AlertTriangle,
494
+ };
495
+ }
496
+
497
+ if (allocation >= 85) {
498
+ return {
499
+ labelKey: 'high',
500
+ text: 'text-amber-700 dark:text-amber-300',
501
+ border: 'border-amber-500/30',
502
+ bg: 'bg-amber-500/10',
503
+ progress: '[&>div]:bg-amber-500',
504
+ icon: Gauge,
505
+ };
506
+ }
507
+
508
+ return {
509
+ labelKey: 'available',
510
+ text: 'text-emerald-700 dark:text-emerald-300',
511
+ border: 'border-emerald-500/30',
512
+ bg: 'bg-emerald-500/10',
513
+ progress: '[&>div]:bg-emerald-500',
514
+ icon: CheckCircle2,
515
+ };
516
+ }
517
+
518
+ type TimelineEventType =
519
+ | 'task'
520
+ | 'timesheet'
521
+ | 'approval'
522
+ | 'comment'
523
+ | 'status';
524
+
525
+ type OperationalTimelineEvent = {
526
+ id: string;
527
+ type: TimelineEventType;
528
+ title: string;
529
+ description: string;
530
+ timestamp: string;
531
+ actorName?: string | null;
532
+ actorAvatarId?: number | null;
533
+ actorUserPhotoId?: number | null;
534
+ icon: LucideIcon;
535
+ toneClassName: string;
536
+ };
537
+
538
+ function getValidTimestamp(value?: string | null) {
539
+ if (!value) {
540
+ return null;
541
+ }
542
+
543
+ const date = new Date(value);
544
+ if (Number.isNaN(date.getTime())) {
545
+ return null;
546
+ }
547
+
548
+ return date.toISOString();
549
+ }
550
+
551
+ function formatRelativeTime(value: string, locale: string) {
552
+ const date = new Date(value);
553
+ if (Number.isNaN(date.getTime())) {
554
+ return '';
555
+ }
556
+
557
+ const diffSeconds = Math.round((date.getTime() - Date.now()) / 1000);
558
+ const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
559
+ ['year', 60 * 60 * 24 * 365],
560
+ ['month', 60 * 60 * 24 * 30],
561
+ ['day', 60 * 60 * 24],
562
+ ['hour', 60 * 60],
563
+ ['minute', 60],
564
+ ];
565
+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
566
+
567
+ for (const [unit, seconds] of units) {
568
+ if (Math.abs(diffSeconds) >= seconds) {
569
+ return formatter.format(Math.round(diffSeconds / seconds), unit);
570
+ }
571
+ }
572
+
573
+ return formatter.format(diffSeconds, 'second');
574
+ }
575
+
576
+ function getTimelineDayKey(value: string) {
577
+ const date = new Date(value);
578
+ if (Number.isNaN(date.getTime())) {
579
+ return value;
580
+ }
581
+
582
+ return date.toISOString().slice(0, 10);
583
+ }
584
+
585
+ type ProjectHealth = {
586
+ value: number;
587
+ labelKey: 'good' | 'warning' | 'danger';
588
+ tone: 'good' | 'warning' | 'danger';
589
+ };
590
+
591
+ function getProjectHealthScore({
592
+ progress,
593
+ averageAllocation,
594
+ overdueTasks,
595
+ pendingTimesheets,
596
+ }: {
597
+ progress?: number | null;
598
+ averageAllocation?: number | null;
599
+ overdueTasks: number;
600
+ pendingTimesheets: number;
601
+ }): ProjectHealth {
602
+ const progressScore = clampPercent(progress);
603
+ const allocation =
604
+ typeof averageAllocation === 'number' && !Number.isNaN(averageAllocation)
605
+ ? Math.round(averageAllocation)
606
+ : 0;
607
+ const allocationPenalty = allocation > 100 ? 16 : allocation > 85 ? 6 : 0;
608
+ const overduePenalty = Math.min(overdueTasks * 9, 32);
609
+ const timesheetPenalty = Math.min(pendingTimesheets * 4, 20);
610
+ const value = clampPercent(
611
+ 72 +
612
+ progressScore * 0.18 -
613
+ allocationPenalty -
614
+ overduePenalty -
615
+ timesheetPenalty
616
+ );
617
+
618
+ if (value >= 75) {
619
+ return { value, labelKey: 'good', tone: 'good' };
620
+ }
621
+ if (value >= 50) {
622
+ return { value, labelKey: 'warning', tone: 'warning' };
623
+ }
624
+
625
+ return { value, labelKey: 'danger', tone: 'danger' };
626
+ }
627
+
628
+ type ProjectKpiTone = 'positive' | 'warning' | 'critical' | 'info' | 'neutral';
629
+
630
+ type ProjectKpiWidgetItem = {
631
+ key: string;
632
+ title: ReactNode;
633
+ value: ReactNode;
634
+ subtitle: ReactNode;
635
+ trend: ReactNode;
636
+ indicator: number;
637
+ icon: LucideIcon;
638
+ tone: ProjectKpiTone;
639
+ };
640
+
641
+ const kpiToneStyles: Record<
642
+ ProjectKpiTone,
643
+ {
644
+ accent: string;
645
+ icon: string;
646
+ value: string;
647
+ indicator: string;
648
+ trend: string;
649
+ }
650
+ > = {
651
+ positive: {
652
+ accent: 'from-emerald-500/25 via-teal-500/10 to-transparent',
653
+ icon: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
654
+ value: 'text-emerald-700 dark:text-emerald-300',
655
+ indicator: 'bg-emerald-500',
656
+ trend: 'text-emerald-700 dark:text-emerald-300',
657
+ },
658
+ warning: {
659
+ accent: 'from-amber-500/25 via-yellow-500/10 to-transparent',
660
+ icon: 'bg-amber-500/10 text-amber-700 dark:text-amber-300',
661
+ value: 'text-amber-700 dark:text-amber-300',
662
+ indicator: 'bg-amber-500',
663
+ trend: 'text-amber-700 dark:text-amber-300',
664
+ },
665
+ critical: {
666
+ accent: 'from-rose-500/25 via-red-500/10 to-transparent',
667
+ icon: 'bg-rose-500/10 text-rose-700 dark:text-rose-300',
668
+ value: 'text-rose-700 dark:text-rose-300',
669
+ indicator: 'bg-rose-500',
670
+ trend: 'text-rose-700 dark:text-rose-300',
671
+ },
672
+ info: {
673
+ accent: 'from-sky-500/25 via-cyan-500/10 to-transparent',
674
+ icon: 'bg-sky-500/10 text-sky-700 dark:text-sky-300',
675
+ value: 'text-sky-700 dark:text-sky-300',
676
+ indicator: 'bg-sky-500',
677
+ trend: 'text-sky-700 dark:text-sky-300',
678
+ },
679
+ neutral: {
680
+ accent: 'from-violet-500/25 via-indigo-500/10 to-transparent',
681
+ icon: 'bg-violet-500/10 text-violet-700 dark:text-violet-300',
682
+ value: 'text-foreground',
683
+ indicator: 'bg-violet-500',
684
+ trend: 'text-muted-foreground',
685
+ },
686
+ };
687
+
688
+ function ProjectKpiWidget({
689
+ item,
690
+ indicatorLabel,
691
+ index = 0,
692
+ }: {
693
+ item: ProjectKpiWidgetItem;
694
+ indicatorLabel: ReactNode;
695
+ index?: number;
696
+ }) {
697
+ const Icon = item.icon;
698
+ const tone = kpiToneStyles[item.tone];
699
+
700
+ return (
701
+ <motion.div
702
+ initial={{ opacity: 0, y: 12 }}
703
+ animate={{ opacity: 1, y: 0 }}
704
+ transition={{
705
+ type: 'spring',
706
+ stiffness: 340,
707
+ damping: 26,
708
+ delay: index * 0.07,
709
+ }}
710
+ whileHover={{ y: -4 }}
711
+ className="min-w-0"
712
+ >
713
+ <Card className="group relative h-full overflow-hidden border-border/70 bg-card py-0 shadow-xs transition-shadow hover:shadow-md">
714
+ <div
715
+ className={[
716
+ 'absolute inset-x-0 top-0 h-20 bg-linear-to-br',
717
+ tone.accent,
718
+ ].join(' ')}
719
+ />
720
+ <CardContent className="relative flex h-full flex-col gap-5 p-4">
721
+ <div className="flex items-start justify-between gap-3">
722
+ <div
723
+ className={[
724
+ 'flex size-10 items-center justify-center rounded-2xl transition-transform group-hover:scale-105',
725
+ tone.icon,
726
+ ].join(' ')}
727
+ >
728
+ <Icon className="size-5" />
729
+ </div>
730
+ <span
731
+ className={[
732
+ 'rounded-full border bg-background/80 px-2 py-0.5 text-[11px] font-medium',
733
+ tone.trend,
734
+ ].join(' ')}
735
+ >
736
+ {item.trend}
737
+ </span>
738
+ </div>
739
+
740
+ <div className="min-w-0">
741
+ <div
742
+ className={[
743
+ 'truncate text-3xl font-semibold tracking-tight tabular-nums',
744
+ tone.value,
745
+ ].join(' ')}
746
+ >
747
+ {item.value}
748
+ </div>
749
+ <div className="mt-1 text-sm font-medium text-foreground">
750
+ {item.title}
751
+ </div>
752
+ <div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
753
+ {item.subtitle}
754
+ </div>
755
+ </div>
756
+
757
+ <div className="mt-auto space-y-2">
758
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
759
+ <span>{indicatorLabel}</span>
760
+ <span>{clampPercent(item.indicator)}%</span>
761
+ </div>
762
+ <div className="h-1.5 overflow-hidden rounded-full bg-muted">
763
+ <div
764
+ className={[
765
+ 'h-full rounded-full transition-all duration-500',
766
+ tone.indicator,
767
+ ].join(' ')}
768
+ style={{ width: `${clampPercent(item.indicator)}%` }}
769
+ />
770
+ </div>
771
+ </div>
772
+ </CardContent>
773
+ </Card>
774
+ </motion.div>
775
+ );
776
+ }
777
+
778
+ function ChartEmptyState({
779
+ icon: Icon,
780
+ title,
781
+ description,
782
+ }: {
783
+ icon: LucideIcon;
784
+ title: ReactNode;
785
+ description: ReactNode;
786
+ }) {
787
+ return (
788
+ <div className="flex h-72 flex-col items-center justify-center rounded-xl border border-dashed bg-muted/10 p-6 text-center">
789
+ <div className="flex size-11 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
790
+ <Icon className="size-5" />
791
+ </div>
792
+ <div className="mt-3 text-sm font-medium">{title}</div>
793
+ <div className="mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
794
+ {description}
795
+ </div>
796
+ </div>
797
+ );
798
+ }
799
+
800
+ function ProjectChartCard({
801
+ title,
802
+ description,
803
+ icon: Icon,
804
+ metric,
805
+ children,
806
+ className,
807
+ isLoading = false,
808
+ }: {
809
+ title: ReactNode;
810
+ description?: ReactNode;
811
+ icon: LucideIcon;
812
+ metric?: ReactNode;
813
+ children: ReactNode;
814
+ className?: string;
815
+ isLoading?: boolean;
816
+ }) {
817
+ return (
818
+ <motion.div
819
+ initial={{ opacity: 0, y: 8 }}
820
+ animate={{ opacity: 1, y: 0 }}
821
+ transition={{ duration: 0.22 }}
822
+ className={className}
823
+ >
824
+ <Card className="h-full overflow-hidden border-border/70 bg-card py-0 shadow-sm">
825
+ <CardContent className="flex h-full flex-col gap-4 p-4">
826
+ <div className="flex items-start justify-between gap-4">
827
+ <div className="flex min-w-0 items-start gap-3">
828
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-2xl bg-muted text-foreground">
829
+ <Icon className="size-5" />
830
+ </div>
831
+ <div className="min-w-0">
832
+ <div className="text-sm font-semibold">{title}</div>
833
+ {description ? (
834
+ <div className="mt-1 text-xs leading-5 text-muted-foreground">
835
+ {description}
836
+ </div>
837
+ ) : null}
838
+ </div>
839
+ </div>
840
+ {metric ? (
841
+ <div className="shrink-0 rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground">
842
+ {metric}
843
+ </div>
844
+ ) : null}
845
+ </div>
846
+ {isLoading ? <Skeleton className="h-72 rounded-xl" /> : children}
847
+ </CardContent>
848
+ </Card>
849
+ </motion.div>
850
+ );
851
+ }
852
+
853
+ function ProjectDetailsSkeleton() {
854
+ return (
855
+ <Page>
856
+ <div className="space-y-6">
857
+ {/* Hero header */}
858
+ <div className="overflow-hidden rounded-3xl border bg-card shadow-sm">
859
+ <div className="border-b p-5 sm:p-6">
860
+ <div className="space-y-4">
861
+ {/* Breadcrumb */}
862
+ <div className="flex items-center gap-2">
863
+ <Skeleton className="h-4 w-20 rounded-full" />
864
+ <Skeleton className="h-3 w-3 rounded-full" />
865
+ <Skeleton className="h-4 w-16 rounded-full" />
866
+ <Skeleton className="h-3 w-3 rounded-full" />
867
+ <Skeleton className="h-4 w-32 rounded-full" />
868
+ </div>
869
+ {/* Title row */}
870
+ <div className="flex items-start gap-4">
871
+ <Skeleton className="size-14 shrink-0 rounded-2xl" />
872
+ <div className="flex-1 space-y-2">
873
+ <div className="flex gap-2">
874
+ <Skeleton className="h-6 w-20 rounded-full" />
875
+ <Skeleton className="h-6 w-16 rounded-full" />
876
+ </div>
877
+ <Skeleton className="h-8 w-72" />
878
+ <Skeleton className="h-4 w-full max-w-md" />
879
+ </div>
880
+ <div className="hidden flex-shrink-0 gap-2 lg:flex">
881
+ <Skeleton className="h-9 w-20 rounded-lg" />
882
+ <Skeleton className="h-9 w-28 rounded-lg" />
883
+ <Skeleton className="h-9 w-9 rounded-lg" />
884
+ </div>
885
+ </div>
886
+ {/* Meta grid */}
887
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-7">
888
+ <Skeleton className="h-16 rounded-xl xl:col-span-2" />
889
+ <Skeleton className="h-16 rounded-xl" />
890
+ <Skeleton className="h-16 rounded-xl" />
891
+ <Skeleton className="h-16 rounded-xl" />
892
+ <Skeleton className="h-16 rounded-xl" />
893
+ <Skeleton className="h-16 rounded-xl" />
894
+ </div>
895
+ {/* Team row */}
896
+ <div className="rounded-2xl border p-4">
897
+ <div className="flex items-center justify-between gap-4">
898
+ <div className="flex items-center gap-3">
899
+ <div className="space-y-1">
900
+ <Skeleton className="h-3 w-12" />
901
+ <Skeleton className="h-5 w-24" />
902
+ </div>
903
+ <div className="flex -space-x-2">
904
+ {Array.from({ length: 4 }).map((_, i) => (
905
+ <Skeleton
906
+ key={i}
907
+ className="size-10 rounded-full border-2 border-background"
908
+ />
909
+ ))}
910
+ </div>
911
+ </div>
912
+ <div className="grid grid-cols-3 gap-3">
913
+ <Skeleton className="h-16 w-28 rounded-xl" />
914
+ <Skeleton className="h-16 w-28 rounded-xl" />
915
+ <Skeleton className="h-16 w-28 rounded-xl" />
916
+ </div>
917
+ </div>
918
+ </div>
919
+ </div>
920
+ </div>
921
+ </div>
922
+
923
+ {/* KPI row */}
924
+ <div className="rounded-3xl border p-3 sm:p-4">
925
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6">
926
+ {Array.from({ length: 6 }).map((_, i) => (
927
+ <div key={i} className="overflow-hidden rounded-xl border p-4">
928
+ <div className="flex items-start justify-between gap-3">
929
+ <Skeleton className="size-10 rounded-2xl" />
930
+ <Skeleton className="h-5 w-16 rounded-full" />
931
+ </div>
932
+ <div className="mt-5 space-y-2">
933
+ <Skeleton className="h-8 w-24" />
934
+ <Skeleton className="h-4 w-32" />
935
+ <Skeleton className="h-3 w-full" />
936
+ </div>
937
+ <div className="mt-auto pt-4 space-y-2">
938
+ <div className="flex justify-between">
939
+ <Skeleton className="h-3 w-16" />
940
+ <Skeleton className="h-3 w-8" />
941
+ </div>
942
+ <Skeleton className="h-1.5 w-full rounded-full" />
943
+ </div>
944
+ </div>
945
+ ))}
946
+ </div>
947
+ </div>
948
+
949
+ {/* Overview */}
950
+ <div className="rounded-xl border p-4">
951
+ <Skeleton className="mb-4 h-5 w-28" />
952
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
953
+ {Array.from({ length: 9 }).map((_, i) => (
954
+ <div key={i} className="space-y-1.5">
955
+ <Skeleton className="h-3 w-20" />
956
+ <Skeleton className="h-5 w-32" />
957
+ </div>
958
+ ))}
959
+ </div>
960
+ <div className="mt-6 space-y-1.5">
961
+ <Skeleton className="h-3 w-16" />
962
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
963
+ {Array.from({ length: 4 }).map((_, i) => (
964
+ <div key={i} className="space-y-1.5">
965
+ <Skeleton className="h-3 w-24" />
966
+ <Skeleton className="h-5 w-36" />
967
+ </div>
968
+ ))}
969
+ </div>
970
+ </div>
971
+ </div>
972
+
973
+ {/* Charts */}
974
+ <div className="rounded-3xl border p-4">
975
+ <Skeleton className="mb-4 h-5 w-36" />
976
+ <div className="grid gap-4 xl:grid-cols-12">
977
+ <Skeleton className="h-80 rounded-xl xl:col-span-8" />
978
+ <Skeleton className="h-80 rounded-xl xl:col-span-4" />
979
+ <Skeleton className="h-72 rounded-xl xl:col-span-5" />
980
+ <Skeleton className="h-72 rounded-xl xl:col-span-4" />
981
+ <Skeleton className="h-72 rounded-xl xl:col-span-3" />
982
+ </div>
983
+ </div>
984
+
985
+ {/* Kanban */}
986
+ <div className="rounded-3xl border p-4">
987
+ <div className="mb-4 flex items-center justify-between">
988
+ <Skeleton className="h-5 w-28" />
989
+ <Skeleton className="h-9 w-28 rounded-lg" />
990
+ </div>
991
+ <div className="mb-4 rounded-2xl border p-3">
992
+ <div className="flex gap-3">
993
+ <Skeleton className="h-10 flex-1 rounded-lg" />
994
+ <Skeleton className="h-10 w-44 rounded-lg" />
995
+ <Skeleton className="h-10 w-44 rounded-lg" />
996
+ </div>
997
+ </div>
998
+ <div className="grid gap-4 xl:grid-cols-4">
999
+ {Array.from({ length: 4 }).map((_, col) => (
1000
+ <div
1001
+ key={col}
1002
+ className="min-h-48 rounded-3xl border p-3 space-y-3"
1003
+ >
1004
+ <div className="rounded-2xl border bg-background/85 p-3 flex items-center justify-between">
1005
+ <div className="space-y-1">
1006
+ <div className="flex items-center gap-2">
1007
+ <Skeleton className="size-2.5 rounded-full" />
1008
+ <Skeleton className="h-4 w-20" />
1009
+ </div>
1010
+ <Skeleton className="h-3 w-12" />
1011
+ </div>
1012
+ <Skeleton className="size-5 rounded-full" />
1013
+ </div>
1014
+ {Array.from({ length: col === 0 ? 3 : col === 1 ? 2 : 1 }).map(
1015
+ (_, card) => (
1016
+ <div
1017
+ key={card}
1018
+ className="rounded-2xl border bg-card p-3 space-y-3"
1019
+ >
1020
+ <div className="flex items-start justify-between gap-2">
1021
+ <div className="flex-1 space-y-1">
1022
+ <Skeleton className="h-4 w-full" />
1023
+ <Skeleton className="h-3 w-3/4" />
1024
+ </div>
1025
+ <Skeleton className="h-5 w-12 rounded-full" />
1026
+ </div>
1027
+ <div className="grid grid-cols-2 gap-2">
1028
+ <Skeleton className="h-8 rounded-xl" />
1029
+ <Skeleton className="h-8 rounded-xl" />
1030
+ </div>
1031
+ <div className="space-y-1.5">
1032
+ <Skeleton className="h-1.5 w-full rounded-full" />
1033
+ </div>
1034
+ <div className="flex items-center justify-between border-t pt-3">
1035
+ <div className="flex items-center gap-2">
1036
+ <Skeleton className="size-7 rounded-full" />
1037
+ <Skeleton className="h-3 w-20" />
1038
+ </div>
1039
+ <div className="flex gap-2">
1040
+ <Skeleton className="h-3 w-6" />
1041
+ <Skeleton className="h-3 w-6" />
1042
+ </div>
1043
+ </div>
1044
+ </div>
1045
+ )
1046
+ )}
1047
+ </div>
1048
+ ))}
1049
+ </div>
1050
+ </div>
1051
+
1052
+ {/* Timeline */}
1053
+ <div className="rounded-3xl border p-4">
1054
+ <Skeleton className="mb-4 h-5 w-24" />
1055
+ <div className="rounded-3xl border p-4 space-y-6">
1056
+ {Array.from({ length: 3 }).map((_, group) => (
1057
+ <div key={group} className="space-y-3">
1058
+ <Skeleton className="h-6 w-28 rounded-full" />
1059
+ {Array.from({ length: 2 }).map((_, event) => (
1060
+ <div key={event} className="grid grid-cols-[2rem_1fr] gap-3">
1061
+ <div className="flex flex-col items-center">
1062
+ <Skeleton className="size-8 rounded-full" />
1063
+ {event === 0 ? (
1064
+ <div className="w-px flex-1 bg-border mt-1" />
1065
+ ) : null}
1066
+ </div>
1067
+ <div className="pb-5">
1068
+ <div className="rounded-2xl border p-4 space-y-3">
1069
+ <div className="flex justify-between">
1070
+ <div className="space-y-1.5">
1071
+ <Skeleton className="h-4 w-40" />
1072
+ <Skeleton className="h-3 w-56" />
1073
+ </div>
1074
+ <Skeleton className="h-3 w-16" />
1075
+ </div>
1076
+ <div className="flex items-center gap-2 border-t pt-3">
1077
+ <Skeleton className="size-7 rounded-full" />
1078
+ <Skeleton className="h-3 w-24" />
1079
+ </div>
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+ ))}
1084
+ </div>
1085
+ ))}
1086
+ </div>
1087
+ </div>
1088
+ </div>
1089
+ </Page>
1090
+ );
1091
+ }
1092
+
1093
+ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1094
+ const t = useTranslations('operations.ProjectDetailsPage');
1095
+ const commonT = useTranslations('operations.Common');
1096
+ const formT = useTranslations('operations.ProjectFormPage');
1097
+ const contractT = useTranslations('operations.ContractFormPage');
1098
+ const { request, currentLocaleCode, getSettingValue } = useApp();
1099
+ const access = useOperationsAccess();
1100
+ const isLimitedView = !access.isDirector && !access.isSupervisor;
1101
+ const router = useRouter();
1102
+ const pathname = usePathname();
1103
+ const searchParams = useSearchParams();
1104
+
1105
+ const getProjectStatusLabel = (value?: string | null) => {
1106
+ if (!value) return commonT('labels.notAvailable');
1107
+ try {
1108
+ return formT(`options.statuses.${value}`);
1109
+ } catch {
1110
+ return formatEnumLabel(value);
1111
+ }
1112
+ };
1113
+ const getContractStatusLabel = (value?: string | null) => {
1114
+ if (!value) return commonT('labels.notAvailable');
1115
+ try {
1116
+ return contractT(`options.statuses.${value}`);
1117
+ } catch {
1118
+ return formatEnumLabel(value);
1119
+ }
1120
+ };
1121
+ const getContractCategoryLabel = (value?: string | null) => {
1122
+ if (!value) return commonT('labels.notAvailable');
1123
+ try {
1124
+ return contractT(`options.contractCategories.${value}`);
1125
+ } catch {
1126
+ return formatEnumLabel(value);
1127
+ }
1128
+ };
1129
+ const getContractTypeLabel = (value?: string | null) => {
1130
+ if (!value) return commonT('labels.notAvailable');
1131
+ try {
1132
+ return contractT(`options.contractTypes.${value}`);
1133
+ } catch {
1134
+ return formatEnumLabel(value);
1135
+ }
1136
+ };
1137
+ const getSignatureStatusLabel = (value?: string | null) => {
1138
+ if (!value) return commonT('labels.notAvailable');
1139
+ try {
1140
+ return contractT(`options.signatureStatuses.${value}`);
1141
+ } catch {
1142
+ return formatEnumLabel(value);
1143
+ }
1144
+ };
1145
+
1146
+ const isEditSheetOpen = useMemo(
1147
+ () =>
1148
+ !isLimitedView &&
1149
+ shouldOpenEditSheet(searchParams.get('edit'), projectId),
1150
+ [isLimitedView, projectId, searchParams]
1151
+ );
1152
+
1153
+ const updateSheetQuery = (open: boolean) => {
1154
+ const params = new URLSearchParams(searchParams.toString());
1155
+
1156
+ if (open) {
1157
+ params.set('edit', '1');
1158
+ } else {
1159
+ params.delete('edit');
1160
+ }
1161
+
1162
+ const query = params.toString();
1163
+ router.replace(query ? `${pathname}?${query}` : pathname, {
1164
+ scroll: false,
1165
+ });
1166
+ };
1167
+
1168
+ const openEditSheet = () => {
1169
+ updateSheetQuery(true);
1170
+ };
1171
+
1172
+ const closeEditSheet = () => {
1173
+ updateSheetQuery(false);
1174
+ };
1175
+
1176
+ const getDeliveryModelLabel = (value?: string | null) => {
1177
+ if (!value) {
1178
+ return commonT('labels.notAvailable');
1179
+ }
1180
+
1181
+ try {
1182
+ return formT(`options.deliveryModels.${value}`);
1183
+ } catch {
1184
+ return formatEnumLabel(value);
1185
+ }
1186
+ };
1187
+
1188
+ const getBillingModelLabel = (value?: string | null) => {
1189
+ if (!value) {
1190
+ return commonT('labels.notAvailable');
1191
+ }
1192
+
1193
+ try {
1194
+ return formT(`options.billingModels.${value}`);
1195
+ } catch {
1196
+ return formatEnumLabel(value);
1197
+ }
1198
+ };
1199
+
1200
+ const getTaskPriorityLabel = (value?: string | null) => {
1201
+ const labels = currentLocaleCode.startsWith('pt')
1202
+ ? { low: 'Baixa', medium: 'Média', high: 'Alta' }
1203
+ : { low: 'Low', medium: 'Medium', high: 'High' };
1204
+
1205
+ return labels[value as keyof typeof labels] ?? formatEnumLabel(value);
1206
+ };
1207
+
1208
+ const {
1209
+ data: project,
1210
+ refetch,
1211
+ isLoading: isProjectLoading,
1212
+ } = useQuery<OperationsProjectDetails>({
1213
+ queryKey: ['operations-project-details', currentLocaleCode, projectId],
1214
+ queryFn: () =>
1215
+ fetchOperations<OperationsProjectDetails>(
1216
+ request,
1217
+ `/operations/projects/${projectId}`
1218
+ ),
1219
+ });
1220
+
1221
+ const {
1222
+ data: rawTasks = [],
1223
+ refetch: refetchTasks,
1224
+ isLoading: isTasksLoading,
1225
+ } = useQuery<ApiBoardTask[]>({
1226
+ queryKey: ['operations-project-board-tasks', projectId],
1227
+ queryFn: () =>
1228
+ fetchOperations<ApiBoardTask[]>(
1229
+ request,
1230
+ `/operations/projects/${projectId}/tasks`
1231
+ ),
1232
+ enabled: Boolean(project),
1233
+ });
1234
+
1235
+ const { data: archivedTasksResponse, refetch: refetchArchivedTasks } =
1236
+ useQuery<{ data: OperationsTaskOption[] }>({
1237
+ queryKey: ['operations-project-archived-tasks', projectId],
1238
+ queryFn: () =>
1239
+ fetchOperations<{ data: OperationsTaskOption[] }>(
1240
+ request,
1241
+ `/operations/tasks?projectId=${projectId}&pageSize=100&sortField=createdAt&sortOrder=desc&archived=true`
1242
+ ),
1243
+ enabled: Boolean(project),
1244
+ });
1245
+
1246
+ const { data: projectStats, isLoading: isProjectStatsLoading } = useQuery<{
1247
+ weeklyVelocity: Array<{ weekLabel: string; loggedHours: number }>;
1248
+ allocationByCollaborator: Array<{ name: string; allocation: number }>;
1249
+ quickRadar: {
1250
+ activeAssignments: number;
1251
+ pendingTimesheets: number;
1252
+ totalWeeklyHours: number;
1253
+ };
1254
+ }>({
1255
+ queryKey: ['operations-project-stats', projectId],
1256
+ queryFn: () =>
1257
+ fetchOperations(request, `/operations/projects/${projectId}/stats`),
1258
+ enabled: Boolean(project) && !isLimitedView,
1259
+ });
1260
+
1261
+ const [boardState, setBoardState] = useState<BoardState | null>(null);
1262
+ const [selectedTask, setSelectedTask] = useState<TaskDetailSheetData | null>(
1263
+ null
1264
+ );
1265
+ const [taskFormOpen, setTaskFormOpen] = useState(false);
1266
+ const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
1267
+ const [taskFormData, setTaskFormData] =
1268
+ useState<TaskFormState>(EMPTY_TASK_FORM);
1269
+ const [taskFormLoading, setTaskFormLoading] = useState(false);
1270
+ const [deletePromptTask, setDeletePromptTask] =
1271
+ useState<TaskDetailSheetData | null>(null);
1272
+ const [inlineCreateColumn, setInlineCreateColumn] =
1273
+ useState<BoardColumnId | null>(null);
1274
+ const [inlineCreateName, setInlineCreateName] = useState('');
1275
+ const [inlineCreateLoading, setInlineCreateLoading] = useState(false);
1276
+ const [boardSearch, setBoardSearch] = useState('');
1277
+ const [boardPriorityFilter, setBoardPriorityFilter] = useState<
1278
+ 'all' | BoardTask['priority']
1279
+ >('all');
1280
+ const [boardGroupMode, setBoardGroupMode] = useState('status');
1281
+ const [timelineTypeFilter, setTimelineTypeFilter] = useState<
1282
+ 'all' | TimelineEventType
1283
+ >('all');
1284
+ const [timelineVisibleCount, setTimelineVisibleCount] = useState(8);
1285
+ const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
1286
+ const [restoringTaskId, setRestoringTaskId] = useState<number | null>(null);
1287
+ const [deletingTaskId, setDeletingTaskId] = useState<number | null>(null);
1288
+ const [activeDragTask, setActiveDragTask] = useState<BoardTask | null>(null);
1289
+
1290
+ const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
1291
+ const archivedTasks = useMemo(
1292
+ () =>
1293
+ (archivedTasksResponse?.data ?? []).filter((task) =>
1294
+ Boolean(task.deletedAt)
1295
+ ),
1296
+ [archivedTasksResponse]
1297
+ );
1298
+
1299
+ const taskColumns: BoardColumns = useMemo(() => {
1300
+ if (project && boardState?.projectId === project.id) {
1301
+ return boardState.columns;
1302
+ }
1303
+ return splitTasksByColumn(apiTasks);
1304
+ }, [project, boardState, apiTasks]);
1305
+
1306
+ const filteredTaskColumns: BoardColumns = useMemo(() => {
1307
+ const normalizedSearch = boardSearch.trim().toLocaleLowerCase();
1308
+ const filterTask = (task: BoardTask) => {
1309
+ const matchesSearch =
1310
+ !normalizedSearch ||
1311
+ [
1312
+ task.name,
1313
+ task.description,
1314
+ task.assigneeName,
1315
+ task.tags,
1316
+ task.priority,
1317
+ ]
1318
+ .filter(Boolean)
1319
+ .some((value) =>
1320
+ String(value).toLocaleLowerCase().includes(normalizedSearch)
1321
+ );
1322
+ const matchesPriority =
1323
+ boardPriorityFilter === 'all' || task.priority === boardPriorityFilter;
1324
+
1325
+ return matchesSearch && matchesPriority;
1326
+ };
1327
+
1328
+ return {
1329
+ todo: taskColumns.todo.filter(filterTask),
1330
+ doing: taskColumns.doing.filter(filterTask),
1331
+ review: taskColumns.review.filter(filterTask),
1332
+ done: taskColumns.done.filter(filterTask),
1333
+ };
1334
+ }, [boardPriorityFilter, boardSearch, taskColumns]);
1335
+
1336
+ const taskAssigneeOptions = useMemo(() => {
1337
+ const seen = new Set<number>();
1338
+ return (
1339
+ project?.assignments
1340
+ .filter((assignment) => {
1341
+ if (
1342
+ !assignment.collaboratorId ||
1343
+ seen.has(assignment.collaboratorId)
1344
+ ) {
1345
+ return false;
1346
+ }
1347
+ seen.add(assignment.collaboratorId);
1348
+ return true;
1349
+ })
1350
+ .map((assignment) => ({
1351
+ id: String(assignment.collaboratorId),
1352
+ label: assignment.collaboratorName,
1353
+ })) ?? []
1354
+ );
1355
+ }, [project]);
1356
+
1357
+ const openCreateTaskForm = useCallback(
1358
+ (defaultStatus: BoardColumnId = 'todo') => {
1359
+ setEditingTaskId(null);
1360
+ setTaskFormData({ ...EMPTY_TASK_FORM, status: defaultStatus });
1361
+ setTaskFormOpen(true);
1362
+ },
1363
+ []
1364
+ );
1365
+
1366
+ const openEditTaskForm = useCallback((task: BoardTask) => {
1367
+ setEditingTaskId(task.id);
1368
+ setTaskFormData({
1369
+ name: task.name,
1370
+ description: task.description ?? '',
1371
+ priority: task.priority,
1372
+ status: task.status,
1373
+ assigneeCollaboratorId: task.assigneeCollaboratorId
1374
+ ? String(task.assigneeCollaboratorId)
1375
+ : 'none',
1376
+ dueDate: normalizeDateInputValue(task.dueDate),
1377
+ estimateHours:
1378
+ task.estimateHours != null ? String(task.estimateHours) : '',
1379
+ tags: task.tags ?? '',
1380
+ });
1381
+ setSelectedTask(null);
1382
+ setTaskFormOpen(true);
1383
+ }, []);
1384
+
1385
+ const handleTaskFormSubmit = useCallback(async () => {
1386
+ if (!taskFormData.name.trim()) return;
1387
+ setTaskFormLoading(true);
1388
+ try {
1389
+ const payload: Record<string, unknown> = {
1390
+ name: taskFormData.name.trim(),
1391
+ description: taskFormData.description || null,
1392
+ priority: taskFormData.priority,
1393
+ status: taskFormData.status,
1394
+ assigneeCollaboratorId:
1395
+ taskFormData.assigneeCollaboratorId !== 'none'
1396
+ ? Number(taskFormData.assigneeCollaboratorId)
1397
+ : null,
1398
+ dueDate: taskFormData.dueDate || null,
1399
+ estimateHours: taskFormData.estimateHours
1400
+ ? Number(taskFormData.estimateHours)
1401
+ : null,
1402
+ tags: taskFormData.tags || null,
1403
+ };
1404
+ if (editingTaskId) {
1405
+ await mutateOperations(
1406
+ request,
1407
+ `/operations/tasks/${editingTaskId}`,
1408
+ 'PATCH',
1409
+ payload
1410
+ );
1411
+ } else {
1412
+ await mutateOperations(request, '/operations/tasks', 'POST', {
1413
+ projectId,
1414
+ ...payload,
1415
+ });
1416
+ }
1417
+ setBoardState(null);
1418
+ await refetchTasks();
1419
+ await refetchArchivedTasks();
1420
+ setTaskFormOpen(false);
1421
+ setEditingTaskId(null);
1422
+ setTaskFormData(EMPTY_TASK_FORM);
1423
+ } finally {
1424
+ setTaskFormLoading(false);
1425
+ }
1426
+ }, [
1427
+ taskFormData,
1428
+ editingTaskId,
1429
+ projectId,
1430
+ request,
1431
+ refetchTasks,
1432
+ refetchArchivedTasks,
1433
+ ]);
1434
+
1435
+ const handleArchiveTask = useCallback(
1436
+ async (taskId: number) => {
1437
+ setArchivingTaskId(taskId);
1438
+ try {
1439
+ await mutateOperations(
1440
+ request,
1441
+ `/operations/tasks/${taskId}`,
1442
+ 'PATCH',
1443
+ {
1444
+ archived: true,
1445
+ }
1446
+ );
1447
+ setBoardState(null);
1448
+ setSelectedTask(null);
1449
+ await refetchTasks();
1450
+ await refetchArchivedTasks();
1451
+ } catch {
1452
+ // ignore
1453
+ } finally {
1454
+ setArchivingTaskId(null);
1455
+ }
1456
+ },
1457
+ [request, refetchTasks, refetchArchivedTasks]
1458
+ );
1459
+
1460
+ const handleRestoreTask = useCallback(
1461
+ async (taskId: number) => {
1462
+ setRestoringTaskId(taskId);
1463
+ try {
1464
+ await mutateOperations(
1465
+ request,
1466
+ `/operations/tasks/${taskId}`,
1467
+ 'PATCH',
1468
+ {
1469
+ archived: false,
1470
+ }
1471
+ );
1472
+ setSelectedTask(null);
1473
+ await refetchTasks();
1474
+ await refetchArchivedTasks();
1475
+ } catch {
1476
+ // ignore
1477
+ } finally {
1478
+ setRestoringTaskId(null);
1479
+ }
1480
+ },
1481
+ [request, refetchTasks, refetchArchivedTasks]
1482
+ );
1483
+
1484
+ const handleInlineCreateTask = useCallback(
1485
+ async (column: BoardColumnId, name: string) => {
1486
+ const trimmed = name.trim();
1487
+ if (!trimmed) {
1488
+ setInlineCreateColumn(null);
1489
+ setInlineCreateName('');
1490
+ return;
1491
+ }
1492
+ setInlineCreateLoading(true);
1493
+ try {
1494
+ await mutateOperations(request, '/operations/tasks', 'POST', {
1495
+ projectId,
1496
+ name: trimmed,
1497
+ status: column,
1498
+ priority: 'medium',
1499
+ });
1500
+ setBoardState(null);
1501
+ setInlineCreateColumn(null);
1502
+ setInlineCreateName('');
1503
+ await refetchTasks();
1504
+ } finally {
1505
+ setInlineCreateLoading(false);
1506
+ }
1507
+ },
1508
+ [projectId, request, refetchTasks]
1509
+ );
1510
+
1511
+ const handleDeleteTask = useCallback(
1512
+ async (taskId: number) => {
1513
+ setDeletingTaskId(taskId);
1514
+ try {
1515
+ await mutateOperations(
1516
+ request,
1517
+ `/operations/tasks/${taskId}?permanent=true`,
1518
+ 'DELETE'
1519
+ );
1520
+ setBoardState(null);
1521
+ setSelectedTask(null);
1522
+ setDeletePromptTask(null);
1523
+ await refetchTasks();
1524
+ await refetchArchivedTasks();
1525
+ } catch {
1526
+ // ignore
1527
+ } finally {
1528
+ setDeletingTaskId(null);
1529
+ }
1530
+ },
1531
+ [request, refetchTasks, refetchArchivedTasks]
1532
+ );
1533
+
1534
+ const allocationChartData = useMemo(() => {
1535
+ if (projectStats?.allocationByCollaborator?.length) {
1536
+ return projectStats.allocationByCollaborator;
1537
+ }
1538
+ if (!project) {
1539
+ return [];
1540
+ }
1541
+ return project.assignments.slice(0, 6).map((assignment) => ({
1542
+ name: getInitials(assignment.collaboratorName),
1543
+ allocation:
1544
+ typeof assignment.allocationPercent === 'number'
1545
+ ? Math.round(assignment.allocationPercent)
1546
+ : 0,
1547
+ }));
1548
+ }, [project, projectStats]);
1549
+
1550
+ const sensors = useSensors(
1551
+ useSensor(PointerSensor, {
1552
+ activationConstraint: { distance: 6 },
1553
+ })
1554
+ );
1555
+
1556
+ const velocityChartData = useMemo(() => {
1557
+ if (projectStats?.weeklyVelocity?.length) {
1558
+ return projectStats.weeklyVelocity.map((row) => ({
1559
+ week: row.weekLabel,
1560
+ loggedHours: row.loggedHours,
1561
+ completedTasks: 0,
1562
+ }));
1563
+ }
1564
+ return [];
1565
+ }, [projectStats]);
1566
+
1567
+ const findColumnByTask = useCallback(
1568
+ (taskId: number) => {
1569
+ const match = KANBAN_COLUMNS.find((column) =>
1570
+ taskColumns[column.id].some((task) => task.id === taskId)
1571
+ );
1572
+
1573
+ return match?.id ?? null;
1574
+ },
1575
+ [taskColumns]
1576
+ );
1577
+
1578
+ const moveTaskToColumn = useCallback(
1579
+ (taskId: number, targetColumn: BoardColumnId) => {
1580
+ const originColumn = findColumnByTask(taskId);
1581
+ if (!originColumn || originColumn === targetColumn) {
1582
+ return;
1583
+ }
1584
+
1585
+ const sourceTask = taskColumns[originColumn].find(
1586
+ (task) => task.id === taskId
1587
+ );
1588
+ if (!sourceTask || !project) {
1589
+ return;
1590
+ }
1591
+
1592
+ // Optimistic update
1593
+ setBoardState({
1594
+ projectId: project.id,
1595
+ columns: {
1596
+ ...taskColumns,
1597
+ [originColumn]: taskColumns[originColumn].filter(
1598
+ (task) => task.id !== taskId
1599
+ ),
1600
+ [targetColumn]: [
1601
+ { ...sourceTask, status: targetColumn },
1602
+ ...taskColumns[targetColumn],
1603
+ ],
1604
+ },
1605
+ });
1606
+
1607
+ // Persist to API
1608
+ mutateOperations(request, `/operations/tasks/${taskId}`, 'PATCH', {
1609
+ status: targetColumn,
1610
+ }).catch(() => {
1611
+ // Rollback optimistic update on error
1612
+ setBoardState(null);
1613
+ void refetchTasks();
1614
+ });
1615
+ },
1616
+ [findColumnByTask, taskColumns, project, request, refetchTasks]
1617
+ );
1618
+
1619
+ const onBoardDragStart = (event: DragStartEvent) => {
1620
+ const taskId = parseTaskId(event.active.id);
1621
+ if (!taskId) return;
1622
+ const col = findColumnByTask(taskId);
1623
+ if (!col) return;
1624
+ const task = taskColumns[col].find((t) => t.id === taskId) ?? null;
1625
+ setActiveDragTask(task);
1626
+ };
1627
+
1628
+ const onBoardDragEnd = (event: DragEndEvent) => {
1629
+ setActiveDragTask(null);
1630
+ const taskId = parseTaskId(event.active.id);
1631
+ const targetColumn = parseColumnId(event.over?.id);
1632
+
1633
+ if (!taskId || !targetColumn) {
1634
+ return;
1635
+ }
1636
+
1637
+ moveTaskToColumn(taskId, targetColumn);
1638
+ };
1639
+
1640
+ const timelineEvents = useMemo<OperationalTimelineEvent[]>(() => {
1641
+ if (!project) {
1642
+ return [];
1643
+ }
1644
+
1645
+ const events: OperationalTimelineEvent[] = [];
1646
+ const projectStart = getValidTimestamp(project.startDate);
1647
+ const projectEnd = getValidTimestamp(project.endDate);
1648
+
1649
+ if (projectStart) {
1650
+ events.push({
1651
+ id: `project-start-${project.id}`,
1652
+ type: 'status',
1653
+ title: t('timeline.projectStarted'),
1654
+ description: t('timeline.projectStartedDescription', {
1655
+ project: project.name,
1656
+ }),
1657
+ timestamp: projectStart,
1658
+ actorName: project.managerName,
1659
+ actorAvatarId: project.managerAvatarId,
1660
+ icon: Rocket,
1661
+ toneClassName: 'bg-sky-500 text-white',
1662
+ });
1663
+ }
1664
+
1665
+ if (projectEnd) {
1666
+ events.push({
1667
+ id: `project-deadline-${project.id}`,
1668
+ type: 'status',
1669
+ title: t('timeline.targetDate'),
1670
+ description: t('timeline.targetDateDescription'),
1671
+ timestamp: projectEnd,
1672
+ actorName: project.managerName,
1673
+ actorAvatarId: project.managerAvatarId,
1674
+ icon: CalendarClock,
1675
+ toneClassName: isPastDue(project.endDate)
1676
+ ? 'bg-rose-500 text-white'
1677
+ : 'bg-violet-500 text-white',
1678
+ });
1679
+ }
1680
+
1681
+ apiTasks.forEach((task) => {
1682
+ const taskCreatedAt =
1683
+ getValidTimestamp(task.createdAt) ??
1684
+ getValidTimestamp(task.dueDate) ??
1685
+ projectStart;
1686
+ const actorName = task.assigneeName || project.managerName;
1687
+
1688
+ if (taskCreatedAt) {
1689
+ events.push({
1690
+ id: `task-created-${task.id}`,
1691
+ type: 'task',
1692
+ title: t('timeline.taskCreated'),
1693
+ description: task.name,
1694
+ timestamp: taskCreatedAt,
1695
+ actorName,
1696
+ actorAvatarId: task.assigneePersonAvatarId,
1697
+ actorUserPhotoId: task.assigneeUserPhotoId,
1698
+ icon: Plus,
1699
+ toneClassName: 'bg-slate-500 text-white',
1700
+ });
1701
+ }
1702
+
1703
+ if (task.status === 'done') {
1704
+ events.push({
1705
+ id: `task-done-${task.id}`,
1706
+ type: 'status',
1707
+ title: t('timeline.taskCompleted'),
1708
+ description: task.name,
1709
+ timestamp:
1710
+ getValidTimestamp(task.dueDate) ??
1711
+ taskCreatedAt ??
1712
+ new Date().toISOString(),
1713
+ actorName,
1714
+ actorAvatarId: task.assigneePersonAvatarId,
1715
+ actorUserPhotoId: task.assigneeUserPhotoId,
1716
+ icon: CheckCircle2,
1717
+ toneClassName: 'bg-emerald-500 text-white',
1718
+ });
1719
+ }
1720
+
1721
+ if (task.description) {
1722
+ events.push({
1723
+ id: `task-comment-${task.id}`,
1724
+ type: 'comment',
1725
+ title: t('timeline.commentAdded'),
1726
+ description: task.name,
1727
+ timestamp: taskCreatedAt ?? new Date().toISOString(),
1728
+ actorName,
1729
+ actorAvatarId: task.assigneePersonAvatarId,
1730
+ actorUserPhotoId: task.assigneeUserPhotoId,
1731
+ icon: MessageSquare,
1732
+ toneClassName: 'bg-indigo-500 text-white',
1733
+ });
1734
+ }
1735
+ });
1736
+
1737
+ if (project.timesheetSummary.totalTimesheets > 0) {
1738
+ events.push({
1739
+ id: `timesheets-${project.id}`,
1740
+ type: 'timesheet',
1741
+ title: t('timeline.timesheetLogged'),
1742
+ description: t('timeline.timesheetLoggedDescription', {
1743
+ count: project.timesheetSummary.totalTimesheets,
1744
+ hours: formatHours(project.timesheetSummary.totalHours),
1745
+ }),
1746
+ timestamp: new Date().toISOString(),
1747
+ actorName: project.managerName,
1748
+ actorAvatarId: project.managerAvatarId,
1749
+ icon: Timer,
1750
+ toneClassName: 'bg-cyan-500 text-white',
1751
+ });
1752
+ }
1753
+
1754
+ if (project.timesheetSummary.pendingTimesheets > 0) {
1755
+ events.push({
1756
+ id: `approvals-${project.id}`,
1757
+ type: 'approval',
1758
+ title: t('timeline.approvalPending'),
1759
+ description: t('timeline.approvalPendingDescription', {
1760
+ count: project.timesheetSummary.pendingTimesheets,
1761
+ }),
1762
+ timestamp: new Date().toISOString(),
1763
+ actorName: project.managerName,
1764
+ actorAvatarId: project.managerAvatarId,
1765
+ icon: GitCommitHorizontal,
1766
+ toneClassName: 'bg-amber-500 text-white',
1767
+ });
1768
+ }
1769
+
1770
+ return events.sort(
1771
+ (a, b) =>
1772
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
1773
+ );
1774
+ }, [apiTasks, project, t]);
1775
+
1776
+ if (isProjectLoading) {
1777
+ return <ProjectDetailsSkeleton />;
1778
+ }
1779
+
1780
+ if (!project) {
1781
+ return (
1782
+ <Page>
1783
+ <OperationsHeader
1784
+ title={t('title')}
1785
+ description={t('description')}
1786
+ current={t('breadcrumb')}
1787
+ />
1788
+ <EmptyState
1789
+ icon={<FolderKanban className="size-12" />}
1790
+ title={commonT('states.emptyTitle')}
1791
+ description={t('notFound')}
1792
+ actionLabel={commonT('actions.refresh')}
1793
+ onAction={() => void refetch()}
1794
+ />
1795
+ </Page>
1796
+ );
1797
+ }
1798
+
1799
+ const totalTasks = apiTasks.length;
1800
+ const completedTasks = taskColumns.done.length;
1801
+ const pendingTasks = totalTasks - completedTasks;
1802
+ const activeCollaborators =
1803
+ project.operationalIndicators.activeAssignments ||
1804
+ project.assignments.filter((assignment) => assignment.status === 'active')
1805
+ .length ||
1806
+ project.assignments.length;
1807
+ const overdueTasks = apiTasks.filter(
1808
+ (task) => task.status !== 'done' && isPastDue(task.dueDate)
1809
+ ).length;
1810
+ const projectProgress = clampPercent(project.progressPercent);
1811
+ const taskProgress =
1812
+ totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
1813
+ const displayedProgress = projectProgress || taskProgress;
1814
+ const averageAllocation = clampPercent(
1815
+ project.operationalIndicators.averageAllocation
1816
+ );
1817
+ const weeklyVelocity =
1818
+ projectStats?.weeklyVelocity?.at(-1)?.loggedHours ??
1819
+ projectStats?.quickRadar?.totalWeeklyHours ??
1820
+ project.operationalIndicators.totalWeeklyHours;
1821
+ const projectHealth = getProjectHealthScore({
1822
+ progress: displayedProgress,
1823
+ averageAllocation,
1824
+ overdueTasks,
1825
+ pendingTimesheets: project.timesheetSummary.pendingTimesheets,
1826
+ });
1827
+ const projectHealthLabel =
1828
+ projectHealth.labelKey === 'good'
1829
+ ? t('health.good')
1830
+ : projectHealth.labelKey === 'warning'
1831
+ ? t('health.warning')
1832
+ : t('health.danger');
1833
+ const projectHealthTrend =
1834
+ projectHealth.labelKey === 'good'
1835
+ ? t('kpi.trends.health.good')
1836
+ : projectHealth.labelKey === 'warning'
1837
+ ? t('kpi.trends.health.warning')
1838
+ : t('kpi.trends.health.danger');
1839
+ const teamPreview = project.assignments.slice(0, 5);
1840
+ const hiddenTeamCount = Math.max(
1841
+ project.assignments.length - teamPreview.length,
1842
+ 0
1843
+ );
1844
+ const overloadedAssignments = project.assignments.filter(
1845
+ (assignment) => (assignment.allocationPercent ?? 0) > 100
1846
+ ).length;
1847
+ const highAllocationAssignments = project.assignments.filter((assignment) => {
1848
+ const allocation = assignment.allocationPercent ?? 0;
1849
+ return allocation >= 85 && allocation <= 100;
1850
+ }).length;
1851
+ const availableAssignments = project.assignments.filter(
1852
+ (assignment) => (assignment.allocationPercent ?? 0) < 85
1853
+ ).length;
1854
+ const burnupChartData =
1855
+ velocityChartData.length > 0
1856
+ ? velocityChartData.reduce<
1857
+ Array<{ week: string; loggedHours: number; planned: number }>
1858
+ >((items, row, index) => {
1859
+ const previous = items[index - 1]?.loggedHours ?? 0;
1860
+ const loggedHours = previous + Number(row.loggedHours ?? 0);
1861
+ const planned =
1862
+ project.operationalIndicators.totalWeeklyHours > 0
1863
+ ? project.operationalIndicators.totalWeeklyHours * (index + 1)
1864
+ : loggedHours;
1865
+ items.push({ week: row.week, loggedHours, planned });
1866
+ return items;
1867
+ }, [])
1868
+ : [
1869
+ { week: t('charts.start'), loggedHours: 0, planned: 0 },
1870
+ {
1871
+ week: t('charts.current'),
1872
+ loggedHours: project.timesheetSummary.totalHours,
1873
+ planned:
1874
+ project.operationalIndicators.totalWeeklyHours ||
1875
+ project.timesheetSummary.totalHours,
1876
+ },
1877
+ ];
1878
+ const taskDistributionData = KANBAN_COLUMNS.map((column) => ({
1879
+ key: column.id,
1880
+ name: column.label,
1881
+ value: taskColumns[column.id].length,
1882
+ fill: `var(--color-${column.id})`,
1883
+ })).filter((item) => item.value > 0);
1884
+ const healthChartData = [
1885
+ {
1886
+ name: t('charts.healthScore'),
1887
+ value: projectHealth.value,
1888
+ fill:
1889
+ projectHealth.tone === 'danger'
1890
+ ? 'hsl(0 84% 60%)'
1891
+ : projectHealth.tone === 'warning'
1892
+ ? 'hsl(38 92% 50%)'
1893
+ : 'var(--color-health)',
1894
+ },
1895
+ ];
1896
+ const chartDashboardLoading = isProjectStatsLoading || isTasksLoading;
1897
+ const filteredTimelineEvents = timelineEvents.filter(
1898
+ (event) => timelineTypeFilter === 'all' || event.type === timelineTypeFilter
1899
+ );
1900
+ const visibleTimelineEvents = filteredTimelineEvents.slice(
1901
+ 0,
1902
+ timelineVisibleCount
1903
+ );
1904
+ const groupedTimelineEvents = visibleTimelineEvents.reduce<
1905
+ Array<{ dayKey: string; events: OperationalTimelineEvent[] }>
1906
+ >((groups, event) => {
1907
+ const dayKey = getTimelineDayKey(event.timestamp);
1908
+ const currentGroup = groups[groups.length - 1];
1909
+ if (currentGroup?.dayKey === dayKey) {
1910
+ currentGroup.events.push(event);
1911
+ } else {
1912
+ groups.push({ dayKey, events: [event] });
1913
+ }
1914
+ return groups;
1915
+ }, []);
1916
+
1917
+ const allocationTone: ProjectKpiTone =
1918
+ project.operationalIndicators.averageAllocation > 100
1919
+ ? 'critical'
1920
+ : project.operationalIndicators.averageAllocation > 85
1921
+ ? 'warning'
1922
+ : 'positive';
1923
+ const pendingTasksTone: ProjectKpiTone =
1924
+ overdueTasks > 0 ? 'critical' : pendingTasks > 0 ? 'warning' : 'positive';
1925
+ const velocityTone: ProjectKpiTone = weeklyVelocity > 0 ? 'positive' : 'info';
1926
+
1927
+ const kpiWidgets: ProjectKpiWidgetItem[] = [
1928
+ {
1929
+ key: 'hours',
1930
+ title: t('cards.loggedHours'),
1931
+ value: formatHours(project.timesheetSummary.totalHours),
1932
+ subtitle: t('cards.loggedHoursDescription'),
1933
+ trend: t('kpi.trends.hours', {
1934
+ count: project.timesheetSummary.totalTimesheets,
1935
+ }),
1936
+ indicator: Math.min(project.timesheetSummary.totalHours, 100),
1937
+ icon: Timer,
1938
+ tone: 'info',
1939
+ },
1940
+ {
1941
+ key: 'health',
1942
+ title: t('cards.projectHealth'),
1943
+ value: projectHealthLabel,
1944
+ subtitle: t('kpi.subtitles.health'),
1945
+ trend: projectHealthTrend,
1946
+ indicator: projectHealth.value,
1947
+ icon: HeartPulse,
1948
+ tone:
1949
+ projectHealth.tone === 'danger'
1950
+ ? 'critical'
1951
+ : projectHealth.tone === 'warning'
1952
+ ? 'warning'
1953
+ : 'positive',
1954
+ },
1955
+ {
1956
+ key: 'velocity',
1957
+ title: t('cards.weeklyVelocity'),
1958
+ value: formatHours(weeklyVelocity),
1959
+ subtitle: t('cards.weeklyVelocityDescription'),
1960
+ trend:
1961
+ weeklyVelocity > 0
1962
+ ? t('kpi.trends.velocity.active')
1963
+ : t('kpi.trends.velocity.empty'),
1964
+ indicator: Math.min(weeklyVelocity, 100),
1965
+ icon: TrendingUp,
1966
+ tone: velocityTone,
1967
+ },
1968
+ {
1969
+ key: 'allocation',
1970
+ title: t('cards.allocation'),
1971
+ value: formatPercent(project.operationalIndicators.averageAllocation),
1972
+ subtitle: t('cards.allocationDescription'),
1973
+ trend:
1974
+ allocationTone === 'critical'
1975
+ ? t('kpi.trends.allocation.critical')
1976
+ : allocationTone === 'warning'
1977
+ ? t('kpi.trends.allocation.warning')
1978
+ : t('kpi.trends.allocation.good'),
1979
+ indicator: averageAllocation,
1980
+ icon: Gauge,
1981
+ tone: allocationTone,
1982
+ },
1983
+ {
1984
+ key: 'pendingTasks',
1985
+ title: t('cards.pendingTasks'),
1986
+ value: pendingTasks,
1987
+ subtitle: t('cards.pendingTasksDescription', { overdue: overdueTasks }),
1988
+ trend:
1989
+ pendingTasksTone === 'critical'
1990
+ ? t('kpi.trends.tasks.critical', { count: overdueTasks })
1991
+ : pendingTasksTone === 'warning'
1992
+ ? t('kpi.trends.tasks.warning')
1993
+ : t('kpi.trends.tasks.good'),
1994
+ indicator:
1995
+ totalTasks > 0 ? Math.round((pendingTasks / totalTasks) * 100) : 0,
1996
+ icon: ClipboardList,
1997
+ tone: pendingTasksTone,
1998
+ },
1999
+ {
2000
+ key: 'activeCollaborators',
2001
+ title: t('cards.activeCollaborators'),
2002
+ value: activeCollaborators,
2003
+ subtitle: t('cards.activeCollaboratorsDescription'),
2004
+ trend:
2005
+ activeCollaborators > 0
2006
+ ? t('kpi.trends.collaborators.active')
2007
+ : t('kpi.trends.collaborators.empty'),
2008
+ indicator:
2009
+ project.assignments.length > 0
2010
+ ? Math.round((activeCollaborators / project.assignments.length) * 100)
2011
+ : 0,
2012
+ icon: Users,
2013
+ tone: activeCollaborators > 0 ? 'positive' : 'warning',
2014
+ },
2015
+ ];
2016
+
2017
+ return (
2018
+ <Page>
2019
+ <motion.section
2020
+ initial={{ opacity: 0, y: 10 }}
2021
+ animate={{ opacity: 1, y: 0 }}
2022
+ transition={{ duration: 0.25 }}
2023
+ className="overflow-hidden rounded-3xl border bg-card shadow-sm"
2024
+ >
2025
+ <div className="relative overflow-hidden border-b bg-linear-to-br from-muted/70 via-background to-background p-5 sm:p-6">
2026
+ <div className="absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-primary/40 to-transparent" />
2027
+ <div className="flex flex-col gap-6">
2028
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
2029
+ <div className="min-w-0 space-y-4">
2030
+ <nav
2031
+ aria-label="Breadcrumb"
2032
+ className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground"
2033
+ >
2034
+ <Link
2035
+ href="/operations/projects"
2036
+ className="transition hover:text-foreground"
2037
+ >
2038
+ {t('breadcrumbTrail.operations')}
2039
+ </Link>
2040
+ <ChevronRight className="size-3.5" />
2041
+ <Link
2042
+ href="/operations/projects"
2043
+ className="transition hover:text-foreground"
2044
+ >
2045
+ {t('breadcrumbTrail.projects')}
2046
+ </Link>
2047
+ <ChevronRight className="size-3.5" />
2048
+ <span className="max-w-[12rem] truncate font-medium text-foreground sm:max-w-md">
2049
+ {project.code || project.name}
2050
+ </span>
2051
+ </nav>
2052
+
2053
+ <div className="flex min-w-0 items-start gap-4">
2054
+ <div className="flex size-14 shrink-0 items-center justify-center rounded-2xl border bg-background shadow-xs">
2055
+ <FolderKanban className="size-7 text-primary" />
2056
+ </div>
2057
+ <div className="min-w-0 space-y-2">
2058
+ <div className="flex flex-wrap items-center gap-2">
2059
+ <span className="rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground">
2060
+ {project.code || commonT('labels.notAvailable')}
2061
+ </span>
2062
+ <StatusBadge
2063
+ label={getProjectStatusLabel(project.status)}
2064
+ className={getStatusBadgeClass(project.status)}
2065
+ />
2066
+ </div>
2067
+ <div className="space-y-1.5">
2068
+ <h1 className="max-w-5xl text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
2069
+ {project.name}
2070
+ </h1>
2071
+ <p className="max-w-3xl text-sm leading-6 text-muted-foreground">
2072
+ {project.summary || t('executive.fallbackDescription')}
2073
+ </p>
2074
+ </div>
2075
+ </div>
2076
+ </div>
2077
+ </div>
2078
+
2079
+ <div className="flex flex-wrap gap-2 lg:justify-end">
2080
+ {access.isDirector ? (
2081
+ <Button
2082
+ size="sm"
2083
+ onClick={openEditSheet}
2084
+ className="cursor-pointer gap-2"
2085
+ >
2086
+ <Pencil className="size-4" />
2087
+ {commonT('actions.edit')}
2088
+ </Button>
2089
+ ) : null}
2090
+ {!isLimitedView ? (
2091
+ <Button
2092
+ variant="outline"
2093
+ size="sm"
2094
+ onClick={() => openCreateTaskForm()}
2095
+ className="cursor-pointer gap-2"
2096
+ >
2097
+ <Plus className="size-4" />
2098
+ {t('taskForm.titleNew')}
2099
+ </Button>
2100
+ ) : null}
2101
+ <Button variant="outline" size="sm" asChild>
2102
+ <Link href="/operations/timesheets">
2103
+ <Timer className="size-4" />
2104
+ {t('quickActions.timesheet')}
2105
+ </Link>
2106
+ </Button>
2107
+ <DropdownMenu>
2108
+ <DropdownMenuTrigger asChild>
2109
+ <Button
2110
+ variant="outline"
2111
+ size="icon"
2112
+ className="size-9 cursor-pointer"
2113
+ aria-label={t('quickActions.more')}
2114
+ >
2115
+ <MoreHorizontal className="size-4" />
2116
+ </Button>
2117
+ </DropdownMenuTrigger>
2118
+ <DropdownMenuContent align="end" className="w-56">
2119
+ <DropdownMenuItem asChild>
2120
+ <Link href="/operations/reports/projects">
2121
+ <BarChart2 className="size-4" />
2122
+ {t('quickActions.reports')}
2123
+ </Link>
2124
+ </DropdownMenuItem>
2125
+ {project.contractId ? (
2126
+ <DropdownMenuItem asChild>
2127
+ <Link
2128
+ href={`/operations/contracts?edit=${project.contractId}`}
2129
+ >
2130
+ <FileText className="size-4" />
2131
+ {commonT('actions.openContract')}
2132
+ </Link>
2133
+ </DropdownMenuItem>
2134
+ ) : null}
2135
+ <DropdownMenuSeparator />
2136
+ <DropdownMenuItem asChild>
2137
+ <Link href="/operations/projects">
2138
+ <FolderKanban className="size-4" />
2139
+ {t('breadcrumbTrail.projects')}
2140
+ </Link>
2141
+ </DropdownMenuItem>
2142
+ </DropdownMenuContent>
2143
+ </DropdownMenu>
2144
+ </div>
2145
+ </div>
2146
+
2147
+ <div className="grid gap-3 text-sm md:grid-cols-2 xl:grid-cols-7">
2148
+ <div className="rounded-xl border bg-background/70 p-3 xl:col-span-2">
2149
+ <div className="flex items-center gap-2 text-muted-foreground">
2150
+ <Users className="size-4" />
2151
+ {commonT('labels.client')}
2152
+ </div>
2153
+ <div className="mt-2 flex items-center gap-2 font-medium">
2154
+ <Avatar className="size-7 border bg-muted">
2155
+ <AvatarImage
2156
+ src={getPersonAvatarUrl(project.clientAvatarId)}
2157
+ alt={project.clientName || commonT('labels.client')}
2158
+ />
2159
+ <AvatarFallback className="text-[10px]">
2160
+ {getInitials(project.clientName)}
2161
+ </AvatarFallback>
2162
+ </Avatar>
2163
+ <span className="truncate">
2164
+ {project.clientName || commonT('labels.notAvailable')}
2165
+ </span>
2166
+ </div>
2167
+ </div>
2168
+ <div className="rounded-xl border bg-background/70 p-3">
2169
+ <div className="flex items-center gap-2 text-muted-foreground">
2170
+ <Rocket className="size-4" />
2171
+ {commonT('labels.deliveryModel')}
2172
+ </div>
2173
+ <div className="mt-2 truncate font-medium">
2174
+ {project.deliveryModel
2175
+ ? getDeliveryModelLabel(project.deliveryModel)
2176
+ : commonT('labels.notAvailable')}
2177
+ </div>
2178
+ </div>
2179
+ <div className="rounded-xl border bg-background/70 p-3">
2180
+ <div className="flex items-center gap-2 text-muted-foreground">
2181
+ <Users className="size-4" />
2182
+ {commonT('labels.manager')}
2183
+ </div>
2184
+ <div className="mt-2 truncate font-medium">
2185
+ {project.managerName || commonT('labels.notAssigned')}
2186
+ </div>
2187
+ </div>
2188
+ <div className="rounded-xl border bg-background/70 p-3">
2189
+ <div className="flex items-center gap-2 text-muted-foreground">
2190
+ <CalendarDays className="size-4" />
2191
+ {commonT('labels.startDate')}
2192
+ </div>
2193
+ <div className="mt-2 font-medium">
2194
+ {formatDate(
2195
+ project.startDate,
2196
+ getSettingValue,
2197
+ currentLocaleCode
2198
+ )}
2199
+ </div>
2200
+ </div>
2201
+ <div className="rounded-xl border bg-background/70 p-3">
2202
+ <div className="flex items-center gap-2 text-muted-foreground">
2203
+ <CalendarClock className="size-4" />
2204
+ {commonT('labels.endDate')}
2205
+ </div>
2206
+ <div className="mt-2 font-medium">
2207
+ {formatDate(
2208
+ project.endDate,
2209
+ getSettingValue,
2210
+ currentLocaleCode
2211
+ )}
2212
+ </div>
2213
+ </div>
2214
+ <div className="rounded-xl border bg-background/70 p-3">
2215
+ <div className="flex items-center gap-2 text-muted-foreground">
2216
+ <Gauge className="size-4" />
2217
+ {commonT('labels.progress')}
2218
+ </div>
2219
+ <div className="mt-2 flex items-center gap-3">
2220
+ <Progress value={displayedProgress} className="h-2" />
2221
+ <span className="text-sm font-semibold tabular-nums">
2222
+ {displayedProgress}%
2223
+ </span>
2224
+ </div>
2225
+ </div>
2226
+ </div>
2227
+
2228
+ <div className="flex flex-col gap-4 rounded-2xl border bg-background/75 p-4 sm:flex-row sm:items-center sm:justify-between">
2229
+ <div className="flex items-center gap-3">
2230
+ <div>
2231
+ <div className="flex items-center gap-2 text-muted-foreground">
2232
+ <Users className="size-4" />
2233
+ <span className="text-xs font-medium uppercase tracking-[0.18em]">
2234
+ {t('executive.team')}
2235
+ </span>
2236
+ </div>
2237
+ <div className="mt-1 text-sm font-semibold">
2238
+ {t('executive.membersCount', {
2239
+ count: project.assignments.length,
2240
+ })}
2241
+ </div>
2242
+ </div>
2243
+ <div className="flex -space-x-2">
2244
+ {teamPreview.map((assignment) => (
2245
+ <Tooltip key={assignment.id}>
2246
+ <TooltipTrigger asChild>
2247
+ <Avatar className="size-10 cursor-default border-2 border-background bg-muted transition-transform hover:z-10 hover:scale-110">
2248
+ <AvatarImage
2249
+ src={
2250
+ getUserPhotoUrl(assignment.userPhotoId) ||
2251
+ getPersonAvatarUrl(assignment.personAvatarId)
2252
+ }
2253
+ alt={assignment.collaboratorName}
2254
+ />
2255
+ <AvatarFallback className="text-xs">
2256
+ {getInitials(assignment.collaboratorName)}
2257
+ </AvatarFallback>
2258
+ </Avatar>
2259
+ </TooltipTrigger>
2260
+ <TooltipContent side="bottom" className="text-xs">
2261
+ <p className="font-medium">
2262
+ {assignment.collaboratorName}
2263
+ </p>
2264
+ {assignment.roleLabel ? (
2265
+ <p className="text-muted-foreground">
2266
+ {assignment.roleLabel}
2267
+ </p>
2268
+ ) : null}
2269
+ </TooltipContent>
2270
+ </Tooltip>
2271
+ ))}
2272
+ {hiddenTeamCount > 0 ? (
2273
+ <div className="flex size-10 items-center justify-center rounded-full border-2 border-background bg-muted text-xs font-semibold text-muted-foreground">
2274
+ +{hiddenTeamCount}
2275
+ </div>
2276
+ ) : null}
2277
+ </div>
2278
+ </div>
2279
+
2280
+ <div className="grid grid-cols-3 gap-3 text-sm sm:min-w-80">
2281
+ <div className="rounded-xl border bg-muted/20 p-3">
2282
+ <div className="text-muted-foreground">
2283
+ {commonT('labels.status')}
2284
+ </div>
2285
+ <div className="mt-1">
2286
+ <StatusBadge
2287
+ label={getProjectStatusLabel(project.status)}
2288
+ className={getStatusBadgeClass(project.status)}
2289
+ />
2290
+ </div>
2291
+ </div>
2292
+ <div className="rounded-xl border bg-muted/20 p-3">
2293
+ <div className="text-muted-foreground">
2294
+ {t('cards.projectHealth')}
2295
+ </div>
2296
+ <div className="mt-1 font-semibold">{projectHealthLabel}</div>
2297
+ </div>
2298
+ <div className="rounded-xl border bg-muted/20 p-3">
2299
+ <div className="text-muted-foreground">
2300
+ {t('executive.completedTasks')}
2301
+ </div>
2302
+ <div className="mt-1 font-semibold">
2303
+ {completedTasks}/{totalTasks}
2304
+ </div>
2305
+ </div>
2306
+ </div>
2307
+ </div>
2308
+ </div>
2309
+ </div>
2310
+ </motion.section>
2311
+
2312
+ {!isLimitedView ? (
2313
+ <>
2314
+ <div className="rounded-3xl border bg-linear-to-b from-muted/50 to-background p-3 shadow-sm sm:p-4">
2315
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6">
2316
+ {kpiWidgets.map((item, index) => (
2317
+ <ProjectKpiWidget
2318
+ key={item.key}
2319
+ item={item}
2320
+ index={index}
2321
+ indicatorLabel={t('kpi.indicator')}
2322
+ />
2323
+ ))}
2324
+ </div>
2325
+ </div>
2326
+
2327
+ <div className="grid gap-4 xl:grid-cols-12">
2328
+ <SectionCard
2329
+ title={t('sections.overview')}
2330
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-12"
2331
+ >
2332
+ <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
2333
+ <div>
2334
+ <dt className="text-muted-foreground">
2335
+ {commonT('labels.project')}
2336
+ </dt>
2337
+ <dd className="font-medium">{project.name}</dd>
2338
+ </div>
2339
+ <div>
2340
+ <dt className="text-muted-foreground">
2341
+ {commonT('labels.code')}
2342
+ </dt>
2343
+ <dd className="font-medium">
2344
+ {project.code || commonT('labels.notAvailable')}
2345
+ </dd>
2346
+ </div>
2347
+ <div>
2348
+ <dt className="text-muted-foreground">
2349
+ {commonT('labels.client')}
2350
+ </dt>
2351
+ <dd className="font-medium">
2352
+ <div className="flex items-center gap-2">
2353
+ <Avatar className="h-8 w-8 border border-border/60 bg-muted">
2354
+ <AvatarImage
2355
+ src={getPersonAvatarUrl(project.clientAvatarId)}
2356
+ alt={project.clientName || commonT('labels.client')}
2357
+ />
2358
+ <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
2359
+ {getInitials(
2360
+ project.clientName || commonT('labels.client')
2361
+ )}
2362
+ </AvatarFallback>
2363
+ </Avatar>
2364
+ <span>
2365
+ {project.clientName || commonT('labels.notAvailable')}
2366
+ </span>
2367
+ </div>
2368
+ </dd>
2369
+ </div>
2370
+ <div>
2371
+ <dt className="text-muted-foreground">
2372
+ {commonT('labels.manager')}
2373
+ </dt>
2374
+ <dd className="font-medium">
2375
+ {project.managerName || commonT('labels.notAssigned')}
2376
+ </dd>
2377
+ </div>
2378
+ <div>
2379
+ <dt className="text-muted-foreground">
2380
+ {commonT('labels.status')}
2381
+ </dt>
2382
+ <dd className="font-medium">
2383
+ <StatusBadge
2384
+ label={getProjectStatusLabel(project.status)}
2385
+ className={getStatusBadgeClass(project.status)}
2386
+ />
2387
+ </dd>
2388
+ </div>
2389
+ <div>
2390
+ <dt className="text-muted-foreground">
2391
+ {commonT('labels.deliveryModel')}
2392
+ </dt>
2393
+ <dd className="font-medium">
2394
+ {project.deliveryModel
2395
+ ? getDeliveryModelLabel(project.deliveryModel)
2396
+ : commonT('labels.notAvailable')}
2397
+ </dd>
2398
+ </div>
2399
+ <div>
2400
+ <dt className="text-muted-foreground">
2401
+ {commonT('labels.startDate')}
2402
+ </dt>
2403
+ <dd className="font-medium">
2404
+ {formatDate(
2405
+ project.startDate,
2406
+ getSettingValue,
2407
+ currentLocaleCode
2408
+ )}
2409
+ </dd>
2410
+ </div>
2411
+ <div>
2412
+ <dt className="text-muted-foreground">
2413
+ {commonT('labels.endDate')}
2414
+ </dt>
2415
+ <dd className="font-medium">
2416
+ {formatDate(
2417
+ project.endDate,
2418
+ getSettingValue,
2419
+ currentLocaleCode
2420
+ )}
2421
+ </dd>
2422
+ </div>
2423
+ <div>
2424
+ <dt className="text-muted-foreground">
2425
+ {commonT('labels.budget')}
2426
+ </dt>
2427
+ <dd className="font-medium">
2428
+ {project.budgetAmount
2429
+ ? formatCurrency(
2430
+ project.budgetAmount,
2431
+ getSettingValue,
2432
+ currentLocaleCode
2433
+ )
2434
+ : commonT('labels.notAvailable')}
2435
+ </dd>
2436
+ </div>
2437
+ <div>
2438
+ <dt className="text-muted-foreground">
2439
+ {commonT('labels.progress')}
2440
+ </dt>
2441
+ <dd className="font-medium">
2442
+ {formatPercent(project.progressPercent)}
2443
+ </dd>
2444
+ </div>
2445
+ <div>
2446
+ <dt className="text-muted-foreground">
2447
+ {commonT('labels.timeline')}
2448
+ </dt>
2449
+ <dd className="font-medium">
2450
+ {formatDateRange(
2451
+ project.startDate,
2452
+ project.endDate,
2453
+ getSettingValue,
2454
+ currentLocaleCode
2455
+ )}
2456
+ </dd>
2457
+ </div>
2458
+ <div>
2459
+ <dt className="text-muted-foreground">
2460
+ {commonT('labels.contractStatus')}
2461
+ </dt>
2462
+ <dd className="font-medium">
2463
+ {project.contractStatus ? (
2464
+ <StatusBadge
2465
+ label={getContractStatusLabel(project.contractStatus)}
2466
+ className={getStatusBadgeClass(project.contractStatus)}
2467
+ />
2468
+ ) : (
2469
+ commonT('labels.notAssigned')
2470
+ )}
2471
+ </dd>
2472
+ </div>
2473
+ </dl>
2474
+ {project.summary ? (
2475
+ <div className="mt-4 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm text-muted-foreground">
2476
+ {project.summary}
2477
+ </div>
2478
+ ) : null}
2479
+
2480
+ {/* Contrato vinculado */}
2481
+ <div className="mt-6 border-t pt-6">
2482
+ <div className="mb-4 flex items-center gap-2 text-sm font-semibold">
2483
+ <FileText className="size-4 text-muted-foreground" />
2484
+ {t('sections.contract')}
2485
+ </div>
2486
+ {project.relatedContract ? (
2487
+ <div className="space-y-4">
2488
+ <div className="flex flex-col gap-3 rounded-xl border bg-muted/20 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
2489
+ <div>
2490
+ <div className="font-medium">
2491
+ {project.relatedContract.name}
2492
+ </div>
2493
+ <div className="text-sm text-muted-foreground">
2494
+ {[
2495
+ project.relatedContract.code,
2496
+ project.relatedContract.clientName,
2497
+ ]
2498
+ .filter(Boolean)
2499
+ .join(' • ') || commonT('labels.notAvailable')}
2500
+ </div>
2501
+ </div>
2502
+ <div className="flex items-center gap-3">
2503
+ <StatusBadge
2504
+ label={getContractStatusLabel(
2505
+ project.relatedContract.status
2506
+ )}
2507
+ className={getStatusBadgeClass(
2508
+ project.relatedContract.status
2509
+ )}
2510
+ />
2511
+ <Button
2512
+ variant="outline"
2513
+ size="sm"
2514
+ asChild
2515
+ className="shrink-0"
2516
+ >
2517
+ <Link
2518
+ href={`/operations/contracts?edit=${project.relatedContract.id}`}
2519
+ >
2520
+ <FileText className="size-4" />
2521
+ {commonT('actions.openContract')}
2522
+ </Link>
2523
+ </Button>
2524
+ </div>
2525
+ </div>
2526
+ <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
2527
+ <div>
2528
+ <dt className="text-muted-foreground">
2529
+ {commonT('labels.contractCategory')}
2530
+ </dt>
2531
+ <dd className="font-medium">
2532
+ {project.relatedContract.contractCategory
2533
+ ? getContractCategoryLabel(
2534
+ project.relatedContract.contractCategory
2535
+ )
2536
+ : commonT('labels.notAvailable')}
2537
+ </dd>
2538
+ </div>
2539
+ <div>
2540
+ <dt className="text-muted-foreground">
2541
+ {commonT('labels.contractType')}
2542
+ </dt>
2543
+ <dd className="font-medium">
2544
+ {project.relatedContract.contractType
2545
+ ? getContractTypeLabel(
2546
+ project.relatedContract.contractType
2547
+ )
2548
+ : commonT('labels.notAvailable')}
2549
+ </dd>
2550
+ </div>
2551
+ <div>
2552
+ <dt className="text-muted-foreground">
2553
+ {commonT('labels.billingModel')}
2554
+ </dt>
2555
+ <dd className="font-medium">
2556
+ {getBillingModelLabel(
2557
+ project.relatedContract.billingModel
2558
+ )}
2559
+ </dd>
2560
+ </div>
2561
+ <div>
2562
+ <dt className="text-muted-foreground">
2563
+ {commonT('labels.timeline')}
2564
+ </dt>
2565
+ <dd className="font-medium">
2566
+ {formatDateRange(
2567
+ project.relatedContract.startDate,
2568
+ project.relatedContract.endDate,
2569
+ getSettingValue,
2570
+ currentLocaleCode
2571
+ )}
2572
+ </dd>
2573
+ </div>
2574
+ <div>
2575
+ <dt className="text-muted-foreground">
2576
+ {commonT('labels.signatureStatus')}
2577
+ </dt>
2578
+ <dd className="font-medium">
2579
+ {project.relatedContract.signatureStatus
2580
+ ? getSignatureStatusLabel(
2581
+ project.relatedContract.signatureStatus
2582
+ )
2583
+ : commonT('labels.notAvailable')}
2584
+ </dd>
2585
+ </div>
2586
+ <div>
2587
+ <dt className="text-muted-foreground">
2588
+ {commonT('labels.budget')}
2589
+ </dt>
2590
+ <dd className="font-medium">
2591
+ {project.relatedContract.budgetAmount
2592
+ ? formatCurrency(
2593
+ project.relatedContract.budgetAmount,
2594
+ getSettingValue,
2595
+ currentLocaleCode
2596
+ )
2597
+ : commonT('labels.notAvailable')}
2598
+ </dd>
2599
+ </div>
2600
+ </dl>
2601
+ </div>
2602
+ ) : (
2603
+ <p className="text-sm text-muted-foreground">
2604
+ {t('noContract')}
2605
+ </p>
2606
+ )}
2607
+ </div>
2608
+ </SectionCard>
2609
+ </div>
2610
+
2611
+ <SectionCard
2612
+ title={t('sections.deliveryHealth')}
2613
+ description={t('sections.deliveryHealthDescription')}
2614
+ className="rounded-3xl border bg-card p-4 shadow-sm"
2615
+ >
2616
+ <div className="grid gap-4 xl:grid-cols-12">
2617
+ <ProjectChartCard
2618
+ title={t('charts.burnup')}
2619
+ description={t('charts.burnupDescription')}
2620
+ icon={LineChartIcon}
2621
+ metric={formatPercent(displayedProgress)}
2622
+ className="xl:col-span-8"
2623
+ isLoading={chartDashboardLoading}
2624
+ >
2625
+ {burnupChartData.length > 1 ? (
2626
+ <ChartContainer
2627
+ className="h-80 w-full"
2628
+ config={boardChartConfig}
2629
+ >
2630
+ <AreaChart data={burnupChartData}>
2631
+ <defs>
2632
+ <linearGradient
2633
+ id="burnupLogged"
2634
+ x1="0"
2635
+ y1="0"
2636
+ x2="0"
2637
+ y2="1"
2638
+ >
2639
+ <stop
2640
+ offset="5%"
2641
+ stopColor="var(--color-loggedHours)"
2642
+ stopOpacity={0.26}
2643
+ />
2644
+ <stop
2645
+ offset="95%"
2646
+ stopColor="var(--color-loggedHours)"
2647
+ stopOpacity={0.02}
2648
+ />
2649
+ </linearGradient>
2650
+ </defs>
2651
+ <CartesianGrid vertical={false} strokeDasharray="3 3" />
2652
+ <XAxis dataKey="week" tickLine={false} axisLine={false} />
2653
+ <YAxis tickLine={false} axisLine={false} width={36} />
2654
+ <ChartTooltip content={<ChartTooltipContent />} />
2655
+ <Area
2656
+ type="monotone"
2657
+ dataKey="planned"
2658
+ stroke="var(--color-planned)"
2659
+ strokeDasharray="4 4"
2660
+ strokeWidth={2}
2661
+ fill="transparent"
2662
+ />
2663
+ <Area
2664
+ type="monotone"
2665
+ dataKey="loggedHours"
2666
+ stroke="var(--color-loggedHours)"
2667
+ strokeWidth={2.5}
2668
+ fill="url(#burnupLogged)"
2669
+ />
2670
+ </AreaChart>
2671
+ </ChartContainer>
2672
+ ) : (
2673
+ <ChartEmptyState
2674
+ icon={LineChartIcon}
2675
+ title={t('charts.emptyTitle')}
2676
+ description={t('charts.emptyBurnup')}
2677
+ />
2678
+ )}
2679
+ </ProjectChartCard>
2680
+
2681
+ <ProjectChartCard
2682
+ title={t('charts.weeklyVelocity')}
2683
+ description={t('charts.weeklyVelocityDescription')}
2684
+ icon={Rocket}
2685
+ metric={formatHours(weeklyVelocity)}
2686
+ className="xl:col-span-4"
2687
+ isLoading={isProjectStatsLoading}
2688
+ >
2689
+ {velocityChartData.length > 0 ? (
2690
+ <ChartContainer
2691
+ className="h-80 w-full"
2692
+ config={boardChartConfig}
2693
+ >
2694
+ <LineChart data={velocityChartData}>
2695
+ <CartesianGrid vertical={false} strokeDasharray="3 3" />
2696
+ <XAxis dataKey="week" tickLine={false} axisLine={false} />
2697
+ <YAxis tickLine={false} axisLine={false} width={30} />
2698
+ <ChartTooltip content={<ChartTooltipContent />} />
2699
+ <Line
2700
+ type="monotone"
2701
+ dataKey="loggedHours"
2702
+ stroke="var(--color-loggedHours)"
2703
+ strokeWidth={2.5}
2704
+ dot={{ r: 3 }}
2705
+ activeDot={{ r: 5 }}
2706
+ />
2707
+ </LineChart>
2708
+ </ChartContainer>
2709
+ ) : (
2710
+ <ChartEmptyState
2711
+ icon={Rocket}
2712
+ title={t('charts.emptyTitle')}
2713
+ description={t('charts.emptyVelocity')}
2714
+ />
2715
+ )}
2716
+ </ProjectChartCard>
2717
+
2718
+ <ProjectChartCard
2719
+ title={t('charts.allocationByCollaborator')}
2720
+ description={t('charts.allocationDescription')}
2721
+ icon={BarChart3}
2722
+ metric={formatPercent(
2723
+ project.operationalIndicators.averageAllocation
2724
+ )}
2725
+ className="xl:col-span-5"
2726
+ isLoading={chartDashboardLoading}
2727
+ >
2728
+ {allocationChartData.length > 0 ? (
2729
+ <ChartContainer
2730
+ className="h-72 w-full"
2731
+ config={boardChartConfig}
2732
+ >
2733
+ <BarChart data={allocationChartData}>
2734
+ <CartesianGrid vertical={false} strokeDasharray="3 3" />
2735
+ <XAxis dataKey="name" tickLine={false} axisLine={false} />
2736
+ <YAxis tickLine={false} axisLine={false} width={32} />
2737
+ <ChartTooltip
2738
+ content={<ChartTooltipContent hideLabel />}
2739
+ />
2740
+ <Bar
2741
+ dataKey="allocation"
2742
+ radius={[8, 8, 3, 3]}
2743
+ fill="var(--color-allocation)"
2744
+ />
2745
+ </BarChart>
2746
+ </ChartContainer>
2747
+ ) : (
2748
+ <ChartEmptyState
2749
+ icon={BarChart3}
2750
+ title={t('charts.emptyTitle')}
2751
+ description={t('charts.emptyAllocation')}
2752
+ />
2753
+ )}
2754
+ </ProjectChartCard>
2755
+
2756
+ <ProjectChartCard
2757
+ title={t('charts.taskDistribution')}
2758
+ description={t('charts.taskDistributionDescription')}
2759
+ icon={ClipboardList}
2760
+ metric={`${totalTasks} ${t('kanban.items')}`}
2761
+ className="xl:col-span-4"
2762
+ isLoading={isTasksLoading}
2763
+ >
2764
+ {taskDistributionData.length > 0 ? (
2765
+ <div className="grid gap-4 md:grid-cols-[1fr_11rem]">
2766
+ <ChartContainer
2767
+ className="h-72 w-full"
2768
+ config={boardChartConfig}
2769
+ >
2770
+ <PieChart>
2771
+ <ChartTooltip
2772
+ content={<ChartTooltipContent hideLabel />}
2773
+ />
2774
+ <Pie
2775
+ data={taskDistributionData}
2776
+ dataKey="value"
2777
+ nameKey="name"
2778
+ innerRadius={58}
2779
+ outerRadius={92}
2780
+ paddingAngle={4}
2781
+ >
2782
+ {taskDistributionData.map((entry) => (
2783
+ <Cell key={entry.key} fill={entry.fill} />
2784
+ ))}
2785
+ </Pie>
2786
+ </PieChart>
2787
+ </ChartContainer>
2788
+ <div className="flex flex-col justify-center gap-2">
2789
+ {taskDistributionData.map((item) => (
2790
+ <div
2791
+ key={item.key}
2792
+ className="flex items-center justify-between gap-3 rounded-lg border bg-muted/10 px-3 py-2 text-xs"
2793
+ >
2794
+ <span className="flex min-w-0 items-center gap-2">
2795
+ <span
2796
+ className="size-2.5 shrink-0 rounded-full"
2797
+ style={{ backgroundColor: item.fill }}
2798
+ />
2799
+ <span className="truncate">{item.name}</span>
2800
+ </span>
2801
+ <span className="font-semibold">{item.value}</span>
2802
+ </div>
2803
+ ))}
2804
+ </div>
2805
+ </div>
2806
+ ) : (
2807
+ <ChartEmptyState
2808
+ icon={ClipboardList}
2809
+ title={t('charts.emptyTitle')}
2810
+ description={t('charts.emptyTasks')}
2811
+ />
2812
+ )}
2813
+ </ProjectChartCard>
2814
+
2815
+ <ProjectChartCard
2816
+ title={t('charts.operationalHealth')}
2817
+ description={t('charts.operationalHealthDescription')}
2818
+ icon={HeartPulse}
2819
+ metric={projectHealthLabel}
2820
+ className="xl:col-span-3"
2821
+ isLoading={chartDashboardLoading}
2822
+ >
2823
+ <div className="grid h-72 place-items-center">
2824
+ <ChartContainer
2825
+ className="h-56 w-full"
2826
+ config={boardChartConfig}
2827
+ >
2828
+ <RadialBarChart
2829
+ data={healthChartData}
2830
+ innerRadius="72%"
2831
+ outerRadius="100%"
2832
+ startAngle={180}
2833
+ endAngle={0}
2834
+ >
2835
+ <PolarAngleAxis
2836
+ type="number"
2837
+ domain={[0, 100]}
2838
+ tick={false}
2839
+ />
2840
+ <RadialBar
2841
+ dataKey="value"
2842
+ cornerRadius={12}
2843
+ background={{ fill: 'hsl(var(--muted))' }}
2844
+ />
2845
+ <ChartTooltip
2846
+ content={<ChartTooltipContent hideLabel />}
2847
+ />
2848
+ </RadialBarChart>
2849
+ </ChartContainer>
2850
+ <div className="-mt-20 text-center">
2851
+ <div className="text-3xl font-semibold tabular-nums">
2852
+ {projectHealth.value}%
2853
+ </div>
2854
+ <div className="mt-1 text-xs text-muted-foreground">
2855
+ {t('charts.healthScore')}
2856
+ </div>
2857
+ </div>
2858
+ </div>
2859
+ </ProjectChartCard>
2860
+ </div>
2861
+ </SectionCard>
2862
+ </>
2863
+ ) : null}
2864
+
2865
+ <SectionCard
2866
+ title={t('sections.taskBoard')}
2867
+ description={t('sections.taskBoardDescription')}
2868
+ className="rounded-3xl border bg-card p-4 shadow-sm"
2869
+ actions={
2870
+ !isLimitedView ? (
2871
+ <Button
2872
+ size="sm"
2873
+ variant="default"
2874
+ className="gap-2"
2875
+ onClick={() => openCreateTaskForm()}
2876
+ >
2877
+ <Plus className="size-4" />
2878
+ {t('taskForm.titleNew')}
2879
+ </Button>
2880
+ ) : undefined
2881
+ }
2882
+ >
2883
+ <div className="mb-4 flex flex-col gap-3 rounded-2xl border bg-muted/20 p-3 lg:flex-row lg:items-center lg:justify-between">
2884
+ <div className="relative min-w-0 flex-1">
2885
+ <Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
2886
+ <Input
2887
+ value={boardSearch}
2888
+ onChange={(event) => setBoardSearch(event.target.value)}
2889
+ placeholder={t('kanban.searchPlaceholder')}
2890
+ className="h-10 bg-background pl-9"
2891
+ />
2892
+ </div>
2893
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
2894
+ <div className="flex items-center gap-2 rounded-xl border bg-background px-3 py-2 text-xs text-muted-foreground">
2895
+ <SlidersHorizontal className="size-4" />
2896
+ {t('kanban.filters')}
2897
+ </div>
2898
+ <Select
2899
+ value={boardPriorityFilter}
2900
+ onValueChange={(value) =>
2901
+ setBoardPriorityFilter(value as typeof boardPriorityFilter)
2902
+ }
2903
+ >
2904
+ <SelectTrigger className="h-10 w-full bg-background sm:w-44">
2905
+ <SelectValue />
2906
+ </SelectTrigger>
2907
+ <SelectContent>
2908
+ <SelectItem value="all">{t('kanban.allPriorities')}</SelectItem>
2909
+ <SelectItem value="high">
2910
+ {getTaskPriorityLabel('high')}
2911
+ </SelectItem>
2912
+ <SelectItem value="medium">
2913
+ {getTaskPriorityLabel('medium')}
2914
+ </SelectItem>
2915
+ <SelectItem value="low">
2916
+ {getTaskPriorityLabel('low')}
2917
+ </SelectItem>
2918
+ </SelectContent>
2919
+ </Select>
2920
+ <Select value={boardGroupMode} onValueChange={setBoardGroupMode}>
2921
+ <SelectTrigger className="h-10 w-full bg-background sm:w-44">
2922
+ <SelectValue />
2923
+ </SelectTrigger>
2924
+ <SelectContent>
2925
+ <SelectItem value="status">
2926
+ {t('kanban.groupStatus')}
2927
+ </SelectItem>
2928
+ </SelectContent>
2929
+ </Select>
2930
+ </div>
2931
+ </div>
2932
+
2933
+ <DndContext
2934
+ sensors={sensors}
2935
+ collisionDetection={kanbanCollision}
2936
+ onDragStart={onBoardDragStart}
2937
+ onDragCancel={() => setActiveDragTask(null)}
2938
+ onDragEnd={onBoardDragEnd}
2939
+ >
2940
+ <div className="relative">
2941
+ <div className="grid auto-cols-[minmax(19rem,1fr)] grid-flow-col gap-4 overflow-x-auto pb-2 xl:grid-flow-row xl:grid-cols-4 xl:overflow-visible xl:pb-0">
2942
+ {KANBAN_COLUMNS.map((column) => (
2943
+ <DroppableColumn key={column.id} columnId={column.id}>
2944
+ {(isOver) => (
2945
+ <div
2946
+ className={[
2947
+ 'flex min-h-[32rem] flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
2948
+ getColumnClassName(column.id),
2949
+ isOver
2950
+ ? 'border-primary shadow-lg ring-2 ring-primary/15'
2951
+ : 'border-border',
2952
+ ].join(' ')}
2953
+ >
2954
+ <div className="mb-3 flex items-center justify-between gap-3 rounded-2xl border bg-background/85 p-3 shadow-xs">
2955
+ <div className="min-w-0">
2956
+ <div className="flex items-center gap-2 text-sm font-semibold">
2957
+ <span
2958
+ className={[
2959
+ 'size-2.5 rounded-full',
2960
+ getColumnDotClassName(column.id),
2961
+ ].join(' ')}
2962
+ />
2963
+ {column.label}
2964
+ </div>
2965
+ <div className="mt-1 text-xs text-muted-foreground">
2966
+ {filteredTaskColumns[column.id].length}/
2967
+ {taskColumns[column.id].length} {t('kanban.items')}
2968
+ </div>
2969
+ </div>
2970
+ <div className="flex items-center gap-1">
2971
+ <span className="rounded-full border bg-background px-2 py-0.5 text-xs font-medium text-muted-foreground">
2972
+ {filteredTaskColumns[column.id].length}
2973
+ </span>
2974
+ {!isLimitedView ? (
2975
+ <button
2976
+ type="button"
2977
+ className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
2978
+ onClick={() => {
2979
+ setInlineCreateColumn(column.id);
2980
+ setInlineCreateName('');
2981
+ }}
2982
+ >
2983
+ <Plus className="size-3.5" />
2984
+ </button>
2985
+ ) : null}
2986
+ </div>
2987
+ </div>
2988
+
2989
+ <div className="flex flex-1 flex-col gap-2">
2990
+ <AnimatePresence initial={false}>
2991
+ {filteredTaskColumns[column.id].map((task) => {
2992
+ const tags = getTaskTags(task);
2993
+ const comments = getTaskCommentCount(task);
2994
+ const attachments = getTaskAttachmentCount(task);
2995
+ return (
2996
+ <DraggableTaskCard
2997
+ key={task.id}
2998
+ task={task}
2999
+ disabled={false}
3000
+ >
3001
+ {(isDragging) => (
3002
+ <motion.div
3003
+ initial={{ opacity: 0, scale: 0.96 }}
3004
+ animate={{ opacity: 1, scale: 1 }}
3005
+ exit={{ opacity: 0, scale: 0.95, y: -4 }}
3006
+ transition={{ duration: 0.18 }}
3007
+ role="button"
3008
+ tabIndex={0}
3009
+ className={[
3010
+ 'group w-full cursor-pointer rounded-2xl border bg-card p-3 text-left shadow-xs transition',
3011
+ isDragging
3012
+ ? 'opacity-0'
3013
+ : 'hover:border-primary/40 hover:shadow-lg',
3014
+ ].join(' ')}
3015
+ onClick={() =>
3016
+ setSelectedTask({
3017
+ ...task,
3018
+ projectName: project?.name,
3019
+ projectCode: project?.code,
3020
+ })
3021
+ }
3022
+ onKeyDown={(event) => {
3023
+ if (
3024
+ event.key === 'Enter' ||
3025
+ event.key === ' '
3026
+ ) {
3027
+ event.preventDefault();
3028
+ setSelectedTask({
3029
+ ...task,
3030
+ projectName: project?.name,
3031
+ projectCode: project?.code,
3032
+ });
3033
+ }
3034
+ }}
3035
+ >
3036
+ <div className="mb-3 flex items-start justify-between gap-2">
3037
+ <div className="min-w-0 space-y-1">
3038
+ <p className="line-clamp-2 text-sm font-semibold leading-snug">
3039
+ {task.name}
3040
+ </p>
3041
+ {task.description ? (
3042
+ <p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
3043
+ {task.description.replace(
3044
+ /<[^>]*>/g,
3045
+ ''
3046
+ )}
3047
+ </p>
3048
+ ) : null}
3049
+ </div>
3050
+ <div className="flex items-start gap-2">
3051
+ <span
3052
+ className={[
3053
+ 'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
3054
+ getPriorityClassName(task.priority),
3055
+ ].join(' ')}
3056
+ >
3057
+ {getTaskPriorityLabel(task.priority)}
3058
+ </span>
3059
+ <Button
3060
+ type="button"
3061
+ variant="ghost"
3062
+ size="icon"
3063
+ className="size-7 shrink-0 rounded-full opacity-0 transition group-hover:opacity-100"
3064
+ onPointerDown={(event) =>
3065
+ event.stopPropagation()
3066
+ }
3067
+ onClick={(event) => {
3068
+ event.stopPropagation();
3069
+ openEditTaskForm(task);
3070
+ }}
3071
+ >
3072
+ <Pencil className="size-3.5" />
3073
+ </Button>
3074
+ </div>
3075
+ </div>
3076
+
3077
+ {tags.length > 0 ? (
3078
+ <div className="mb-3 flex flex-wrap gap-1">
3079
+ {tags.slice(0, 4).map((tag) => (
3080
+ <span
3081
+ key={`${task.id}-${tag}`}
3082
+ className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
3083
+ >
3084
+ {tag}
3085
+ </span>
3086
+ ))}
3087
+ {tags.length > 4 ? (
3088
+ <span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
3089
+ +{tags.length - 4}
3090
+ </span>
3091
+ ) : null}
3092
+ </div>
3093
+ ) : null}
3094
+
3095
+ <div className="grid grid-cols-2 gap-2 text-xs">
3096
+ <div
3097
+ className={[
3098
+ 'rounded-xl border bg-muted/20 px-2 py-1.5',
3099
+ isPastDue(task.dueDate) &&
3100
+ task.status !== 'done'
3101
+ ? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300'
3102
+ : 'text-muted-foreground',
3103
+ ].join(' ')}
3104
+ >
3105
+ <span className="flex items-center gap-1">
3106
+ <AlarmClock className="size-3.5" />
3107
+ {formatDate(
3108
+ task.dueDate,
3109
+ getSettingValue,
3110
+ currentLocaleCode
3111
+ )}
3112
+ </span>
3113
+ </div>
3114
+ <div className="rounded-xl border bg-muted/20 px-2 py-1.5 text-muted-foreground">
3115
+ <span className="flex items-center gap-1">
3116
+ <Timer className="size-3.5" />
3117
+ {task.estimateHours != null
3118
+ ? `${task.estimateHours}h`
3119
+ : t('kanban.noEstimate')}
3120
+ </span>
3121
+ </div>
3122
+ </div>
3123
+
3124
+ <div className="mt-3 space-y-1.5">
3125
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
3126
+ <span>{t('kanban.progress')}</span>
3127
+ <span>
3128
+ {getTaskProgress(task.status)}%
3129
+ </span>
3130
+ </div>
3131
+ <Progress
3132
+ value={getTaskProgress(task.status)}
3133
+ className="h-1.5"
3134
+ />
3135
+ </div>
3136
+
3137
+ <div className="mt-3 flex items-center justify-between gap-3 border-t pt-3">
3138
+ <div className="flex min-w-0 items-center gap-2">
3139
+ <div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
3140
+ {(() => {
3141
+ const photoUrl = getUserPhotoUrl(
3142
+ task.assigneeUserPhotoId
3143
+ );
3144
+ const avatarUrl =
3145
+ task.assigneePersonAvatarId
3146
+ ? getPersonAvatarUrl(
3147
+ task.assigneePersonAvatarId
3148
+ )
3149
+ : null;
3150
+ const imgSrc =
3151
+ photoUrl ?? avatarUrl;
3152
+ return imgSrc ? (
3153
+ // eslint-disable-next-line @next/next/no-img-element
3154
+ <img
3155
+ src={imgSrc}
3156
+ alt={
3157
+ task.assigneeName ||
3158
+ commonT('labels.notAssigned')
3159
+ }
3160
+ className="size-full object-cover"
3161
+ />
3162
+ ) : (
3163
+ getInitials(task.assigneeName)
3164
+ );
3165
+ })()}
3166
+ </div>
3167
+ <span className="truncate text-[11px] text-muted-foreground">
3168
+ {task.assigneeName ||
3169
+ commonT('labels.notAssigned')}
3170
+ </span>
3171
+ </div>
3172
+ <div className="flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
3173
+ <span className="inline-flex items-center gap-1">
3174
+ <MessageSquare className="size-3.5" />
3175
+ {comments}
3176
+ </span>
3177
+ <span className="inline-flex items-center gap-1">
3178
+ <Paperclip className="size-3.5" />
3179
+ {attachments}
3180
+ </span>
3181
+ </div>
3182
+ </div>
3183
+ </motion.div>
3184
+ )}
3185
+ </DraggableTaskCard>
3186
+ );
3187
+ })}
3188
+ </AnimatePresence>
3189
+ {filteredTaskColumns[column.id].length === 0 ? (
3190
+ <div className="rounded-2xl border border-dashed bg-background/70 p-4 text-center text-xs text-muted-foreground">
3191
+ {boardSearch || boardPriorityFilter !== 'all'
3192
+ ? t('kanban.noFilteredTasks')
3193
+ : t('kanban.emptyColumn')}
3194
+ </div>
3195
+ ) : null}
3196
+ {!isLimitedView && inlineCreateColumn === column.id ? (
3197
+ <div className="space-y-1.5 rounded-2xl border bg-card p-2 shadow-sm">
3198
+ <Input
3199
+ autoFocus
3200
+ placeholder={t('taskForm.namePlaceholder')}
3201
+ value={inlineCreateName}
3202
+ onChange={(e) =>
3203
+ setInlineCreateName(e.target.value)
3204
+ }
3205
+ onKeyDown={(e) => {
3206
+ if (e.key === 'Enter') {
3207
+ e.preventDefault();
3208
+ void handleInlineCreateTask(
3209
+ column.id,
3210
+ inlineCreateName
3211
+ );
3212
+ } else if (e.key === 'Escape') {
3213
+ setInlineCreateColumn(null);
3214
+ setInlineCreateName('');
3215
+ }
3216
+ }}
3217
+ onBlur={() => {
3218
+ if (!inlineCreateName.trim()) {
3219
+ setInlineCreateColumn(null);
3220
+ setInlineCreateName('');
3221
+ }
3222
+ }}
3223
+ disabled={inlineCreateLoading}
3224
+ className="h-8 text-sm"
3225
+ />
3226
+ <div className="flex gap-1">
3227
+ <Button
3228
+ type="button"
3229
+ size="sm"
3230
+ className="h-7 px-2 text-xs"
3231
+ disabled={
3232
+ !inlineCreateName.trim() ||
3233
+ inlineCreateLoading
3234
+ }
3235
+ onMouseDown={(e) => e.preventDefault()}
3236
+ onClick={() =>
3237
+ void handleInlineCreateTask(
3238
+ column.id,
3239
+ inlineCreateName
3240
+ )
3241
+ }
3242
+ >
3243
+ {t('taskForm.titleNew')}
3244
+ </Button>
3245
+ <Button
3246
+ type="button"
3247
+ variant="ghost"
3248
+ size="sm"
3249
+ className="h-7 px-2 text-xs"
3250
+ onMouseDown={(e) => e.preventDefault()}
3251
+ onClick={() => {
3252
+ setInlineCreateColumn(null);
3253
+ setInlineCreateName('');
3254
+ }}
3255
+ >
3256
+ {commonT('actions.cancel')}
3257
+ </Button>
3258
+ </div>
3259
+ </div>
3260
+ ) : !isLimitedView ? (
3261
+ <button
3262
+ type="button"
3263
+ className="mt-auto flex w-full cursor-pointer items-center justify-center gap-1 rounded-2xl border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground transition hover:border-primary/40 hover:bg-primary/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
3264
+ onClick={() => {
3265
+ setInlineCreateColumn(column.id);
3266
+ setInlineCreateName('');
3267
+ }}
3268
+ >
3269
+ <Plus className="size-3" />
3270
+ {t('taskForm.titleNew')}
3271
+ </button>
3272
+ ) : null}
3273
+ </div>
3274
+ </div>
3275
+ )}
3276
+ </DroppableColumn>
3277
+ ))}
3278
+ </div>
3279
+ {/* Scroll fade overlay — visible only on mobile/tablet */}
3280
+ <div
3281
+ aria-hidden
3282
+ className="pointer-events-none absolute inset-y-0 right-0 w-16 bg-linear-to-l from-background/80 to-transparent xl:hidden"
3283
+ />
3284
+ </div>
3285
+ {/* DragOverlay renders the floating card following the pointer */}
3286
+ <DragOverlay dropAnimation={{ duration: 160, easing: 'ease' }}>
3287
+ {activeDragTask
3288
+ ? (() => {
3289
+ const overlayTask = activeDragTask;
3290
+ const overlayTags = getTaskTags(overlayTask);
3291
+ const overlayComments = getTaskCommentCount(overlayTask);
3292
+ const overlayAttachments = getTaskAttachmentCount(overlayTask);
3293
+ return (
3294
+ <div className="w-76 cursor-grabbing rounded-2xl border border-primary/60 bg-card p-3 shadow-2xl ring-2 ring-primary/20 opacity-95">
3295
+ <div className="mb-3 flex items-start justify-between gap-2">
3296
+ <div className="min-w-0 space-y-1">
3297
+ <p className="line-clamp-2 text-sm font-semibold leading-snug">
3298
+ {overlayTask.name}
3299
+ </p>
3300
+ {overlayTask.description ? (
3301
+ <p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
3302
+ {overlayTask.description.replace(/<[^>]*>/g, '')}
3303
+ </p>
3304
+ ) : null}
3305
+ </div>
3306
+ <span
3307
+ className={[
3308
+ 'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
3309
+ getPriorityClassName(overlayTask.priority),
3310
+ ].join(' ')}
3311
+ >
3312
+ {getTaskPriorityLabel(overlayTask.priority)}
3313
+ </span>
3314
+ </div>
3315
+
3316
+ {overlayTags.length > 0 ? (
3317
+ <div className="mb-3 flex flex-wrap gap-1">
3318
+ {overlayTags.slice(0, 4).map((tag) => (
3319
+ <span
3320
+ key={tag}
3321
+ className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
3322
+ >
3323
+ {tag}
3324
+ </span>
3325
+ ))}
3326
+ {overlayTags.length > 4 ? (
3327
+ <span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
3328
+ +{overlayTags.length - 4}
3329
+ </span>
3330
+ ) : null}
3331
+ </div>
3332
+ ) : null}
3333
+
3334
+ <div className="grid grid-cols-2 gap-2 text-xs">
3335
+ <div
3336
+ className={[
3337
+ 'rounded-xl border bg-muted/20 px-2 py-1.5',
3338
+ isPastDue(overlayTask.dueDate) &&
3339
+ overlayTask.status !== 'done'
3340
+ ? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300'
3341
+ : 'text-muted-foreground',
3342
+ ].join(' ')}
3343
+ >
3344
+ <span className="flex items-center gap-1">
3345
+ <AlarmClock className="size-3.5" />
3346
+ {formatDate(
3347
+ overlayTask.dueDate,
3348
+ getSettingValue,
3349
+ currentLocaleCode
3350
+ )}
3351
+ </span>
3352
+ </div>
3353
+ <div className="rounded-xl border bg-muted/20 px-2 py-1.5 text-muted-foreground">
3354
+ <span className="flex items-center gap-1">
3355
+ <Timer className="size-3.5" />
3356
+ {overlayTask.estimateHours != null
3357
+ ? `${overlayTask.estimateHours}h`
3358
+ : t('kanban.noEstimate')}
3359
+ </span>
3360
+ </div>
3361
+ </div>
3362
+
3363
+ <div className="mt-3 space-y-1.5">
3364
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
3365
+ <span>{t('kanban.progress')}</span>
3366
+ <span>{getTaskProgress(overlayTask.status)}%</span>
3367
+ </div>
3368
+ <Progress
3369
+ value={getTaskProgress(overlayTask.status)}
3370
+ className="h-1.5"
3371
+ />
3372
+ </div>
3373
+
3374
+ <div className="mt-3 flex items-center justify-between gap-3 border-t pt-3">
3375
+ <div className="flex min-w-0 items-center gap-2">
3376
+ <div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
3377
+ {(() => {
3378
+ const photoUrl = getUserPhotoUrl(
3379
+ overlayTask.assigneeUserPhotoId
3380
+ );
3381
+ const avatarUrl =
3382
+ overlayTask.assigneePersonAvatarId
3383
+ ? getPersonAvatarUrl(
3384
+ overlayTask.assigneePersonAvatarId
3385
+ )
3386
+ : null;
3387
+ const imgSrc = photoUrl ?? avatarUrl;
3388
+ return imgSrc ? (
3389
+ // eslint-disable-next-line @next/next/no-img-element
3390
+ <img
3391
+ src={imgSrc}
3392
+ alt={
3393
+ overlayTask.assigneeName ||
3394
+ commonT('labels.notAssigned')
3395
+ }
3396
+ className="size-full object-cover"
3397
+ />
3398
+ ) : (
3399
+ getInitials(overlayTask.assigneeName)
3400
+ );
3401
+ })()}
3402
+ </div>
3403
+ <span className="truncate text-[11px] text-muted-foreground">
3404
+ {overlayTask.assigneeName ||
3405
+ commonT('labels.notAssigned')}
3406
+ </span>
3407
+ </div>
3408
+ <div className="flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
3409
+ <span className="inline-flex items-center gap-1">
3410
+ <MessageSquare className="size-3.5" />
3411
+ {overlayComments}
3412
+ </span>
3413
+ <span className="inline-flex items-center gap-1">
3414
+ <Paperclip className="size-3.5" />
3415
+ {overlayAttachments}
3416
+ </span>
3417
+ </div>
3418
+ </div>
3419
+ </div>
3420
+ );
3421
+ })()
3422
+ : null}
3423
+ </DragOverlay>
3424
+ </DndContext>
3425
+ </SectionCard>
3426
+
3427
+ <SectionCard
3428
+ title={t('sections.timeline')}
3429
+ description={t('sections.timelineDescription')}
3430
+ className="rounded-3xl border bg-card p-4 shadow-sm"
3431
+ actions={
3432
+ <div className="flex flex-wrap items-center gap-2">
3433
+ <Select
3434
+ value={timelineTypeFilter}
3435
+ onValueChange={(value) => {
3436
+ setTimelineTypeFilter(value as typeof timelineTypeFilter);
3437
+ setTimelineVisibleCount(8);
3438
+ }}
3439
+ >
3440
+ <SelectTrigger className="h-9 w-44 bg-background">
3441
+ <SelectValue />
3442
+ </SelectTrigger>
3443
+ <SelectContent>
3444
+ <SelectItem value="all">{t('timeline.filters.all')}</SelectItem>
3445
+ <SelectItem value="task">
3446
+ {t('timeline.filters.task')}
3447
+ </SelectItem>
3448
+ <SelectItem value="status">
3449
+ {t('timeline.filters.status')}
3450
+ </SelectItem>
3451
+ <SelectItem value="timesheet">
3452
+ {t('timeline.filters.timesheet')}
3453
+ </SelectItem>
3454
+ <SelectItem value="approval">
3455
+ {t('timeline.filters.approval')}
3456
+ </SelectItem>
3457
+ <SelectItem value="comment">
3458
+ {t('timeline.filters.comment')}
3459
+ </SelectItem>
3460
+ </SelectContent>
3461
+ </Select>
3462
+ </div>
3463
+ }
3464
+ >
3465
+ <div className="rounded-3xl border bg-linear-to-b from-muted/30 to-background p-4">
3466
+ {groupedTimelineEvents.length > 0 ? (
3467
+ <div className="space-y-6">
3468
+ {groupedTimelineEvents.map((group) => (
3469
+ <div key={group.dayKey} className="space-y-3">
3470
+ <div className="sticky top-0 z-10 w-fit rounded-full border bg-background px-3 py-1 text-xs font-medium text-muted-foreground shadow-xs">
3471
+ {formatDate(
3472
+ group.dayKey,
3473
+ getSettingValue,
3474
+ currentLocaleCode
3475
+ )}
3476
+ </div>
3477
+ <div className="space-y-0">
3478
+ {group.events.map((event, index) => {
3479
+ const Icon = event.icon;
3480
+ return (
3481
+ <motion.div
3482
+ key={event.id}
3483
+ initial={{ opacity: 0, y: 8 }}
3484
+ animate={{ opacity: 1, y: 0 }}
3485
+ transition={{ duration: 0.18 }}
3486
+ className="grid grid-cols-[2rem_1fr] gap-3"
3487
+ >
3488
+ <div className="flex flex-col items-center">
3489
+ <div
3490
+ className={[
3491
+ 'flex size-8 items-center justify-center rounded-full shadow-sm',
3492
+ event.toneClassName,
3493
+ ].join(' ')}
3494
+ >
3495
+ <Icon className="size-4" />
3496
+ </div>
3497
+ {index < group.events.length - 1 ? (
3498
+ <div className="w-px flex-1 bg-border" />
3499
+ ) : null}
3500
+ </div>
3501
+ <div className="pb-5">
3502
+ <div className="rounded-2xl border bg-card p-4 shadow-xs transition hover:shadow-sm">
3503
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
3504
+ <div className="min-w-0">
3505
+ <div className="flex flex-wrap items-center gap-2">
3506
+ <span className="text-sm font-semibold">
3507
+ {event.title}
3508
+ </span>
3509
+ <span className="rounded-full border bg-muted/40 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
3510
+ {t(`timeline.types.${event.type}`)}
3511
+ </span>
3512
+ </div>
3513
+ <p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
3514
+ {event.description}
3515
+ </p>
3516
+ </div>
3517
+ <div className="shrink-0 text-xs text-muted-foreground">
3518
+ {formatRelativeTime(
3519
+ event.timestamp,
3520
+ currentLocaleCode
3521
+ )}
3522
+ </div>
3523
+ </div>
3524
+ <div className="mt-3 flex items-center gap-2 border-t pt-3">
3525
+ <Avatar className="size-7 border bg-muted">
3526
+ <AvatarImage
3527
+ src={
3528
+ getUserPhotoUrl(event.actorUserPhotoId) ||
3529
+ getPersonAvatarUrl(event.actorAvatarId)
3530
+ }
3531
+ alt={
3532
+ event.actorName ||
3533
+ commonT('labels.notAssigned')
3534
+ }
3535
+ />
3536
+ <AvatarFallback className="text-[10px]">
3537
+ {getInitials(event.actorName)}
3538
+ </AvatarFallback>
3539
+ </Avatar>
3540
+ <span className="truncate text-xs text-muted-foreground">
3541
+ {event.actorName ||
3542
+ commonT('labels.notAssigned')}
3543
+ </span>
3544
+ </div>
3545
+ </div>
3546
+ </div>
3547
+ </motion.div>
3548
+ );
3549
+ })}
3550
+ </div>
3551
+ </div>
3552
+ ))}
3553
+ {visibleTimelineEvents.length < filteredTimelineEvents.length ? (
3554
+ <div className="flex justify-center">
3555
+ <Button
3556
+ type="button"
3557
+ variant="outline"
3558
+ size="sm"
3559
+ onClick={() =>
3560
+ setTimelineVisibleCount((current) => current + 8)
3561
+ }
3562
+ >
3563
+ {t('timeline.loadMore')}
3564
+ </Button>
3565
+ </div>
3566
+ ) : null}
3567
+ </div>
3568
+ ) : (
3569
+ <div className="flex min-h-56 flex-col items-center justify-center rounded-2xl border border-dashed bg-background p-6 text-center">
3570
+ <div className="flex size-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
3571
+ <GitCommitHorizontal className="size-6" />
3572
+ </div>
3573
+ <div className="mt-3 text-sm font-medium">
3574
+ {t('timeline.emptyTitle')}
3575
+ </div>
3576
+ <p className="mt-1 max-w-sm text-xs leading-5 text-muted-foreground">
3577
+ {t('timeline.empty')}
3578
+ </p>
3579
+ </div>
3580
+ )}
3581
+ </div>
3582
+ </SectionCard>
3583
+
3584
+ <SectionCard
3585
+ title={t('sections.archivedTasks')}
3586
+ description={t('sections.archivedTasksDescription')}
3587
+ className="rounded-xl border bg-card p-4 shadow-sm"
3588
+ >
3589
+ {archivedTasks.length > 0 ? (
3590
+ <div className="overflow-x-auto rounded-lg border bg-muted/10">
3591
+ <Table>
3592
+ <TableHeader>
3593
+ <TableRow>
3594
+ <TableHead>{commonT('labels.task')}</TableHead>
3595
+ <TableHead>{commonT('labels.status')}</TableHead>
3596
+ <TableHead>{t('taskForm.deadlineLabel')}</TableHead>
3597
+ <TableHead className="text-right">
3598
+ {commonT('labels.actions')}
3599
+ </TableHead>
3600
+ </TableRow>
3601
+ </TableHeader>
3602
+ <TableBody>
3603
+ {archivedTasks.map((task) => (
3604
+ <TableRow
3605
+ key={task.id}
3606
+ className="cursor-pointer hover:bg-muted/30"
3607
+ onClick={() => setSelectedTask(task)}
3608
+ >
3609
+ <TableCell>
3610
+ <div className="min-w-0">
3611
+ <div className="truncate font-medium">{task.name}</div>
3612
+ {task.description ? (
3613
+ <div className="truncate text-xs text-muted-foreground">
3614
+ {task.description}
3615
+ </div>
3616
+ ) : null}
3617
+ </div>
3618
+ </TableCell>
3619
+ <TableCell>
3620
+ <StatusBadge
3621
+ label={
3622
+ KANBAN_COLUMNS.find(
3623
+ (column) => column.id === task.status
3624
+ )?.label ?? formatEnumLabel(task.status)
3625
+ }
3626
+ className={getStatusBadgeClass(task.status)}
3627
+ />
3628
+ </TableCell>
3629
+ <TableCell>
3630
+ {formatDate(
3631
+ task.dueDate,
3632
+ getSettingValue,
3633
+ currentLocaleCode
3634
+ )}
3635
+ </TableCell>
3636
+ <TableCell>
3637
+ <div className="flex justify-end gap-2">
3638
+ <Button
3639
+ variant="outline"
3640
+ size="sm"
3641
+ className="gap-2"
3642
+ disabled={restoringTaskId === task.id}
3643
+ onClick={(event) => {
3644
+ event.stopPropagation();
3645
+ void handleRestoreTask(task.id);
3646
+ }}
3647
+ >
3648
+ {restoringTaskId === task.id ? (
3649
+ <Loader2 className="size-4 animate-spin" />
3650
+ ) : (
3651
+ <ArchiveRestore className="size-4" />
3652
+ )}
3653
+ {commonT('actions.unarchive')}
3654
+ </Button>
3655
+ <Button
3656
+ variant="destructive"
3657
+ size="sm"
3658
+ className="gap-2"
3659
+ onClick={(event) => {
3660
+ event.stopPropagation();
3661
+ setDeletePromptTask(task);
3662
+ }}
3663
+ >
3664
+ <Trash2 className="size-4" />
3665
+ {commonT('actions.delete')}
3666
+ </Button>
3667
+ </div>
3668
+ </TableCell>
3669
+ </TableRow>
3670
+ ))}
3671
+ </TableBody>
3672
+ </Table>
3673
+ </div>
3674
+ ) : (
3675
+ <ChartEmptyState
3676
+ icon={Archive}
3677
+ title={commonT('states.emptyTitle')}
3678
+ description={t('emptyArchivedDescription')}
3679
+ />
3680
+ )}
3681
+ </SectionCard>
3682
+
3683
+ <div className="grid gap-4 xl:grid-cols-12">
3684
+ <SectionCard
3685
+ title={t('sections.team')}
3686
+ description={t('sections.teamDescription')}
3687
+ className={[
3688
+ 'rounded-2xl border bg-card p-4 shadow-sm',
3689
+ isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
3690
+ ].join(' ')}
3691
+ >
3692
+ {project.assignments.length > 0 ? (
3693
+ <div className="space-y-4">
3694
+ <div className="grid gap-3 sm:grid-cols-3">
3695
+ <div className="rounded-2xl border bg-emerald-500/10 p-3">
3696
+ <div className="text-xs text-muted-foreground">
3697
+ {t('teamPanel.available')}
3698
+ </div>
3699
+ <div className="mt-1 text-2xl font-semibold text-emerald-700 dark:text-emerald-300">
3700
+ {availableAssignments}
3701
+ </div>
3702
+ </div>
3703
+ <div className="rounded-2xl border bg-amber-500/10 p-3">
3704
+ <div className="text-xs text-muted-foreground">
3705
+ {t('teamPanel.highAllocation')}
3706
+ </div>
3707
+ <div className="mt-1 text-2xl font-semibold text-amber-700 dark:text-amber-300">
3708
+ {highAllocationAssignments}
3709
+ </div>
3710
+ </div>
3711
+ <div className="rounded-2xl border bg-rose-500/10 p-3">
3712
+ <div className="text-xs text-muted-foreground">
3713
+ {t('teamPanel.overload')}
3714
+ </div>
3715
+ <div className="mt-1 text-2xl font-semibold text-rose-700 dark:text-rose-300">
3716
+ {overloadedAssignments}
3717
+ </div>
3718
+ </div>
3719
+ </div>
3720
+
3721
+ <div className="grid gap-3 md:grid-cols-2">
3722
+ {project.assignments.map((assignment) => {
3723
+ const allocationValue =
3724
+ typeof assignment.allocationPercent === 'number'
3725
+ ? Math.round(assignment.allocationPercent)
3726
+ : 0;
3727
+ const allocation = clampPercent(allocationValue);
3728
+ const weeklyHours = assignment.weeklyHours ?? 0;
3729
+ const usedHours =
3730
+ weeklyHours > 0
3731
+ ? (weeklyHours * Math.max(allocationValue, 0)) / 100
3732
+ : 0;
3733
+ const availablePercent = Math.max(0, 100 - allocationValue);
3734
+ const availabilityHours =
3735
+ weeklyHours > 0 ? Math.max(0, weeklyHours - usedHours) : 0;
3736
+ const tone = getAllocationTone(allocationValue);
3737
+ const ToneIcon = tone.icon;
3738
+
3739
+ return (
3740
+ <motion.div
3741
+ key={assignment.id}
3742
+ whileHover={{ y: -2 }}
3743
+ className={[
3744
+ 'overflow-hidden rounded-2xl border bg-background shadow-xs transition hover:shadow-md',
3745
+ tone.border,
3746
+ ].join(' ')}
3747
+ >
3748
+ <div className="border-b bg-linear-to-br from-muted/50 to-background p-4">
3749
+ <div className="flex items-start justify-between gap-3">
3750
+ <div className="flex min-w-0 items-center gap-3">
3751
+ <Avatar className="size-12 border bg-muted">
3752
+ <AvatarImage
3753
+ src={
3754
+ getUserPhotoUrl(assignment.userPhotoId) ||
3755
+ getPersonAvatarUrl(assignment.personAvatarId)
3756
+ }
3757
+ alt={assignment.collaboratorName}
3758
+ />
3759
+ <AvatarFallback className="text-xs font-semibold">
3760
+ {getInitials(assignment.collaboratorName)}
3761
+ </AvatarFallback>
3762
+ </Avatar>
3763
+ <div className="min-w-0">
3764
+ <div className="truncate text-sm font-semibold">
3765
+ {assignment.collaboratorName}
3766
+ </div>
3767
+ <div className="truncate text-xs text-muted-foreground">
3768
+ {assignment.roleLabel ||
3769
+ commonT('labels.notAssigned')}
3770
+ </div>
3771
+ </div>
3772
+ </div>
3773
+ <div className="flex shrink-0 flex-col items-end gap-2">
3774
+ <StatusBadge
3775
+ label={formatEnumLabel(assignment.status)}
3776
+ className={getStatusBadgeClass(assignment.status)}
3777
+ />
3778
+ <span
3779
+ className={[
3780
+ 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-semibold',
3781
+ tone.border,
3782
+ tone.bg,
3783
+ tone.text,
3784
+ ].join(' ')}
3785
+ >
3786
+ <ToneIcon className="size-3" />
3787
+ {t(`teamPanel.status.${tone.labelKey}`)}
3788
+ </span>
3789
+ </div>
3790
+ </div>
3791
+ </div>
3792
+
3793
+ <div className="space-y-4 p-4">
3794
+ <div className="space-y-2">
3795
+ <div className="flex items-center justify-between text-xs">
3796
+ <span className="text-muted-foreground">
3797
+ {commonT('labels.allocationPercent')}
3798
+ </span>
3799
+ <span
3800
+ className={['font-semibold', tone.text].join(' ')}
3801
+ >
3802
+ {formatPercent(assignment.allocationPercent)}
3803
+ </span>
3804
+ </div>
3805
+ <Progress
3806
+ value={allocation}
3807
+ className={['h-2.5', tone.progress].join(' ')}
3808
+ />
3809
+ {allocationValue > 100 ? (
3810
+ <div className="flex items-center gap-1 text-xs text-rose-700 dark:text-rose-300">
3811
+ <AlertTriangle className="size-3.5" />
3812
+ {t('teamPanel.overloadWarning', {
3813
+ value: allocationValue - 100,
3814
+ })}
3815
+ </div>
3816
+ ) : null}
3817
+ </div>
3818
+
3819
+ <div className="grid grid-cols-2 gap-3 text-xs xl:grid-cols-4">
3820
+ <div className="rounded-xl border bg-muted/20 p-2">
3821
+ <div className="text-muted-foreground">
3822
+ {commonT('labels.weeklyCapacity')}
3823
+ </div>
3824
+ <div className="mt-1 font-semibold">
3825
+ {weeklyHours
3826
+ ? formatHours(weeklyHours)
3827
+ : commonT('labels.notAvailable')}
3828
+ </div>
3829
+ </div>
3830
+ <div className="rounded-xl border bg-muted/20 p-2">
3831
+ <div className="text-muted-foreground">
3832
+ {t('teamPanel.usedHours')}
3833
+ </div>
3834
+ <div className="mt-1 font-semibold">
3835
+ {weeklyHours
3836
+ ? formatHours(usedHours)
3837
+ : commonT('labels.notAvailable')}
3838
+ </div>
3839
+ </div>
3840
+ <div className="rounded-xl border bg-muted/20 p-2">
3841
+ <div className="text-muted-foreground">
3842
+ {t('teamPanel.availability')}
3843
+ </div>
3844
+ <div className="mt-1 font-semibold">
3845
+ {weeklyHours
3846
+ ? formatHours(availabilityHours)
3847
+ : `${clampPercent(availablePercent)}%`}
3848
+ </div>
3849
+ </div>
3850
+ <div className="rounded-xl border bg-muted/20 p-2">
3851
+ <div className="text-muted-foreground">
3852
+ {commonT('labels.timeline')}
3853
+ </div>
3854
+ <div className="mt-1 truncate font-semibold">
3855
+ {formatDateRange(
3856
+ assignment.startDate,
3857
+ assignment.endDate,
3858
+ getSettingValue,
3859
+ currentLocaleCode
3860
+ )}
3861
+ </div>
3862
+ </div>
3863
+ </div>
3864
+ </div>
3865
+ </motion.div>
3866
+ );
3867
+ })}
3868
+ </div>
3869
+ </div>
3870
+ ) : (
3871
+ <ChartEmptyState
3872
+ icon={Users}
3873
+ title={commonT('states.emptyTitle')}
3874
+ description={t('noAssignments')}
3875
+ />
3876
+ )}
3877
+ </SectionCard>
3878
+
3879
+ {!isLimitedView ? (
3880
+ <SectionCard
3881
+ title={t('sections.indicators')}
3882
+ description={t('sections.indicatorsDescription')}
3883
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-4"
3884
+ >
3885
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
3886
+ {[
3887
+ {
3888
+ icon: Users,
3889
+ label: t('indicators.activeAssignments'),
3890
+ value: project.operationalIndicators.activeAssignments,
3891
+ tone: 'text-sky-700 dark:text-sky-300',
3892
+ bg: 'bg-sky-500/10',
3893
+ },
3894
+ {
3895
+ icon: CheckCircle2,
3896
+ label: t('indicators.completedAssignments'),
3897
+ value: project.operationalIndicators.completedAssignments,
3898
+ tone: 'text-emerald-700 dark:text-emerald-300',
3899
+ bg: 'bg-emerald-500/10',
3900
+ },
3901
+ {
3902
+ icon: Gauge,
3903
+ label: t('indicators.averageAllocation'),
3904
+ value: formatPercent(
3905
+ project.operationalIndicators.averageAllocation
3906
+ ),
3907
+ tone:
3908
+ averageAllocation > 100
3909
+ ? 'text-rose-700 dark:text-rose-300'
3910
+ : averageAllocation > 85
3911
+ ? 'text-amber-700 dark:text-amber-300'
3912
+ : 'text-emerald-700 dark:text-emerald-300',
3913
+ bg:
3914
+ averageAllocation > 100
3915
+ ? 'bg-rose-500/10'
3916
+ : averageAllocation > 85
3917
+ ? 'bg-amber-500/10'
3918
+ : 'bg-emerald-500/10',
3919
+ },
3920
+ {
3921
+ icon: Timer,
3922
+ label: t('indicators.totalWeeklyHours'),
3923
+ value: formatHours(
3924
+ project.operationalIndicators.totalWeeklyHours
3925
+ ),
3926
+ tone: 'text-violet-700 dark:text-violet-300',
3927
+ bg: 'bg-violet-500/10',
3928
+ },
3929
+ {
3930
+ icon: ClipboardList,
3931
+ label: t('cards.timesheets'),
3932
+ value: project.timesheetSummary.totalTimesheets,
3933
+ tone: 'text-foreground',
3934
+ bg: 'bg-muted/40',
3935
+ },
3936
+ {
3937
+ icon: AlarmClock,
3938
+ label: commonT('labels.pending'),
3939
+ value: project.timesheetSummary.pendingTimesheets,
3940
+ tone:
3941
+ project.timesheetSummary.pendingTimesheets > 0
3942
+ ? 'text-amber-700 dark:text-amber-300'
3943
+ : 'text-foreground',
3944
+ bg:
3945
+ project.timesheetSummary.pendingTimesheets > 0
3946
+ ? 'bg-amber-500/10'
3947
+ : 'bg-muted/40',
3948
+ },
3949
+ {
3950
+ icon: BarChart2,
3951
+ label: t('cards.loggedHours'),
3952
+ value: formatHours(project.timesheetSummary.totalHours),
3953
+ tone: 'text-sky-700 dark:text-sky-300',
3954
+ bg: 'bg-sky-500/10',
3955
+ },
3956
+ ].map(({ icon: Icon, label, value, tone, bg }) => (
3957
+ <div
3958
+ key={label}
3959
+ className="flex items-center gap-3 rounded-xl border bg-card p-3 transition-shadow hover:shadow-sm"
3960
+ >
3961
+ <div
3962
+ className={[
3963
+ 'flex size-9 shrink-0 items-center justify-center rounded-xl',
3964
+ bg,
3965
+ ].join(' ')}
3966
+ >
3967
+ <Icon className={['size-4', tone].join(' ')} />
3968
+ </div>
3969
+ <div className="min-w-0 flex-1">
3970
+ <div className="truncate text-xs text-muted-foreground">
3971
+ {label}
3972
+ </div>
3973
+ <div
3974
+ className={[
3975
+ 'mt-0.5 text-sm font-semibold tabular-nums',
3976
+ tone,
3977
+ ].join(' ')}
3978
+ >
3979
+ {value}
3980
+ </div>
3981
+ </div>
3982
+ </div>
3983
+ ))}
3984
+ </div>
3985
+ </SectionCard>
3986
+ ) : null}
3987
+ </div>
3988
+
3989
+ <SectionCard
3990
+ title={t('sections.costs')}
3991
+ description={t('sections.costsDescription')}
3992
+ className="rounded-2xl border bg-card p-4 shadow-sm"
3993
+ >
3994
+ <ProjectCostsSection projectId={projectId} />
3995
+ </SectionCard>
3996
+
3997
+ <TaskDetailSheet
3998
+ task={selectedTask}
3999
+ open={selectedTask !== null}
4000
+ onOpenChange={(open) => {
4001
+ if (!open) {
4002
+ setSelectedTask(null);
4003
+ }
4004
+ }}
4005
+ statusLabel={(status) =>
4006
+ KANBAN_COLUMNS.find((column) => column.id === status)?.label ?? status
4007
+ }
4008
+ footer={
4009
+ selectedTask && !isLimitedView ? (
4010
+ <div className="grid grid-cols-2 gap-3">
4011
+ {archivedTasks.some((task) => task.id === selectedTask.id) ? (
4012
+ <>
4013
+ <Button
4014
+ variant="outline"
4015
+ size="sm"
4016
+ className="h-10 gap-2"
4017
+ disabled={restoringTaskId === selectedTask.id}
4018
+ onClick={() => void handleRestoreTask(selectedTask.id)}
4019
+ >
4020
+ {restoringTaskId === selectedTask.id ? (
4021
+ <Loader2 className="size-3.5 animate-spin" />
4022
+ ) : (
4023
+ <ArchiveRestore className="size-3.5" />
4024
+ )}
4025
+ {commonT('actions.unarchive')}
4026
+ </Button>
4027
+ <Button
4028
+ variant="destructive"
4029
+ size="sm"
4030
+ className="h-10 gap-2"
4031
+ onClick={() => setDeletePromptTask(selectedTask)}
4032
+ >
4033
+ <Trash2 className="size-3.5" />
4034
+ {commonT('actions.delete')}
4035
+ </Button>
4036
+ </>
4037
+ ) : (
4038
+ <Button
4039
+ variant="outline"
4040
+ size="sm"
4041
+ className="col-span-2 h-10 gap-2"
4042
+ disabled={archivingTaskId === selectedTask.id}
4043
+ onClick={() => void handleArchiveTask(selectedTask.id)}
4044
+ >
4045
+ {archivingTaskId === selectedTask.id ? (
4046
+ <Loader2 className="size-3.5 animate-spin" />
4047
+ ) : (
4048
+ <Archive className="size-3.5" />
4049
+ )}
4050
+ {commonT('actions.archive')}
4051
+ </Button>
4052
+ )}
4053
+ </div>
4054
+ ) : null
4055
+ }
4056
+ />
4057
+
4058
+ {!isLimitedView ? (
4059
+ <Sheet
4060
+ open={isEditSheetOpen}
4061
+ onOpenChange={(open) => {
4062
+ if (!open) {
4063
+ closeEditSheet();
4064
+ }
4065
+ }}
4066
+ >
4067
+ <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
4068
+ <SheetHeader>
4069
+ <SheetTitle>{formT('editTitle')}</SheetTitle>
4070
+ <SheetDescription>{formT('description')}</SheetDescription>
4071
+ </SheetHeader>
4072
+
4073
+ <ProjectFormScreen
4074
+ projectId={projectId}
4075
+ onCancel={closeEditSheet}
4076
+ onSaved={async () => {
4077
+ closeEditSheet();
4078
+ await refetch();
4079
+ }}
4080
+ />
4081
+ </SheetContent>
4082
+ </Sheet>
4083
+ ) : null}
4084
+
4085
+ {!isLimitedView ? (
4086
+ <Sheet
4087
+ open={taskFormOpen}
4088
+ onOpenChange={(open) => {
4089
+ if (!open) {
4090
+ setTaskFormOpen(false);
4091
+ setEditingTaskId(null);
4092
+ setTaskFormData(EMPTY_TASK_FORM);
4093
+ }
4094
+ }}
4095
+ >
4096
+ <SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
4097
+ <SheetHeader>
4098
+ <SheetTitle>
4099
+ {editingTaskId
4100
+ ? t('taskForm.titleEdit')
4101
+ : t('taskForm.titleNew')}
4102
+ </SheetTitle>
4103
+ </SheetHeader>
4104
+
4105
+ <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
4106
+ <div className="space-y-1.5">
4107
+ <Label htmlFor="task-name">{t('taskForm.nameLabel')} *</Label>
4108
+ <Input
4109
+ id="task-name"
4110
+ placeholder={t('taskForm.namePlaceholder')}
4111
+ value={taskFormData.name}
4112
+ onChange={(e) =>
4113
+ setTaskFormData((prev) => ({
4114
+ ...prev,
4115
+ name: e.target.value,
4116
+ }))
4117
+ }
4118
+ />
4119
+ </div>
4120
+
4121
+ <div className="space-y-1.5">
4122
+ <Label htmlFor="task-description">
4123
+ {t('taskForm.descriptionLabel')}
4124
+ </Label>
4125
+ <RichTextEditor
4126
+ value={taskFormData.description}
4127
+ onChange={(val) =>
4128
+ setTaskFormData((prev) => ({
4129
+ ...prev,
4130
+ description: val,
4131
+ }))
4132
+ }
4133
+ />
4134
+ </div>
4135
+
4136
+ <div className="grid grid-cols-2 gap-3">
4137
+ <div className="space-y-1.5">
4138
+ <Label>{t('taskForm.priorityLabel')}</Label>
4139
+ <Select
4140
+ value={taskFormData.priority}
4141
+ onValueChange={(v) =>
4142
+ setTaskFormData((prev) => ({
4143
+ ...prev,
4144
+ priority: v as TaskFormState['priority'],
4145
+ }))
4146
+ }
4147
+ >
4148
+ <SelectTrigger className="w-full">
4149
+ <SelectValue />
4150
+ </SelectTrigger>
4151
+ <SelectContent>
4152
+ <SelectItem value="low">
4153
+ {getTaskPriorityLabel('low')}
4154
+ </SelectItem>
4155
+ <SelectItem value="medium">
4156
+ {getTaskPriorityLabel('medium')}
4157
+ </SelectItem>
4158
+ <SelectItem value="high">
4159
+ {getTaskPriorityLabel('high')}
4160
+ </SelectItem>
4161
+ </SelectContent>
4162
+ </Select>
4163
+ </div>
4164
+
4165
+ <div className="space-y-1.5">
4166
+ <Label>{t('taskForm.columnLabel')}</Label>
4167
+ <Select
4168
+ value={taskFormData.status}
4169
+ onValueChange={(v) =>
4170
+ setTaskFormData((prev) => ({
4171
+ ...prev,
4172
+ status: v as BoardColumnId,
4173
+ }))
4174
+ }
4175
+ >
4176
+ <SelectTrigger className="w-full">
4177
+ <SelectValue />
4178
+ </SelectTrigger>
4179
+ <SelectContent>
4180
+ {KANBAN_COLUMNS.map((col) => (
4181
+ <SelectItem key={col.id} value={col.id}>
4182
+ {col.label}
4183
+ </SelectItem>
4184
+ ))}
4185
+ </SelectContent>
4186
+ </Select>
4187
+ </div>
4188
+ </div>
4189
+
4190
+ <div className="space-y-1.5">
4191
+ <Label>Responsável</Label>
4192
+ <Select
4193
+ value={taskFormData.assigneeCollaboratorId}
4194
+ onValueChange={(value) =>
4195
+ setTaskFormData((prev) => ({
4196
+ ...prev,
4197
+ assigneeCollaboratorId: value,
4198
+ }))
4199
+ }
4200
+ >
4201
+ <SelectTrigger className="w-full">
4202
+ <SelectValue placeholder={commonT('labels.notAssigned')} />
4203
+ </SelectTrigger>
4204
+ <SelectContent>
4205
+ <SelectItem value="none">
4206
+ {commonT('labels.notAssigned')}
4207
+ </SelectItem>
4208
+ {taskAssigneeOptions.map((option) => (
4209
+ <SelectItem key={option.id} value={option.id}>
4210
+ {option.label}
4211
+ </SelectItem>
4212
+ ))}
4213
+ </SelectContent>
4214
+ </Select>
4215
+ </div>
4216
+
4217
+ <div className="grid grid-cols-2 gap-3">
4218
+ <div className="space-y-1.5">
4219
+ <Label htmlFor="task-due-date">
4220
+ {t('taskForm.deadlineLabel')}
4221
+ </Label>
4222
+ <Input
4223
+ id="task-due-date"
4224
+ type="date"
4225
+ value={taskFormData.dueDate}
4226
+ onChange={(e) =>
4227
+ setTaskFormData((prev) => ({
4228
+ ...prev,
4229
+ dueDate: e.target.value,
4230
+ }))
4231
+ }
4232
+ />
4233
+ </div>
4234
+
4235
+ <div className="space-y-1.5">
4236
+ <Label htmlFor="task-estimate">
4237
+ {t('taskForm.estimateLabel')}
4238
+ </Label>
4239
+ <Input
4240
+ id="task-estimate"
4241
+ type="number"
4242
+ min="0"
4243
+ step="0.5"
4244
+ placeholder="0"
4245
+ value={taskFormData.estimateHours}
4246
+ onChange={(e) =>
4247
+ setTaskFormData((prev) => ({
4248
+ ...prev,
4249
+ estimateHours: e.target.value,
4250
+ }))
4251
+ }
4252
+ />
4253
+ </div>
4254
+ </div>
4255
+
4256
+ <div className="space-y-1.5">
4257
+ <Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
4258
+ <Input
4259
+ id="task-tags"
4260
+ placeholder={t('taskForm.tagsPlaceholder')}
4261
+ value={taskFormData.tags}
4262
+ onChange={(e) =>
4263
+ setTaskFormData((prev) => ({
4264
+ ...prev,
4265
+ tags: e.target.value,
4266
+ }))
4267
+ }
4268
+ />
4269
+ </div>
4270
+
4271
+ {editingTaskId ? (
4272
+ <div className="space-y-1.5">
4273
+ <Label className="flex items-center gap-1.5">
4274
+ <Paperclip className="size-3.5" />
4275
+ {t('taskForm.attachmentsLabel')}
4276
+ </Label>
4277
+ <TaskFileAttachments taskId={editingTaskId} />
4278
+ </div>
4279
+ ) : null}
4280
+ </div>
4281
+
4282
+ <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
4283
+ <div className="flex gap-2">
4284
+ {editingTaskId ? (
4285
+ <Button
4286
+ type="button"
4287
+ variant="outline"
4288
+ disabled={
4289
+ taskFormLoading || archivingTaskId === editingTaskId
4290
+ }
4291
+ onClick={() => {
4292
+ if (!editingTaskId) return;
4293
+ const id = editingTaskId;
4294
+ setTaskFormOpen(false);
4295
+ setEditingTaskId(null);
4296
+ setTaskFormData(EMPTY_TASK_FORM);
4297
+ void handleArchiveTask(id);
4298
+ }}
4299
+ >
4300
+ {archivingTaskId === editingTaskId ? (
4301
+ <Loader2 className="mr-2 size-4 animate-spin" />
4302
+ ) : (
4303
+ <Archive className="mr-2 size-4" />
4304
+ )}
4305
+ {commonT('actions.archive')}
4306
+ </Button>
4307
+ ) : null}
4308
+ </div>
4309
+ <div className="flex gap-2">
4310
+ <Button
4311
+ variant="outline"
4312
+ onClick={() => {
4313
+ setTaskFormOpen(false);
4314
+ setEditingTaskId(null);
4315
+ setTaskFormData(EMPTY_TASK_FORM);
4316
+ }}
4317
+ disabled={taskFormLoading}
4318
+ >
4319
+ {commonT('actions.cancel')}
4320
+ </Button>
4321
+ <Button
4322
+ onClick={() => void handleTaskFormSubmit()}
4323
+ disabled={taskFormLoading || !taskFormData.name.trim()}
4324
+ >
4325
+ {taskFormLoading
4326
+ ? t('taskForm.saving')
4327
+ : editingTaskId
4328
+ ? commonT('actions.save')
4329
+ : commonT('actions.create')}
4330
+ </Button>
4331
+ </div>
4332
+ </div>
4333
+ </SheetContent>
4334
+ </Sheet>
4335
+ ) : null}
4336
+
4337
+ {!isLimitedView ? (
4338
+ <>
4339
+ <Dialog
4340
+ open={deletePromptTask !== null}
4341
+ onOpenChange={(open) => {
4342
+ if (!open) setDeletePromptTask(null);
4343
+ }}
4344
+ >
4345
+ <DialogContent className="sm:max-w-sm">
4346
+ <DialogHeader>
4347
+ <DialogTitle>{t('dialogs.deleteTitle')}</DialogTitle>
4348
+ </DialogHeader>
4349
+ <p className="text-sm text-muted-foreground">
4350
+ {t('dialogs.deleteDescription')}
4351
+ </p>
4352
+ <DialogFooter className="mt-4">
4353
+ <Button
4354
+ variant="outline"
4355
+ onClick={() => setDeletePromptTask(null)}
4356
+ >
4357
+ {commonT('actions.cancel')}
4358
+ </Button>
4359
+ {deletePromptTask ? (
4360
+ <Button
4361
+ variant="destructive"
4362
+ disabled={deletingTaskId === deletePromptTask.id}
4363
+ onClick={() => void handleDeleteTask(deletePromptTask.id)}
4364
+ >
4365
+ {deletingTaskId === deletePromptTask.id ? (
4366
+ <Loader2 className="mr-2 size-3.5 animate-spin" />
4367
+ ) : null}
4368
+ {commonT('actions.delete')}
4369
+ </Button>
4370
+ ) : null}
4371
+ </DialogFooter>
4372
+ </DialogContent>
4373
+ </Dialog>
4374
+ </>
4375
+ ) : null}
4376
+ </Page>
4377
+ );
4378
+ }