@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,699 @@
1
+ import type { CommandHandler } from '@open-mercato/shared/lib/commands'
2
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { UniqueConstraintViolationException } from '@mikro-orm/core'
5
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
+ import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
9
+ import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
10
+ import { StaffTeamMember, StaffTimeProject, StaffTimeProjectMember, type StaffTimeProjectStatus, type StaffTimeProjectMemberStatus } from '../data/entities'
11
+
12
+ const timeProjectCrudIndexer: CrudIndexerConfig<StaffTimeProject> = {
13
+ entityType: 'staff:staff_time_project',
14
+ }
15
+ const timeProjectMemberCrudIndexer: CrudIndexerConfig<StaffTimeProjectMember> = {
16
+ entityType: 'staff:staff_time_project_member',
17
+ }
18
+ import {
19
+ staffTimeProjectCreateSchema,
20
+ staffTimeProjectUpdateSchema,
21
+ staffTimeProjectMemberAssignSchema,
22
+ staffTimeProjectMemberUpdateSchema,
23
+ type StaffTimeProjectCreateInput,
24
+ type StaffTimeProjectUpdateInput,
25
+ type StaffTimeProjectMemberAssignInput,
26
+ type StaffTimeProjectMemberUpdateInput,
27
+ } from '../data/validators'
28
+ import { staffTimeProjectCrudEvents, staffTimeProjectMemberCrudEvents } from '../lib/crud'
29
+ import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
30
+
31
+ function isUniqueViolation(error: unknown): boolean {
32
+ if (error instanceof UniqueConstraintViolationException) return true
33
+ if (!error || typeof error !== 'object') return false
34
+ const code = (error as { code?: string }).code
35
+ if (code === '23505') return true
36
+ const message = (error as { message?: string }).message
37
+ return typeof message === 'string' && message.toLowerCase().includes('duplicate key')
38
+ }
39
+
40
+ type TimeProjectSnapshot = {
41
+ id: string
42
+ tenantId: string
43
+ organizationId: string
44
+ name: string
45
+ customerId: string | null
46
+ code: string
47
+ description: string | null
48
+ projectType: string | null
49
+ color: string | null
50
+ status: string
51
+ ownerUserId: string | null
52
+ costCenter: string | null
53
+ startDate: string | null
54
+ deletedAt: string | null
55
+ }
56
+
57
+ type TimeProjectUndoPayload = {
58
+ before?: TimeProjectSnapshot | null
59
+ after?: TimeProjectSnapshot | null
60
+ }
61
+
62
+ type TimeProjectMemberSnapshot = {
63
+ id: string
64
+ tenantId: string
65
+ organizationId: string
66
+ timeProjectId: string
67
+ staffMemberId: string
68
+ role: string | null
69
+ status: string
70
+ showInGrid: boolean
71
+ assignedStartDate: string
72
+ assignedEndDate: string | null
73
+ deletedAt: string | null
74
+ }
75
+
76
+ type TimeProjectMemberUndoPayload = {
77
+ before?: TimeProjectMemberSnapshot | null
78
+ after?: TimeProjectMemberSnapshot | null
79
+ }
80
+
81
+ async function loadTimeProjectSnapshot(em: EntityManager, id: string): Promise<TimeProjectSnapshot | null> {
82
+ const project = await findOneWithDecryption(em, StaffTimeProject, { id }, undefined, { tenantId: null, organizationId: null })
83
+ if (!project) return null
84
+ return {
85
+ id: project.id,
86
+ tenantId: project.tenantId,
87
+ organizationId: project.organizationId,
88
+ name: project.name,
89
+ customerId: project.customerId ?? null,
90
+ code: project.code,
91
+ description: project.description ?? null,
92
+ projectType: project.projectType ?? null,
93
+ color: project.color ?? null,
94
+ status: project.status,
95
+ ownerUserId: project.ownerUserId ?? null,
96
+ costCenter: project.costCenter ?? null,
97
+ startDate: project.startDate instanceof Date ? project.startDate.toISOString().split('T')[0] : (project.startDate ?? null),
98
+ deletedAt: project.deletedAt ? project.deletedAt.toISOString() : null,
99
+ }
100
+ }
101
+
102
+ async function loadTimeProjectMemberSnapshot(em: EntityManager, id: string): Promise<TimeProjectMemberSnapshot | null> {
103
+ const member = await findOneWithDecryption(em, StaffTimeProjectMember, { id }, undefined, { tenantId: null, organizationId: null })
104
+ if (!member) return null
105
+ return {
106
+ id: member.id,
107
+ tenantId: member.tenantId,
108
+ organizationId: member.organizationId,
109
+ timeProjectId: member.timeProjectId,
110
+ staffMemberId: member.staffMemberId,
111
+ role: member.role ?? null,
112
+ status: member.status,
113
+ showInGrid: member.showInGrid ?? false,
114
+ assignedStartDate: member.assignedStartDate instanceof Date ? member.assignedStartDate.toISOString().split('T')[0] : String(member.assignedStartDate),
115
+ assignedEndDate: member.assignedEndDate instanceof Date ? member.assignedEndDate.toISOString().split('T')[0] : (member.assignedEndDate ?? null),
116
+ deletedAt: member.deletedAt ? member.deletedAt.toISOString() : null,
117
+ }
118
+ }
119
+
120
+ const createTimeProjectCommand: CommandHandler<StaffTimeProjectCreateInput, { timeProjectId: string }> = {
121
+ id: 'staff.timesheets.time_projects.create',
122
+ async execute(rawInput, ctx) {
123
+ const parsed = staffTimeProjectCreateSchema.parse(rawInput)
124
+ ensureTenantScope(ctx, parsed.tenantId)
125
+ ensureOrganizationScope(ctx, parsed.organizationId)
126
+
127
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
128
+ const now = new Date()
129
+ const project = em.create(StaffTimeProject, {
130
+ tenantId: parsed.tenantId,
131
+ organizationId: parsed.organizationId,
132
+ name: parsed.name,
133
+ customerId: parsed.customerId ?? null,
134
+ code: parsed.code,
135
+ description: parsed.description ?? null,
136
+ projectType: parsed.projectType ?? null,
137
+ color: parsed.color ?? null,
138
+ status: parsed.status ?? 'active',
139
+ ownerUserId: parsed.ownerUserId ?? null,
140
+ costCenter: parsed.costCenter ?? null,
141
+ startDate: parsed.startDate ?? null,
142
+ createdAt: now,
143
+ updatedAt: now,
144
+ deletedAt: null,
145
+ })
146
+ em.persist(project)
147
+ try {
148
+ await em.flush()
149
+ } catch (err) {
150
+ if (isUniqueViolation(err)) {
151
+ const { translate } = await resolveTranslations()
152
+ throw new CrudHttpError(409, {
153
+ error: translate('staff.timesheets.errors.projectCodeDuplicate', 'A project with this code already exists.'),
154
+ fieldErrors: { code: translate('staff.timesheets.errors.projectCodeDuplicate', 'A project with this code already exists.') },
155
+ })
156
+ }
157
+ throw err
158
+ }
159
+
160
+ await emitCrudSideEffects({
161
+ dataEngine: ctx.container.resolve('dataEngine'),
162
+ action: 'created',
163
+ entity: project,
164
+ identifiers: {
165
+ id: project.id,
166
+ organizationId: project.organizationId,
167
+ tenantId: project.tenantId,
168
+ },
169
+ events: staffTimeProjectCrudEvents,
170
+ indexer: timeProjectCrudIndexer,
171
+ })
172
+
173
+ return { timeProjectId: project.id }
174
+ },
175
+ captureAfter: async (_input, result, ctx) => {
176
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
177
+ const snapshot = await loadTimeProjectSnapshot(em, result.timeProjectId)
178
+ if (!snapshot) return null
179
+ return { snapshot }
180
+ },
181
+ buildLog: async ({ result, ctx }) => {
182
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
183
+ const snapshot = await loadTimeProjectSnapshot(em, result.timeProjectId)
184
+ if (!snapshot) return null
185
+ const { translate } = await resolveTranslations()
186
+ return {
187
+ actionLabel: translate('staff.audit.timesheets.time_projects.create', 'Create time project'),
188
+ resourceKind: 'staff.timesheets.time_project',
189
+ resourceId: snapshot.id,
190
+ tenantId: snapshot.tenantId,
191
+ organizationId: snapshot.organizationId,
192
+ snapshotAfter: snapshot,
193
+ payload: {
194
+ undo: {
195
+ after: snapshot,
196
+ } satisfies TimeProjectUndoPayload,
197
+ },
198
+ }
199
+ },
200
+ undo: async ({ logEntry, ctx }) => {
201
+ const payload = extractUndoPayload<TimeProjectUndoPayload>(logEntry)
202
+ const after = payload?.after
203
+ if (!after) return
204
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
205
+ const project = await em.findOne(StaffTimeProject, { id: after.id })
206
+ if (project) {
207
+ project.deletedAt = new Date()
208
+ await em.flush()
209
+
210
+ await emitCrudUndoSideEffects({
211
+ dataEngine: ctx.container.resolve('dataEngine'),
212
+ action: 'deleted',
213
+ entity: project,
214
+ identifiers: {
215
+ id: project.id,
216
+ organizationId: project.organizationId,
217
+ tenantId: project.tenantId,
218
+ },
219
+ events: staffTimeProjectCrudEvents,
220
+ })
221
+ }
222
+ },
223
+ }
224
+
225
+ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { timeProjectId: string }> = {
226
+ id: 'staff.timesheets.time_projects.update',
227
+ async prepare(rawInput, ctx) {
228
+ const parsed = staffTimeProjectUpdateSchema.parse(rawInput)
229
+ const em = (ctx.container.resolve('em') as EntityManager)
230
+ const snapshot = await loadTimeProjectSnapshot(em, parsed.id)
231
+ if (!snapshot) return {}
232
+ return { before: snapshot }
233
+ },
234
+ async execute(rawInput, ctx) {
235
+ const parsed = staffTimeProjectUpdateSchema.parse(rawInput)
236
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
237
+ const project = await findOneWithDecryption(
238
+ em,
239
+ StaffTimeProject,
240
+ { id: parsed.id, deletedAt: null },
241
+ undefined,
242
+ { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
243
+ )
244
+ if (!project) throw new CrudHttpError(404, { error: 'Time project not found.' })
245
+ ensureTenantScope(ctx, project.tenantId)
246
+ ensureOrganizationScope(ctx, project.organizationId)
247
+
248
+ if (parsed.name !== undefined) project.name = parsed.name
249
+ if (parsed.customerId !== undefined) project.customerId = parsed.customerId ?? null
250
+ if (parsed.code !== undefined) project.code = parsed.code
251
+ if (parsed.description !== undefined) project.description = parsed.description ?? null
252
+ if (parsed.projectType !== undefined) project.projectType = parsed.projectType ?? null
253
+ if (parsed.color !== undefined) project.color = parsed.color ?? null
254
+ if (parsed.status !== undefined) project.status = parsed.status
255
+ if (parsed.ownerUserId !== undefined) project.ownerUserId = parsed.ownerUserId ?? null
256
+ if (parsed.costCenter !== undefined) project.costCenter = parsed.costCenter ?? null
257
+ if (parsed.startDate !== undefined) project.startDate = parsed.startDate ?? null
258
+ project.updatedAt = new Date()
259
+ try {
260
+ await em.flush()
261
+ } catch (err) {
262
+ if (isUniqueViolation(err)) {
263
+ const { translate } = await resolveTranslations()
264
+ throw new CrudHttpError(409, {
265
+ error: translate('staff.timesheets.errors.projectCodeDuplicate', 'A project with this code already exists.'),
266
+ fieldErrors: { code: translate('staff.timesheets.errors.projectCodeDuplicate', 'A project with this code already exists.') },
267
+ })
268
+ }
269
+ throw err
270
+ }
271
+
272
+ await emitCrudSideEffects({
273
+ dataEngine: ctx.container.resolve('dataEngine'),
274
+ action: 'updated',
275
+ entity: project,
276
+ identifiers: {
277
+ id: project.id,
278
+ organizationId: project.organizationId,
279
+ tenantId: project.tenantId,
280
+ },
281
+ events: staffTimeProjectCrudEvents,
282
+ indexer: timeProjectCrudIndexer,
283
+ })
284
+
285
+ return { timeProjectId: project.id }
286
+ },
287
+ buildLog: async ({ snapshots, ctx }) => {
288
+ const before = snapshots.before as TimeProjectSnapshot | undefined
289
+ if (!before) return null
290
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
291
+ const after = await loadTimeProjectSnapshot(em, before.id)
292
+ if (!after) return null
293
+ const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [
294
+ 'name',
295
+ 'customerId',
296
+ 'code',
297
+ 'description',
298
+ 'projectType',
299
+ 'color',
300
+ 'status',
301
+ 'ownerUserId',
302
+ 'costCenter',
303
+ 'startDate',
304
+ 'deletedAt',
305
+ ])
306
+ const { translate } = await resolveTranslations()
307
+ return {
308
+ actionLabel: translate('staff.audit.timesheets.time_projects.update', 'Update time project'),
309
+ resourceKind: 'staff.timesheets.time_project',
310
+ resourceId: before.id,
311
+ tenantId: before.tenantId,
312
+ organizationId: before.organizationId,
313
+ snapshotBefore: before,
314
+ snapshotAfter: after,
315
+ changes,
316
+ payload: {
317
+ undo: {
318
+ before,
319
+ after,
320
+ } satisfies TimeProjectUndoPayload,
321
+ },
322
+ }
323
+ },
324
+ undo: async ({ logEntry, ctx }) => {
325
+ const payload = extractUndoPayload<TimeProjectUndoPayload>(logEntry)
326
+ const before = payload?.before
327
+ if (!before) return
328
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
329
+ const project = await em.findOne(StaffTimeProject, { id: before.id })
330
+ if (!project) return
331
+ project.name = before.name
332
+ project.customerId = before.customerId ?? null
333
+ project.code = before.code
334
+ project.description = before.description ?? null
335
+ project.projectType = before.projectType ?? null
336
+ project.color = before.color ?? null
337
+ project.status = (before.status ?? 'active') as StaffTimeProjectStatus
338
+ project.ownerUserId = before.ownerUserId ?? null
339
+ project.costCenter = before.costCenter ?? null
340
+ project.startDate = before.startDate ? new Date(before.startDate) : null
341
+ project.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null
342
+ project.updatedAt = new Date()
343
+ await em.flush()
344
+
345
+ await emitCrudUndoSideEffects({
346
+ dataEngine: ctx.container.resolve('dataEngine'),
347
+ action: 'updated',
348
+ entity: project,
349
+ identifiers: {
350
+ id: project.id,
351
+ organizationId: project.organizationId,
352
+ tenantId: project.tenantId,
353
+ },
354
+ events: staffTimeProjectCrudEvents,
355
+ indexer: timeProjectCrudIndexer,
356
+ })
357
+ },
358
+ }
359
+
360
+ const deleteTimeProjectCommand: CommandHandler<{ id?: string }, { timeProjectId: string }> = {
361
+ id: 'staff.timesheets.time_projects.delete',
362
+ async prepare(input, ctx) {
363
+ const id = input?.id
364
+ if (!id) throw new CrudHttpError(400, { error: 'Time project id is required.' })
365
+ const em = (ctx.container.resolve('em') as EntityManager)
366
+ const snapshot = await loadTimeProjectSnapshot(em, id)
367
+ if (!snapshot) return {}
368
+ return { before: snapshot }
369
+ },
370
+ async execute(input, ctx) {
371
+ const id = input?.id
372
+ if (!id) throw new CrudHttpError(400, { error: 'Time project id is required.' })
373
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
374
+ const project = await findOneWithDecryption(
375
+ em,
376
+ StaffTimeProject,
377
+ { id, deletedAt: null },
378
+ undefined,
379
+ { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
380
+ )
381
+ if (!project) throw new CrudHttpError(404, { error: 'Time project not found.' })
382
+ ensureTenantScope(ctx, project.tenantId)
383
+ ensureOrganizationScope(ctx, project.organizationId)
384
+
385
+ project.deletedAt = new Date()
386
+ project.updatedAt = new Date()
387
+ await em.flush()
388
+
389
+ await emitCrudSideEffects({
390
+ dataEngine: ctx.container.resolve('dataEngine'),
391
+ action: 'deleted',
392
+ entity: project,
393
+ identifiers: {
394
+ id: project.id,
395
+ organizationId: project.organizationId,
396
+ tenantId: project.tenantId,
397
+ },
398
+ events: staffTimeProjectCrudEvents,
399
+ indexer: timeProjectCrudIndexer,
400
+ })
401
+ return { timeProjectId: project.id }
402
+ },
403
+ buildLog: async ({ snapshots }) => {
404
+ const before = snapshots.before as TimeProjectSnapshot | undefined
405
+ if (!before) return null
406
+ const { translate } = await resolveTranslations()
407
+ return {
408
+ actionLabel: translate('staff.audit.timesheets.time_projects.delete', 'Delete time project'),
409
+ resourceKind: 'staff.timesheets.time_project',
410
+ resourceId: before.id,
411
+ tenantId: before.tenantId,
412
+ organizationId: before.organizationId,
413
+ snapshotBefore: before,
414
+ payload: {
415
+ undo: {
416
+ before,
417
+ } satisfies TimeProjectUndoPayload,
418
+ },
419
+ }
420
+ },
421
+ undo: async ({ logEntry, ctx }) => {
422
+ const payload = extractUndoPayload<TimeProjectUndoPayload>(logEntry)
423
+ const before = payload?.before
424
+ if (!before) return
425
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
426
+ let project = await em.findOne(StaffTimeProject, { id: before.id })
427
+ if (!project) {
428
+ project = em.create(StaffTimeProject, {
429
+ id: before.id,
430
+ tenantId: before.tenantId,
431
+ organizationId: before.organizationId,
432
+ name: before.name,
433
+ customerId: before.customerId ?? null,
434
+ code: before.code,
435
+ description: before.description ?? null,
436
+ projectType: before.projectType ?? null,
437
+ status: (before.status ?? 'active') as StaffTimeProjectStatus,
438
+ ownerUserId: before.ownerUserId ?? null,
439
+ costCenter: before.costCenter ?? null,
440
+ startDate: before.startDate ? new Date(before.startDate) : null,
441
+ deletedAt: null,
442
+ createdAt: new Date(),
443
+ updatedAt: new Date(),
444
+ })
445
+ em.persist(project)
446
+ } else {
447
+ project.name = before.name
448
+ project.customerId = before.customerId ?? null
449
+ project.code = before.code
450
+ project.description = before.description ?? null
451
+ project.projectType = before.projectType ?? null
452
+ project.status = (before.status ?? 'active') as StaffTimeProjectStatus
453
+ project.ownerUserId = before.ownerUserId ?? null
454
+ project.costCenter = before.costCenter ?? null
455
+ project.startDate = before.startDate ? new Date(before.startDate) : null
456
+ project.deletedAt = null
457
+ project.updatedAt = new Date()
458
+ }
459
+ await em.flush()
460
+
461
+ await emitCrudUndoSideEffects({
462
+ dataEngine: ctx.container.resolve('dataEngine'),
463
+ action: 'created',
464
+ entity: project,
465
+ identifiers: {
466
+ id: project.id,
467
+ organizationId: project.organizationId,
468
+ tenantId: project.tenantId,
469
+ },
470
+ events: staffTimeProjectCrudEvents,
471
+ indexer: timeProjectCrudIndexer,
472
+ })
473
+ },
474
+ }
475
+
476
+ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssignInput, { timeProjectMemberId: string }> = {
477
+ id: 'staff.timesheets.time_project_members.assign',
478
+ async execute(rawInput, ctx) {
479
+ const parsed = staffTimeProjectMemberAssignSchema.parse(rawInput)
480
+ ensureTenantScope(ctx, parsed.tenantId)
481
+ ensureOrganizationScope(ctx, parsed.organizationId)
482
+
483
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
484
+
485
+ // Validate referenced project and staff member are in-scope before persisting.
486
+ // Without this check a foreign or stale UUID would produce a dangling reference.
487
+ const projectExists = await em.findOne(
488
+ StaffTimeProject,
489
+ { id: parsed.timeProjectId, tenantId: parsed.tenantId, organizationId: parsed.organizationId, deletedAt: null },
490
+ { fields: ['id'] },
491
+ )
492
+ if (!projectExists) {
493
+ const { translate } = await resolveTranslations()
494
+ throw new CrudHttpError(422, {
495
+ error: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),
496
+ fieldErrors: {
497
+ timeProjectId: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),
498
+ },
499
+ })
500
+ }
501
+ const memberExists = await em.findOne(
502
+ StaffTeamMember,
503
+ { id: parsed.staffMemberId, tenantId: parsed.tenantId, organizationId: parsed.organizationId, deletedAt: null },
504
+ { fields: ['id'] },
505
+ )
506
+ if (!memberExists) {
507
+ const { translate } = await resolveTranslations()
508
+ throw new CrudHttpError(422, {
509
+ error: translate('staff.timesheets.errors.staffMemberNotFound', 'Staff member not found or not accessible.'),
510
+ fieldErrors: {
511
+ staffMemberId: translate('staff.timesheets.errors.staffMemberNotFound', 'Staff member not found or not accessible.'),
512
+ },
513
+ })
514
+ }
515
+
516
+ const now = new Date()
517
+ const member = em.create(StaffTimeProjectMember, {
518
+ tenantId: parsed.tenantId,
519
+ organizationId: parsed.organizationId,
520
+ timeProjectId: parsed.timeProjectId,
521
+ staffMemberId: parsed.staffMemberId,
522
+ role: parsed.role ?? null,
523
+ status: parsed.status ?? 'active',
524
+ showInGrid: false,
525
+ assignedStartDate: parsed.assignedStartDate,
526
+ assignedEndDate: parsed.assignedEndDate ?? null,
527
+ createdAt: now,
528
+ updatedAt: now,
529
+ deletedAt: null,
530
+ })
531
+ em.persist(member)
532
+ await em.flush()
533
+
534
+ await emitCrudSideEffects({
535
+ dataEngine: ctx.container.resolve('dataEngine'),
536
+ action: 'created',
537
+ entity: member,
538
+ identifiers: { id: member.id, organizationId: member.organizationId, tenantId: member.tenantId },
539
+ events: staffTimeProjectMemberCrudEvents,
540
+ indexer: timeProjectMemberCrudIndexer,
541
+ })
542
+
543
+ return { timeProjectMemberId: member.id }
544
+ },
545
+ captureAfter: async (_input, result, ctx) => {
546
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
547
+ const snapshot = await loadTimeProjectMemberSnapshot(em, result.timeProjectMemberId)
548
+ if (!snapshot) return null
549
+ return { snapshot }
550
+ },
551
+ buildLog: async ({ result, ctx }) => {
552
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
553
+ const snapshot = await loadTimeProjectMemberSnapshot(em, result.timeProjectMemberId)
554
+ if (!snapshot) return null
555
+ const { translate } = await resolveTranslations()
556
+ return {
557
+ actionLabel: translate('staff.audit.timesheets.time_project_members.assign', 'Assign time project member'),
558
+ resourceKind: 'staff.timesheets.time_project_member',
559
+ resourceId: snapshot.id,
560
+ tenantId: snapshot.tenantId,
561
+ organizationId: snapshot.organizationId,
562
+ snapshotAfter: snapshot,
563
+ payload: {
564
+ undo: {
565
+ after: snapshot,
566
+ } satisfies TimeProjectMemberUndoPayload,
567
+ },
568
+ }
569
+ },
570
+ undo: async ({ logEntry, ctx }) => {
571
+ const payload = extractUndoPayload<TimeProjectMemberUndoPayload>(logEntry)
572
+ const after = payload?.after
573
+ if (!after) return
574
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
575
+ const member = await em.findOne(StaffTimeProjectMember, { id: after.id })
576
+ if (member) {
577
+ member.deletedAt = new Date()
578
+ await em.flush()
579
+
580
+ await emitCrudUndoSideEffects({
581
+ dataEngine: ctx.container.resolve('dataEngine'),
582
+ action: 'deleted',
583
+ entity: member,
584
+ identifiers: { id: member.id, organizationId: member.organizationId, tenantId: member.tenantId },
585
+ events: staffTimeProjectMemberCrudEvents,
586
+ })
587
+ }
588
+ },
589
+ }
590
+
591
+ const unassignTimeProjectMemberCommand: CommandHandler<{ id?: string }, { timeProjectMemberId: string }> = {
592
+ id: 'staff.timesheets.time_project_members.unassign',
593
+ async prepare(input, ctx) {
594
+ const id = input?.id
595
+ if (!id) throw new CrudHttpError(400, { error: 'Time project member id is required.' })
596
+ const em = (ctx.container.resolve('em') as EntityManager)
597
+ const snapshot = await loadTimeProjectMemberSnapshot(em, id)
598
+ if (!snapshot) return {}
599
+ return { before: snapshot }
600
+ },
601
+ async execute(input, ctx) {
602
+ const id = input?.id
603
+ if (!id) throw new CrudHttpError(400, { error: 'Time project member id is required.' })
604
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
605
+ const member = await findOneWithDecryption(
606
+ em,
607
+ StaffTimeProjectMember,
608
+ { id, deletedAt: null },
609
+ undefined,
610
+ { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
611
+ )
612
+ if (!member) throw new CrudHttpError(404, { error: 'Time project member not found.' })
613
+ ensureTenantScope(ctx, member.tenantId)
614
+ ensureOrganizationScope(ctx, member.organizationId)
615
+
616
+ member.deletedAt = new Date()
617
+ member.updatedAt = new Date()
618
+ await em.flush()
619
+
620
+ await emitCrudSideEffects({
621
+ dataEngine: ctx.container.resolve('dataEngine'),
622
+ action: 'deleted',
623
+ entity: member,
624
+ identifiers: { id: member.id, organizationId: member.organizationId, tenantId: member.tenantId },
625
+ events: staffTimeProjectMemberCrudEvents,
626
+ indexer: timeProjectMemberCrudIndexer,
627
+ })
628
+
629
+ return { timeProjectMemberId: member.id }
630
+ },
631
+ buildLog: async ({ snapshots }) => {
632
+ const before = snapshots.before as TimeProjectMemberSnapshot | undefined
633
+ if (!before) return null
634
+ const { translate } = await resolveTranslations()
635
+ return {
636
+ actionLabel: translate('staff.audit.timesheets.time_project_members.unassign', 'Unassign time project member'),
637
+ resourceKind: 'staff.timesheets.time_project_member',
638
+ resourceId: before.id,
639
+ tenantId: before.tenantId,
640
+ organizationId: before.organizationId,
641
+ snapshotBefore: before,
642
+ payload: {
643
+ undo: {
644
+ before,
645
+ } satisfies TimeProjectMemberUndoPayload,
646
+ },
647
+ }
648
+ },
649
+ undo: async ({ logEntry, ctx }) => {
650
+ const payload = extractUndoPayload<TimeProjectMemberUndoPayload>(logEntry)
651
+ const before = payload?.before
652
+ if (!before) return
653
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
654
+ let member = await em.findOne(StaffTimeProjectMember, { id: before.id })
655
+ if (!member) {
656
+ member = em.create(StaffTimeProjectMember, {
657
+ id: before.id,
658
+ tenantId: before.tenantId,
659
+ organizationId: before.organizationId,
660
+ timeProjectId: before.timeProjectId,
661
+ staffMemberId: before.staffMemberId,
662
+ role: before.role ?? null,
663
+ status: (before.status ?? 'active') as StaffTimeProjectMemberStatus,
664
+ showInGrid: before.showInGrid ?? false,
665
+ assignedStartDate: new Date(before.assignedStartDate),
666
+ assignedEndDate: before.assignedEndDate ? new Date(before.assignedEndDate) : null,
667
+ deletedAt: null,
668
+ createdAt: new Date(),
669
+ updatedAt: new Date(),
670
+ })
671
+ em.persist(member)
672
+ } else {
673
+ member.timeProjectId = before.timeProjectId
674
+ member.staffMemberId = before.staffMemberId
675
+ member.role = before.role ?? null
676
+ member.status = (before.status ?? 'active') as StaffTimeProjectMemberStatus
677
+ member.assignedStartDate = new Date(before.assignedStartDate)
678
+ member.assignedEndDate = before.assignedEndDate ? new Date(before.assignedEndDate) : null
679
+ member.deletedAt = null
680
+ member.updatedAt = new Date()
681
+ }
682
+ await em.flush()
683
+
684
+ await emitCrudUndoSideEffects({
685
+ dataEngine: ctx.container.resolve('dataEngine'),
686
+ action: 'created',
687
+ entity: member,
688
+ identifiers: { id: member.id, organizationId: member.organizationId, tenantId: member.tenantId },
689
+ events: staffTimeProjectMemberCrudEvents,
690
+ indexer: timeProjectMemberCrudIndexer,
691
+ })
692
+ },
693
+ }
694
+
695
+ registerCommand(createTimeProjectCommand)
696
+ registerCommand(updateTimeProjectCommand)
697
+ registerCommand(deleteTimeProjectCommand)
698
+ registerCommand(assignTimeProjectMemberCommand)
699
+ registerCommand(unassignTimeProjectMemberCommand)