@hed-hog/operations 0.0.321 → 0.0.325

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