@open-mercato/core 0.6.4-develop.4217.1.c9aa050183 → 0.6.4-develop.4236.1.9fa6806b34

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 (370) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_time_entry/index.js +37 -0
  3. package/dist/generated/entities/staff_time_entry/index.js.map +7 -0
  4. package/dist/generated/entities/staff_time_entry_segment/index.js +23 -0
  5. package/dist/generated/entities/staff_time_entry_segment/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +35 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +7 -0
  8. package/dist/generated/entities/staff_time_project_member/index.js +29 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +7 -0
  10. package/dist/generated/entities.ids.generated.js +5 -1
  11. package/dist/generated/entities.ids.generated.js.map +2 -2
  12. package/dist/generated/entity-fields-registry.js +64 -0
  13. package/dist/generated/entity-fields-registry.js.map +2 -2
  14. package/dist/helpers/integration/timesheetFixtures.js +50 -0
  15. package/dist/helpers/integration/timesheetFixtures.js.map +7 -0
  16. package/dist/modules/attachments/api/library/[id]/route.js +20 -16
  17. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +18 -14
  19. package/dist/modules/attachments/api/route.js.map +2 -2
  20. package/dist/modules/auth/api/roles/acl/route.js +10 -4
  21. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  22. package/dist/modules/auth/api/sidebar/preferences/route.js +27 -20
  23. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  24. package/dist/modules/auth/api/users/acl/route.js +16 -11
  25. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +87 -71
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/services/sidebarPreferencesService.js +39 -30
  29. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  30. package/dist/modules/catalog/commands/categories.js +61 -12
  31. package/dist/modules/catalog/commands/categories.js.map +2 -2
  32. package/dist/modules/catalog/commands/products.js +79 -54
  33. package/dist/modules/catalog/commands/products.js.map +2 -2
  34. package/dist/modules/catalog/commands/variants.js +29 -16
  35. package/dist/modules/catalog/commands/variants.js.map +2 -2
  36. package/dist/modules/currencies/commands/currencies.js +15 -8
  37. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users.js +27 -26
  39. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/password/reset-confirm.js +5 -5
  41. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +11 -10
  43. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  44. package/dist/modules/customers/commands/addresses.js +35 -21
  45. package/dist/modules/customers/commands/addresses.js.map +2 -2
  46. package/dist/modules/customers/commands/companies.js +163 -162
  47. package/dist/modules/customers/commands/companies.js.map +2 -2
  48. package/dist/modules/customers/commands/deals.js +3 -4
  49. package/dist/modules/customers/commands/deals.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +19 -22
  51. package/dist/modules/customers/commands/interactions.js.map +2 -2
  52. package/dist/modules/customers/commands/people.js +18 -15
  53. package/dist/modules/customers/commands/people.js.map +2 -2
  54. package/dist/modules/customers/commands/personCompanyLinks.js +105 -94
  55. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  56. package/dist/modules/customers/commands/pipeline-stages.js +30 -23
  57. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  58. package/dist/modules/customers/commands/pipelines.js +27 -20
  59. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  60. package/dist/modules/customers/commands/tags.js +13 -5
  61. package/dist/modules/customers/commands/tags.js.map +2 -2
  62. package/dist/modules/dashboards/api/users/widgets/route.js +0 -1
  63. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  64. package/dist/modules/dashboards/api/widgets/data/route.js +29 -1
  65. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  66. package/dist/modules/data_sync/lib/sync-engine.js +4 -4
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  68. package/dist/modules/data_sync/lib/sync-run-service.js +51 -27
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  70. package/dist/modules/directory/commands/organizations.js +192 -158
  71. package/dist/modules/directory/commands/organizations.js.map +3 -3
  72. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +22 -16
  73. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  74. package/dist/modules/messages/commands/messages.js +77 -75
  75. package/dist/modules/messages/commands/messages.js.map +2 -2
  76. package/dist/modules/messages/commands/shared.js +132 -132
  77. package/dist/modules/messages/commands/shared.js.map +2 -2
  78. package/dist/modules/perspectives/api/[tableId]/route.js +37 -26
  79. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  80. package/dist/modules/resources/commands/resources.js +125 -117
  81. package/dist/modules/resources/commands/resources.js.map +2 -2
  82. package/dist/modules/resources/commands/tags.js +7 -3
  83. package/dist/modules/resources/commands/tags.js.map +2 -2
  84. package/dist/modules/sales/api/quotes/send/route.js +12 -11
  85. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  86. package/dist/modules/sales/commands/documents.js +629 -478
  87. package/dist/modules/sales/commands/documents.js.map +2 -2
  88. package/dist/modules/sales/commands/payments.js +146 -146
  89. package/dist/modules/sales/commands/payments.js.map +2 -2
  90. package/dist/modules/sales/commands/returns.js +68 -60
  91. package/dist/modules/sales/commands/returns.js.map +2 -2
  92. package/dist/modules/staff/acl.js +10 -1
  93. package/dist/modules/staff/acl.js.map +2 -2
  94. package/dist/modules/staff/analytics.js +33 -0
  95. package/dist/modules/staff/analytics.js.map +7 -0
  96. package/dist/modules/staff/api/guards.js +31 -0
  97. package/dist/modules/staff/api/guards.js.map +7 -0
  98. package/dist/modules/staff/api/interceptors.js +96 -0
  99. package/dist/modules/staff/api/interceptors.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +170 -0
  101. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/my-projects/route.js +103 -0
  103. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +147 -0
  105. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +171 -0
  107. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +180 -0
  109. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +155 -0
  111. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +173 -0
  113. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +260 -0
  115. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/time-entries/route.js +188 -0
  117. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +159 -0
  119. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/time-projects/route.js +230 -0
  121. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +7 -0
  122. package/dist/modules/staff/backend/staff/timesheets/page.js +710 -0
  123. package/dist/modules/staff/backend/staff/timesheets/page.js.map +7 -0
  124. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +22 -0
  125. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +7 -0
  126. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +125 -0
  127. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +7 -0
  128. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +16 -0
  129. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +7 -0
  130. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +418 -0
  131. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +7 -0
  132. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +16 -0
  133. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +7 -0
  134. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +79 -0
  135. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +7 -0
  136. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +16 -0
  137. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +7 -0
  138. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +602 -0
  139. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +7 -0
  140. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +25 -0
  141. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +7 -0
  142. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +123 -0
  143. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +7 -0
  144. package/dist/modules/staff/cli.js +38 -1
  145. package/dist/modules/staff/cli.js.map +2 -2
  146. package/dist/modules/staff/commands/index.js +2 -0
  147. package/dist/modules/staff/commands/index.js.map +2 -2
  148. package/dist/modules/staff/commands/leave-requests.js +30 -28
  149. package/dist/modules/staff/commands/leave-requests.js.map +3 -3
  150. package/dist/modules/staff/commands/team-members.js +21 -20
  151. package/dist/modules/staff/commands/team-members.js.map +2 -2
  152. package/dist/modules/staff/commands/timesheets-entries.js +409 -0
  153. package/dist/modules/staff/commands/timesheets-entries.js.map +7 -0
  154. package/dist/modules/staff/commands/timesheets-projects.js +618 -0
  155. package/dist/modules/staff/commands/timesheets-projects.js.map +7 -0
  156. package/dist/modules/staff/data/enrichers.js +104 -0
  157. package/dist/modules/staff/data/enrichers.js.map +7 -0
  158. package/dist/modules/staff/data/entities.js +226 -1
  159. package/dist/modules/staff/data/entities.js.map +2 -2
  160. package/dist/modules/staff/data/validators.js +113 -1
  161. package/dist/modules/staff/data/validators.js.map +2 -2
  162. package/dist/modules/staff/events.js +13 -1
  163. package/dist/modules/staff/events.js.map +2 -2
  164. package/dist/modules/staff/lib/crud.js +7 -1
  165. package/dist/modules/staff/lib/crud.js.map +2 -2
  166. package/dist/modules/staff/lib/staffMemberResolver.js +15 -0
  167. package/dist/modules/staff/lib/staffMemberResolver.js.map +7 -0
  168. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js +60 -0
  169. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js.map +7 -0
  170. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js +260 -0
  171. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js.map +7 -0
  172. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js +41 -0
  173. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js.map +7 -0
  174. package/dist/modules/staff/lib/timesheets-projects/initials.js +10 -0
  175. package/dist/modules/staff/lib/timesheets-projects/initials.js.map +7 -0
  176. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js +12 -0
  177. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js.map +7 -0
  178. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js +55 -0
  179. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js.map +7 -0
  180. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js +66 -0
  181. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js.map +7 -0
  182. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +81 -0
  183. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +7 -0
  184. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js +58 -0
  185. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js.map +7 -0
  186. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js +152 -0
  187. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js.map +7 -0
  188. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +37 -0
  189. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +7 -0
  190. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js +57 -0
  191. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js.map +7 -0
  192. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js +50 -0
  193. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js.map +7 -0
  194. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js +163 -0
  195. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js.map +7 -0
  196. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js +209 -0
  197. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js.map +7 -0
  198. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js +52 -0
  199. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js.map +7 -0
  200. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +77 -0
  201. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +7 -0
  202. package/dist/modules/staff/lib/timesheets-ui/ListView.js +173 -0
  203. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +7 -0
  204. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js +32 -0
  205. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js.map +7 -0
  206. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +270 -0
  207. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +7 -0
  208. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js +57 -0
  209. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js.map +7 -0
  210. package/dist/modules/staff/lib/timesheets-ui/colors.js +43 -0
  211. package/dist/modules/staff/lib/timesheets-ui/colors.js.map +7 -0
  212. package/dist/modules/staff/migrations/Migration20260326135612.js +24 -0
  213. package/dist/modules/staff/migrations/Migration20260326135612.js.map +7 -0
  214. package/dist/modules/staff/migrations/Migration20260413102715.js +23 -0
  215. package/dist/modules/staff/migrations/Migration20260413102715.js.map +7 -0
  216. package/dist/modules/staff/migrations/Migration20260413111602.js +13 -0
  217. package/dist/modules/staff/migrations/Migration20260413111602.js.map +7 -0
  218. package/dist/modules/staff/migrations/Migration20260511112759.js +19 -0
  219. package/dist/modules/staff/migrations/Migration20260511112759.js.map +7 -0
  220. package/dist/modules/staff/search.js +35 -0
  221. package/dist/modules/staff/search.js.map +2 -2
  222. package/dist/modules/staff/setup.js +15 -1
  223. package/dist/modules/staff/setup.js.map +2 -2
  224. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js +16 -0
  225. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js.map +7 -0
  226. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js +126 -0
  227. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js.map +7 -0
  228. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js +26 -0
  229. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js.map +7 -0
  230. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js +15 -0
  231. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js.map +7 -0
  232. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +238 -0
  233. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +7 -0
  234. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js +26 -0
  235. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +145 -0
  237. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection-table.js +12 -0
  239. package/dist/modules/staff/widgets/injection-table.js.map +7 -0
  240. package/dist/modules/sync_excel/api/import/route.js +19 -17
  241. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  242. package/dist/modules/translations/commands/translations.js +22 -19
  243. package/dist/modules/translations/commands/translations.js.map +2 -2
  244. package/generated/entities/staff_time_entry/index.ts +17 -0
  245. package/generated/entities/staff_time_entry_segment/index.ts +10 -0
  246. package/generated/entities/staff_time_project/index.ts +16 -0
  247. package/generated/entities/staff_time_project_member/index.ts +13 -0
  248. package/generated/entities.ids.generated.ts +5 -1
  249. package/generated/entity-fields-registry.ts +64 -0
  250. package/package.json +7 -7
  251. package/src/helpers/integration/timesheetFixtures.ts +61 -0
  252. package/src/modules/attachments/api/library/[id]/route.ts +24 -17
  253. package/src/modules/attachments/api/route.ts +20 -14
  254. package/src/modules/auth/api/roles/acl/route.ts +11 -5
  255. package/src/modules/auth/api/sidebar/preferences/route.ts +33 -24
  256. package/src/modules/auth/api/users/acl/route.ts +17 -12
  257. package/src/modules/auth/commands/users.ts +96 -80
  258. package/src/modules/auth/services/sidebarPreferencesService.ts +40 -32
  259. package/src/modules/catalog/commands/categories.ts +61 -12
  260. package/src/modules/catalog/commands/products.ts +93 -60
  261. package/src/modules/catalog/commands/variants.ts +29 -16
  262. package/src/modules/currencies/commands/currencies.ts +27 -14
  263. package/src/modules/customer_accounts/api/admin/users.ts +31 -26
  264. package/src/modules/customer_accounts/api/password/reset-confirm.ts +5 -6
  265. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +14 -13
  266. package/src/modules/customers/commands/addresses.ts +35 -23
  267. package/src/modules/customers/commands/companies.ts +166 -165
  268. package/src/modules/customers/commands/deals.ts +2 -4
  269. package/src/modules/customers/commands/interactions.ts +20 -26
  270. package/src/modules/customers/commands/people.ts +18 -15
  271. package/src/modules/customers/commands/personCompanyLinks.ts +109 -100
  272. package/src/modules/customers/commands/pipeline-stages.ts +31 -27
  273. package/src/modules/customers/commands/pipelines.ts +29 -23
  274. package/src/modules/customers/commands/tags.ts +13 -5
  275. package/src/modules/dashboards/api/users/widgets/route.ts +0 -1
  276. package/src/modules/dashboards/api/widgets/data/route.ts +36 -1
  277. package/src/modules/data_sync/lib/sync-engine.ts +4 -5
  278. package/src/modules/data_sync/lib/sync-run-service.ts +57 -28
  279. package/src/modules/directory/commands/organizations.ts +203 -166
  280. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +26 -18
  281. package/src/modules/messages/commands/messages.ts +82 -80
  282. package/src/modules/messages/commands/shared.ts +138 -133
  283. package/src/modules/perspectives/api/[tableId]/route.ts +38 -27
  284. package/src/modules/resources/commands/resources.ts +127 -117
  285. package/src/modules/resources/commands/tags.ts +7 -3
  286. package/src/modules/sales/api/quotes/send/route.ts +17 -12
  287. package/src/modules/sales/commands/documents.ts +673 -481
  288. package/src/modules/sales/commands/payments.ts +158 -152
  289. package/src/modules/sales/commands/returns.ts +74 -63
  290. package/src/modules/staff/acl.ts +11 -0
  291. package/src/modules/staff/analytics.ts +30 -0
  292. package/src/modules/staff/api/guards.ts +59 -0
  293. package/src/modules/staff/api/interceptors.ts +122 -0
  294. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +191 -0
  295. package/src/modules/staff/api/timesheets/my-projects/route.ts +115 -0
  296. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +159 -0
  297. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +187 -0
  298. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +191 -0
  299. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +168 -0
  300. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +191 -0
  301. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +292 -0
  302. package/src/modules/staff/api/timesheets/time-entries/route.ts +193 -0
  303. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +167 -0
  304. package/src/modules/staff/api/timesheets/time-projects/route.ts +244 -0
  305. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +20 -0
  306. package/src/modules/staff/backend/staff/timesheets/page.tsx +899 -0
  307. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +12 -0
  308. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +141 -0
  309. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +12 -0
  310. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +579 -0
  311. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +12 -0
  312. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +90 -0
  313. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +23 -0
  314. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +765 -0
  315. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +138 -0
  316. package/src/modules/staff/cli.ts +40 -1
  317. package/src/modules/staff/commands/index.ts +2 -0
  318. package/src/modules/staff/commands/leave-requests.ts +37 -29
  319. package/src/modules/staff/commands/team-members.ts +25 -20
  320. package/src/modules/staff/commands/timesheets-entries.ts +504 -0
  321. package/src/modules/staff/commands/timesheets-projects.ts +699 -0
  322. package/src/modules/staff/data/enrichers.ts +134 -0
  323. package/src/modules/staff/data/entities.ts +198 -0
  324. package/src/modules/staff/data/validators.ts +129 -0
  325. package/src/modules/staff/events.ts +13 -0
  326. package/src/modules/staff/i18n/de.json +209 -1
  327. package/src/modules/staff/i18n/en.json +209 -1
  328. package/src/modules/staff/i18n/es.json +209 -1
  329. package/src/modules/staff/i18n/pl.json +209 -1
  330. package/src/modules/staff/lib/crud.ts +8 -0
  331. package/src/modules/staff/lib/staffMemberResolver.ts +22 -0
  332. package/src/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.ts +89 -0
  333. package/src/modules/staff/lib/timesheets-projects/computeProjectsKpis.ts +311 -0
  334. package/src/modules/staff/lib/timesheets-projects/dateBuckets.ts +37 -0
  335. package/src/modules/staff/lib/timesheets-projects/initials.ts +6 -0
  336. package/src/modules/staff/lib/timesheets-projects/kpiMath.ts +8 -0
  337. package/src/modules/staff/lib/timesheets-projects/listProjectMembersPreview.ts +83 -0
  338. package/src/modules/staff/lib/timesheets-projects-ui/HoursSparkline.tsx +75 -0
  339. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +110 -0
  340. package/src/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.tsx +73 -0
  341. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.tsx +185 -0
  342. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +53 -0
  343. package/src/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.tsx +63 -0
  344. package/src/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.ts +63 -0
  345. package/src/modules/staff/lib/timesheets-ui/AddRowDropdown.tsx +188 -0
  346. package/src/modules/staff/lib/timesheets-ui/CalendarPicker.tsx +229 -0
  347. package/src/modules/staff/lib/timesheets-ui/ColorPicker.tsx +65 -0
  348. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +99 -0
  349. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +230 -0
  350. package/src/modules/staff/lib/timesheets-ui/ProjectColorDot.tsx +40 -0
  351. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +327 -0
  352. package/src/modules/staff/lib/timesheets-ui/ViewSwitcher.tsx +60 -0
  353. package/src/modules/staff/lib/timesheets-ui/colors.ts +58 -0
  354. package/src/modules/staff/migrations/.snapshot-open-mercato.json +1148 -0
  355. package/src/modules/staff/migrations/Migration20260326135612.ts +26 -0
  356. package/src/modules/staff/migrations/Migration20260413102715.ts +25 -0
  357. package/src/modules/staff/migrations/Migration20260413111602.ts +13 -0
  358. package/src/modules/staff/migrations/Migration20260511112759.ts +21 -0
  359. package/src/modules/staff/search.ts +35 -0
  360. package/src/modules/staff/setup.ts +15 -0
  361. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.ts +17 -0
  362. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.tsx +158 -0
  363. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.ts +25 -0
  364. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/config.ts +15 -0
  365. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +297 -0
  366. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.ts +25 -0
  367. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +161 -0
  368. package/src/modules/staff/widgets/injection-table.ts +10 -0
  369. package/src/modules/sync_excel/api/import/route.ts +23 -18
  370. package/src/modules/translations/commands/translations.ts +49 -41
@@ -0,0 +1,122 @@
1
+ import type { ApiInterceptor, InterceptorContext } from '@open-mercato/shared/lib/crud/api-interceptor'
2
+ import { hasFeature } from '@open-mercato/shared/security/features'
3
+ import { getStaffMemberByUserId } from '../lib/staffMemberResolver'
4
+
5
+ type RbacServiceLike = {
6
+ userHasAllFeatures: (
7
+ userId: string,
8
+ required: string[],
9
+ scope: { tenantId: string | null; organizationId: string | null },
10
+ ) => Promise<boolean>
11
+ }
12
+
13
+ /**
14
+ * Checks whether the caller has manage_all rights, honoring wildcard ACL grants
15
+ * (`staff.*`, `*`) and the super-admin flag.
16
+ *
17
+ * `context.userFeatures` is populated only on CRUD-factory routes (factory.ts
18
+ * calls `rbacService.getGrantedFeatures`). Custom routes that wire interceptors
19
+ * manually (like `dashboards/widgets/data`) pass `auth.features` directly, and
20
+ * the JWT does NOT include features — so we must fall back to loading the ACL
21
+ * from `rbacService.userHasAllFeatures`, which is cached and wildcard-aware.
22
+ */
23
+ async function hasManageAllFeature(context: InterceptorContext): Promise<boolean> {
24
+ if (hasFeature(context.userFeatures, 'staff.timesheets.manage_all')) return true
25
+ try {
26
+ const rbac = context.container.resolve('rbacService') as RbacServiceLike | undefined
27
+ if (rbac?.userHasAllFeatures) {
28
+ return await rbac.userHasAllFeatures(
29
+ context.userId,
30
+ ['staff.timesheets.manage_all'],
31
+ { tenantId: context.tenantId ?? null, organizationId: context.organizationId ?? null },
32
+ )
33
+ }
34
+ } catch {
35
+ // rbacService not available — fall through to deny
36
+ }
37
+ return false
38
+ }
39
+
40
+ export const interceptors: ApiInterceptor[] = [
41
+ {
42
+ id: 'staff.timesheets.self-scope-widget-data',
43
+ targetRoute: 'dashboards/widgets/data',
44
+ methods: ['POST'],
45
+ priority: 70,
46
+ async before(request, context) {
47
+ const entityType = request.body?.entityType
48
+ if (entityType !== 'staff:staff_time_entries') {
49
+ return { ok: true }
50
+ }
51
+
52
+ if (await hasManageAllFeature(context)) {
53
+ return { ok: true }
54
+ }
55
+
56
+ const staffMember = await getStaffMemberByUserId(
57
+ context.em,
58
+ context.userId,
59
+ context.tenantId ?? null,
60
+ context.organizationId ?? null,
61
+ )
62
+
63
+ if (!staffMember) {
64
+ return {
65
+ ok: false,
66
+ statusCode: 403,
67
+ message: 'User is not a staff member.',
68
+ }
69
+ }
70
+
71
+ const existingFilters = Array.isArray(request.body?.filters) ? request.body.filters : []
72
+ const otherFilters = existingFilters.filter(
73
+ (f: Record<string, unknown>) => f.field !== 'staffMemberId',
74
+ )
75
+
76
+ return {
77
+ ok: true,
78
+ body: {
79
+ ...request.body,
80
+ filters: [
81
+ ...otherFilters,
82
+ { field: 'staffMemberId', operator: 'eq', value: staffMember.id },
83
+ ],
84
+ },
85
+ }
86
+ },
87
+ },
88
+ {
89
+ id: 'staff.timesheets.self-scope-time-entries',
90
+ targetRoute: 'staff/timesheets/time-entries',
91
+ methods: ['GET'],
92
+ priority: 70,
93
+ async before(request, context) {
94
+ if (await hasManageAllFeature(context)) {
95
+ return { ok: true }
96
+ }
97
+
98
+ const staffMember = await getStaffMemberByUserId(
99
+ context.em,
100
+ context.userId,
101
+ context.tenantId ?? null,
102
+ context.organizationId ?? null,
103
+ )
104
+
105
+ if (!staffMember) {
106
+ return {
107
+ ok: false,
108
+ statusCode: 403,
109
+ message: 'User is not a staff member.',
110
+ }
111
+ }
112
+
113
+ return {
114
+ ok: true,
115
+ query: {
116
+ ...(request.query ?? {}),
117
+ staffMemberId: staffMember.id,
118
+ },
119
+ }
120
+ },
121
+ },
122
+ ]
@@ -0,0 +1,191 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
9
+ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import type { EntityManager } from '@mikro-orm/postgresql'
11
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
+ import { StaffTimeProjectMember, StaffTeamMember } from '../../../../data/entities'
13
+ import { staffMyProjectVisibilityUpdateSchema } from '../../../../data/validators'
14
+ import {
15
+ resolveUserFeatures,
16
+ runStaffMutationGuardAfterSuccess,
17
+ runStaffMutationGuards,
18
+ } from '../../../guards'
19
+
20
+ export const metadata = {
21
+ PATCH: { requireAuth: true, requireFeatures: ['staff.timesheets.manage_own'] },
22
+ }
23
+
24
+ function extractProjectIdFromUrl(req: Request): string | null {
25
+ try {
26
+ const url = new URL(req.url)
27
+ const match = url.pathname.match(/\/my-projects\/([^/]+)(?:\/|$)/)
28
+ return match?.[1] ?? null
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ /**
35
+ * PATCH /api/staff/timesheets/my-projects/{projectId}
36
+ *
37
+ * Self-service endpoint for the authenticated user to toggle visibility of a
38
+ * time project on their own My Timesheets grid. Does not require the admin-only
39
+ * `staff.timesheets.projects.manage` feature — only `staff.timesheets.manage_own`.
40
+ *
41
+ * Added 2026-04-13 to close a gap in the original UX enhancements spec:
42
+ * "+ Add row" and the new X remove button need to persist per-user grid membership.
43
+ */
44
+ export async function PATCH(req: Request) {
45
+ try {
46
+ const container = await createRequestContainer()
47
+ const auth = await getAuthFromRequest(req)
48
+ const { translate } = await resolveTranslations()
49
+ if (!auth) {
50
+ throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
51
+ }
52
+
53
+ const projectId = extractProjectIdFromUrl(req)
54
+ if (!projectId || !z.string().uuid().safeParse(projectId).success) {
55
+ throw new CrudHttpError(400, {
56
+ error: translate('staff.timesheets.errors.invalidProjectId', 'Invalid project id.'),
57
+ })
58
+ }
59
+
60
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
61
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
62
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
63
+ if (!tenantId || !organizationId) {
64
+ throw new CrudHttpError(400, {
65
+ error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.'),
66
+ })
67
+ }
68
+
69
+ const rawBody = await readJsonSafe(req, {})
70
+ const parsed = staffMyProjectVisibilityUpdateSchema.safeParse(rawBody)
71
+ if (!parsed.success) {
72
+ throw new CrudHttpError(400, {
73
+ error: translate('staff.timesheets.errors.invalidBody', 'Invalid request body.'),
74
+ details: parsed.error.flatten(),
75
+ })
76
+ }
77
+
78
+ const em = (container.resolve('em') as EntityManager).fork()
79
+ const scopeCtx = { tenantId, organizationId }
80
+
81
+ const staffMember = await findOneWithDecryption(
82
+ em,
83
+ StaffTeamMember,
84
+ { userId: auth.sub, tenantId, organizationId, deletedAt: null },
85
+ {},
86
+ scopeCtx,
87
+ )
88
+ if (!staffMember) {
89
+ throw new CrudHttpError(403, {
90
+ error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.'),
91
+ })
92
+ }
93
+
94
+ const membership = await findOneWithDecryption(
95
+ em,
96
+ StaffTimeProjectMember,
97
+ {
98
+ timeProjectId: projectId,
99
+ staffMemberId: staffMember.id,
100
+ tenantId,
101
+ organizationId,
102
+ deletedAt: null,
103
+ status: 'active',
104
+ },
105
+ {},
106
+ scopeCtx,
107
+ )
108
+ if (!membership) {
109
+ throw new CrudHttpError(404, {
110
+ error: translate('staff.timesheets.errors.notAssigned', 'You are not assigned to this project.'),
111
+ })
112
+ }
113
+
114
+ const guardResult = await runStaffMutationGuards(
115
+ container,
116
+ {
117
+ tenantId,
118
+ organizationId,
119
+ userId: auth.sub ?? '',
120
+ resourceKind: 'staff.timesheets.time_project_member',
121
+ resourceId: membership.id,
122
+ operation: 'update',
123
+ requestMethod: req.method,
124
+ requestHeaders: req.headers,
125
+ mutationPayload: parsed.data as unknown as Record<string, unknown>,
126
+ },
127
+ resolveUserFeatures(auth),
128
+ )
129
+ if (!guardResult.ok) {
130
+ return NextResponse.json(
131
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
132
+ { status: guardResult.errorStatus ?? 422 },
133
+ )
134
+ }
135
+
136
+ membership.showInGrid = parsed.data.showInGrid
137
+ await em.flush()
138
+
139
+ if (guardResult.afterSuccessCallbacks.length) {
140
+ await runStaffMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
141
+ tenantId,
142
+ organizationId,
143
+ userId: auth.sub ?? '',
144
+ resourceKind: 'staff.timesheets.time_project_member',
145
+ resourceId: membership.id,
146
+ operation: 'update',
147
+ requestMethod: req.method,
148
+ requestHeaders: req.headers,
149
+ })
150
+ }
151
+
152
+ return NextResponse.json({ ok: true, showInGrid: membership.showInGrid }, { status: 200 })
153
+ } catch (err) {
154
+ if (err instanceof CrudHttpError) {
155
+ return NextResponse.json(err.body, { status: err.status })
156
+ }
157
+ console.error('staff.timesheets.my-projects.patch failed', err)
158
+ const { translate } = await resolveTranslations()
159
+ return NextResponse.json(
160
+ { error: translate('staff.timesheets.errors.updateMyProject', 'Failed to update project visibility.') },
161
+ { status: 400 },
162
+ )
163
+ }
164
+ }
165
+
166
+ export const openApi: OpenApiRouteDoc = {
167
+ tag: 'Staff',
168
+ summary: 'My project grid visibility',
169
+ methods: {
170
+ PATCH: {
171
+ summary: 'Toggle grid visibility for one of the caller\'s assigned time projects',
172
+ description:
173
+ 'Self-service endpoint. Only the authenticated user can toggle `show_in_grid` on their own active membership for the given project. Does not require the admin-only `staff.timesheets.projects.manage` feature.',
174
+ requestBody: {
175
+ contentType: 'application/json',
176
+ schema: staffMyProjectVisibilityUpdateSchema,
177
+ },
178
+ responses: [
179
+ {
180
+ status: 200,
181
+ description: 'Visibility updated',
182
+ schema: z.object({ ok: z.boolean(), showInGrid: z.boolean() }),
183
+ },
184
+ { status: 400, description: 'Invalid input', schema: z.object({ error: z.string() }) },
185
+ { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
186
+ { status: 403, description: 'No staff member linked', schema: z.object({ error: z.string() }) },
187
+ { status: 404, description: 'Project membership not found', schema: z.object({ error: z.string() }) },
188
+ ],
189
+ },
190
+ },
191
+ }
@@ -0,0 +1,115 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
10
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
11
+ import { StaffTimeProjectMember, StaffTeamMember } from '../../../data/entities'
12
+
13
+ export const metadata = {
14
+ GET: { requireAuth: true, requireFeatures: ['staff.timesheets.view'] },
15
+ }
16
+
17
+ /**
18
+ * GET /api/staff/timesheets/my-projects
19
+ *
20
+ * Spec N+1 Mitigation — Query 1:
21
+ * Returns staff_time_project_members for the authenticated staff member.
22
+ * Used by "My Timesheets" grid to resolve assigned project IDs.
23
+ */
24
+ export async function GET(req: Request) {
25
+ try {
26
+ const container = await createRequestContainer()
27
+ const auth = await getAuthFromRequest(req)
28
+ const { translate } = await resolveTranslations()
29
+ if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
30
+
31
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
32
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
33
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
34
+ if (!tenantId || !organizationId) {
35
+ throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
36
+ }
37
+
38
+ const em = (container.resolve('em') as EntityManager).fork()
39
+ const scopeCtx = { tenantId, organizationId }
40
+
41
+ const staffMember = await findOneWithDecryption(
42
+ em,
43
+ StaffTeamMember,
44
+ { userId: auth.sub, tenantId, organizationId, deletedAt: null },
45
+ {},
46
+ scopeCtx,
47
+ )
48
+ if (!staffMember) {
49
+ throw new CrudHttpError(403, { error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.') })
50
+ }
51
+
52
+ const assignments = await findWithDecryption(
53
+ em,
54
+ StaffTimeProjectMember,
55
+ { staffMemberId: staffMember.id, tenantId, organizationId, deletedAt: null, status: 'active' },
56
+ {},
57
+ scopeCtx,
58
+ )
59
+
60
+ const items = assignments.map((assignment) => ({
61
+ id: assignment.id,
62
+ time_project_id: assignment.timeProjectId,
63
+ staff_member_id: assignment.staffMemberId,
64
+ role: assignment.role ?? null,
65
+ status: assignment.status ?? null,
66
+ assigned_start_date: assignment.assignedStartDate ?? null,
67
+ assigned_end_date: assignment.assignedEndDate ?? null,
68
+ show_in_grid: assignment.showInGrid ?? false,
69
+ }))
70
+
71
+ return NextResponse.json({ items, total: items.length }, { status: 200 })
72
+ } catch (err) {
73
+ if (err instanceof CrudHttpError) {
74
+ return NextResponse.json(err.body, { status: err.status })
75
+ }
76
+ console.error('staff.timesheets.my-projects failed', err)
77
+ const { translate } = await resolveTranslations()
78
+ return NextResponse.json(
79
+ { error: translate('staff.timesheets.errors.myProjects', 'Failed to load your projects.') },
80
+ { status: 400 },
81
+ )
82
+ }
83
+ }
84
+
85
+ export const openApi: OpenApiRouteDoc = {
86
+ tag: 'Staff',
87
+ summary: 'My assigned time projects',
88
+ methods: {
89
+ GET: {
90
+ summary: 'List assigned time project memberships for the current user',
91
+ description: 'Returns staff_time_project_members where the authenticated user is an active member. Used by the My Timesheets grid to resolve assigned project IDs (spec N+1 mitigation query 1).',
92
+ responses: [
93
+ {
94
+ status: 200,
95
+ description: 'Assigned project memberships',
96
+ schema: z.object({
97
+ items: z.array(z.object({
98
+ id: z.string().uuid(),
99
+ time_project_id: z.string().uuid(),
100
+ staff_member_id: z.string().uuid(),
101
+ role: z.string().nullable(),
102
+ status: z.string().nullable(),
103
+ assigned_start_date: z.string().nullable(),
104
+ assigned_end_date: z.string().nullable(),
105
+ show_in_grid: z.boolean(),
106
+ })),
107
+ total: z.number(),
108
+ }),
109
+ },
110
+ { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
111
+ { status: 403, description: 'No staff member linked', schema: z.object({ error: z.string() }) },
112
+ ],
113
+ },
114
+ },
115
+ }
@@ -0,0 +1,159 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
10
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
11
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
12
+ import { StaffTeamMember } from '../../../../data/entities'
13
+ import {
14
+ computeCollabProjectsKpis,
15
+ computePmProjectsKpis,
16
+ } from '../../../../lib/timesheets-projects/computeProjectsKpis'
17
+
18
+ const VIEW_FEATURE = 'staff.timesheets.projects.view'
19
+ const MANAGE_FEATURE = 'staff.timesheets.projects.manage'
20
+
21
+ export const metadata = {
22
+ GET: { requireAuth: true, requireFeatures: [VIEW_FEATURE] },
23
+ }
24
+
25
+ const deltaSchema = z.object({
26
+ current: z.number(),
27
+ previous: z.number(),
28
+ deltaPct: z.number().nullable(),
29
+ })
30
+
31
+ const pmResponseSchema = z.object({
32
+ role: z.literal('pm'),
33
+ totals: z.object({
34
+ total: z.number().int(),
35
+ active: z.number().int(),
36
+ onHold: z.number().int(),
37
+ completed: z.number().int(),
38
+ }),
39
+ hoursWeek: deltaSchema,
40
+ hoursMonth: deltaSchema,
41
+ teamActive: z.object({ count: z.number().int() }),
42
+ assignedToMe: z.object({ total: z.number().int(), active: z.number().int() }),
43
+ })
44
+
45
+ const collabResponseSchema = z.object({
46
+ role: z.literal('collab'),
47
+ myProjects: z.object({
48
+ total: z.number().int(),
49
+ active: z.number().int(),
50
+ }),
51
+ myHoursWeek: deltaSchema,
52
+ myHoursMonth: deltaSchema,
53
+ })
54
+
55
+ export async function GET(req: Request) {
56
+ try {
57
+ const container = await createRequestContainer()
58
+ const auth = await getAuthFromRequest(req)
59
+ const { translate } = await resolveTranslations()
60
+
61
+ if (!auth) {
62
+ throw new CrudHttpError(401, {
63
+ error: translate('staff.errors.unauthorized', 'Unauthorized'),
64
+ })
65
+ }
66
+
67
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
68
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
69
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
70
+ if (!tenantId || !organizationId) {
71
+ throw new CrudHttpError(400, {
72
+ error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.'),
73
+ })
74
+ }
75
+
76
+ const em = container.resolve('em') as EntityManager
77
+ const rbac = container.resolve('rbacService') as RbacService
78
+ const isPm = await rbac.userHasAllFeatures(auth.sub, [MANAGE_FEATURE], {
79
+ tenantId,
80
+ organizationId,
81
+ })
82
+
83
+ const staffMember = await findOneWithDecryption(
84
+ em.fork(),
85
+ StaffTeamMember,
86
+ { userId: auth.sub, tenantId, organizationId, deletedAt: null },
87
+ {},
88
+ { tenantId, organizationId },
89
+ )
90
+
91
+ if (isPm) {
92
+ const result = await computePmProjectsKpis({
93
+ em,
94
+ tenantId,
95
+ organizationId,
96
+ callerStaffMemberId: staffMember?.id ?? null,
97
+ })
98
+ return NextResponse.json(result, { status: 200 })
99
+ }
100
+
101
+ if (!staffMember) {
102
+ throw new CrudHttpError(403, {
103
+ error: translate(
104
+ 'staff.timesheets.errors.noStaffMember',
105
+ 'No staff member linked to your account.',
106
+ ),
107
+ })
108
+ }
109
+
110
+ const result = await computeCollabProjectsKpis({
111
+ em,
112
+ tenantId,
113
+ organizationId,
114
+ staffMemberId: staffMember.id,
115
+ })
116
+ return NextResponse.json(result, { status: 200 })
117
+ } catch (err) {
118
+ if (err instanceof CrudHttpError) {
119
+ return NextResponse.json(err.body, { status: err.status })
120
+ }
121
+ console.error('staff.timesheets.projects.kpis failed', err)
122
+ const { translate } = await resolveTranslations()
123
+ return NextResponse.json(
124
+ {
125
+ error: translate(
126
+ 'staff.timesheets.errors.projectsKpis',
127
+ 'Failed to load project KPIs.',
128
+ ),
129
+ },
130
+ { status: 500 },
131
+ )
132
+ }
133
+ }
134
+
135
+ export const openApi: OpenApiRouteDoc = {
136
+ tag: 'Staff',
137
+ summary: 'Timesheet projects KPIs',
138
+ methods: {
139
+ GET: {
140
+ summary: 'Aggregate KPIs for the timesheets Projects page',
141
+ description:
142
+ 'Returns a role-aware KPI payload. Users with `staff.timesheets.projects.manage` get the PM shape (portfolio totals, monthly team hours, active team member count). Other viewers get the Collaborator shape scoped to their own memberships and hours.',
143
+ responses: [
144
+ {
145
+ status: 200,
146
+ description: 'PM or Collaborator KPIs',
147
+ schema: z.union([pmResponseSchema, collabResponseSchema]),
148
+ },
149
+ { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
150
+ {
151
+ status: 403,
152
+ description: 'Missing viewing feature or no staff member linked',
153
+ schema: z.object({ error: z.string() }),
154
+ },
155
+ { status: 500, description: 'Aggregation failure', schema: z.object({ error: z.string() }) },
156
+ ],
157
+ },
158
+ },
159
+ }