@open-mercato/core 0.6.4-develop.4210.1.d412061cfe → 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,297 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
5
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { Input } from '@open-mercato/ui/primitives/input'
8
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
9
+ import { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'
10
+
11
+ type ProjectOption = { id: string; name: string; code: string | null }
12
+
13
+ type TimerState = {
14
+ entryId: string | null
15
+ running: boolean
16
+ startedAt: string | null
17
+ projectId: string | null
18
+ }
19
+
20
+ function formatElapsed(startedAt: string): string {
21
+ const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())
22
+ const totalSeconds = Math.floor(elapsed / 1000)
23
+ const hours = Math.floor(totalSeconds / 3600)
24
+ const minutes = Math.floor((totalSeconds % 3600) / 60)
25
+ const seconds = totalSeconds % 60
26
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
27
+ }
28
+
29
+ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingSettings>> = ({
30
+ mode,
31
+ settings = DEFAULT_SETTINGS,
32
+ onSettingsChange,
33
+ refreshToken,
34
+ onRefreshStateChange,
35
+ }) => {
36
+ const t = useT()
37
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
38
+
39
+ const [projects, setProjects] = React.useState<ProjectOption[]>([])
40
+ const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(hydrated.lastProjectId)
41
+ const [notes, setNotes] = React.useState('')
42
+ const [timer, setTimer] = React.useState<TimerState>({ entryId: null, running: false, startedAt: null, projectId: null })
43
+ const [elapsed, setElapsed] = React.useState('00:00:00')
44
+ const [staffMemberId, setStaffMemberId] = React.useState<string | null>(null)
45
+ const [loading, setLoading] = React.useState(true)
46
+ const [actionLoading, setActionLoading] = React.useState(false)
47
+ const [error, setError] = React.useState<string | null>(null)
48
+
49
+ const loadState = React.useCallback(async () => {
50
+ onRefreshStateChange?.(true)
51
+ setLoading(true)
52
+ setError(null)
53
+ try {
54
+ // Load assigned projects
55
+ const assignmentsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
56
+ '/api/staff/timesheets/my-projects?pageSize=100',
57
+ undefined,
58
+ { errorMessage: '', fallback: { items: [] } },
59
+ )
60
+ const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []
61
+ const projectIds = assignmentItems
62
+ .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))
63
+ .filter((id) => id.length > 0)
64
+
65
+ if (projectIds.length > 0) {
66
+ const projectsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
67
+ `/api/staff/timesheets/time-projects?ids=${projectIds.join(',')}&pageSize=100`,
68
+ undefined,
69
+ { errorMessage: '', fallback: { items: [] } },
70
+ )
71
+ const items = Array.isArray(projectsRes.items) ? projectsRes.items : []
72
+ setProjects(items.map((item) => ({
73
+ id: String(item.id ?? ''),
74
+ name: String(item.name ?? ''),
75
+ code: typeof item.code === 'string' ? item.code : null,
76
+ })))
77
+ } else {
78
+ setProjects([])
79
+ }
80
+
81
+ // Check for active timer — look for today's entries with startedAt set and endedAt null
82
+ const selfRes = await readApiResultOrThrow<{ member?: { id: string } | null }>(
83
+ '/api/staff/team-members/self',
84
+ undefined,
85
+ { errorMessage: '', fallback: { member: null } },
86
+ )
87
+ const memberId = selfRes.member?.id ?? null
88
+ setStaffMemberId(memberId)
89
+ if (memberId) {
90
+ const today = new Date().toISOString().slice(0, 10)
91
+ const entriesRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
92
+ `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,
93
+ undefined,
94
+ { errorMessage: '', fallback: { items: [] } },
95
+ )
96
+ const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []
97
+ const running = entries.find((e) => {
98
+ const startedAt = e.started_at ?? e.startedAt
99
+ const endedAt = e.ended_at ?? e.endedAt
100
+ return startedAt != null && endedAt == null
101
+ })
102
+ if (running) {
103
+ setTimer({
104
+ entryId: String(running.id ?? ''),
105
+ running: true,
106
+ startedAt: String(running.started_at ?? running.startedAt ?? ''),
107
+ projectId: String(running.time_project_id ?? running.timeProjectId ?? ''),
108
+ })
109
+ } else {
110
+ setTimer({ entryId: null, running: false, startedAt: null, projectId: null })
111
+ }
112
+ }
113
+ } catch (err) {
114
+ console.error('staff.timesheets.timeReporting.load', err)
115
+ setError(t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state'))
116
+ } finally {
117
+ setLoading(false)
118
+ onRefreshStateChange?.(false)
119
+ }
120
+ }, [onRefreshStateChange, t])
121
+
122
+ React.useEffect(() => {
123
+ void loadState()
124
+ }, [loadState, refreshToken])
125
+
126
+ // Tick elapsed time
127
+ React.useEffect(() => {
128
+ if (!timer.running || !timer.startedAt) return
129
+ const tick = () => setElapsed(formatElapsed(timer.startedAt!))
130
+ tick()
131
+ const interval = setInterval(tick, 1000)
132
+ return () => clearInterval(interval)
133
+ }, [timer.running, timer.startedAt])
134
+
135
+ const handleStart = React.useCallback(async () => {
136
+ if (!selectedProjectId || !staffMemberId) return
137
+ if (timer.running) return
138
+ setActionLoading(true)
139
+ try {
140
+ const today = new Date().toISOString().slice(0, 10)
141
+ // Create entry + start timer
142
+ const createRes = await apiCall<Record<string, unknown>>('/api/staff/timesheets/time-entries', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({
146
+ staffMemberId,
147
+ date: today,
148
+ timeProjectId: selectedProjectId,
149
+ durationMinutes: 0,
150
+ notes: notes.trim() || null,
151
+ source: 'timer',
152
+ }),
153
+ })
154
+ if (!createRes.ok) throw new Error('Failed to create entry')
155
+ const body = createRes.result as Record<string, unknown> | null
156
+ const entryId = String(body?.id ?? (body?.item as Record<string, unknown> | undefined)?.id ?? '')
157
+ if (!entryId) throw new Error('Failed to extract entry ID')
158
+
159
+ await apiCall(`/api/staff/timesheets/time-entries/${entryId}/timer-start`, { method: 'POST' })
160
+
161
+ onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })
162
+ await loadState()
163
+ } catch (err) {
164
+ console.error('staff.timesheets.timeReporting.start', err)
165
+ setError(t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer'))
166
+ } finally {
167
+ setActionLoading(false)
168
+ }
169
+ }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, t])
170
+
171
+ const handleStop = React.useCallback(async () => {
172
+ if (!timer.entryId) return
173
+ setActionLoading(true)
174
+ try {
175
+ await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' })
176
+ await loadState()
177
+ } catch (err) {
178
+ console.error('staff.timesheets.timeReporting.stop', err)
179
+ setError(t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer'))
180
+ } finally {
181
+ setActionLoading(false)
182
+ }
183
+ }, [timer.entryId, loadState, t])
184
+
185
+ if (mode === 'settings') {
186
+ return (
187
+ <div className="space-y-2 text-sm">
188
+ <p className="text-muted-foreground">
189
+ {t('staff.timesheets.widgets.timeReporting.settings.description', 'No additional settings. Select a project and start tracking from the widget.')}
190
+ </p>
191
+ </div>
192
+ )
193
+ }
194
+
195
+ if (loading) {
196
+ return (
197
+ <div className="flex h-full items-center justify-center py-8">
198
+ <p className="text-sm text-muted-foreground">{t('staff.timesheets.widgets.timeReporting.loading', 'Loading...')}</p>
199
+ </div>
200
+ )
201
+ }
202
+
203
+ if (error) {
204
+ return (
205
+ <div className="flex h-full items-center justify-center py-8">
206
+ <p className="text-sm text-destructive">{error}</p>
207
+ </div>
208
+ )
209
+ }
210
+
211
+ if (projects.length === 0) {
212
+ return (
213
+ <div className="flex h-full items-center justify-center py-8">
214
+ <p className="text-sm text-muted-foreground">
215
+ {t('staff.timesheets.widgets.timeReporting.noProjects', 'No projects assigned.')}
216
+ </p>
217
+ </div>
218
+ )
219
+ }
220
+
221
+ // Timer is running
222
+ if (timer.running) {
223
+ const runningProject = projects.find((p) => p.id === timer.projectId)
224
+ return (
225
+ <div className="space-y-3">
226
+ <div className="text-sm text-muted-foreground">
227
+ {runningProject?.name ?? t('staff.timesheets.widgets.timeReporting.unknownProject', 'Unknown project')}
228
+ </div>
229
+ <div className="text-center">
230
+ <p className="font-mono text-3xl font-bold tabular-nums">{elapsed}</p>
231
+ <p className="mt-1 text-xs text-muted-foreground">
232
+ {t('staff.timesheets.widgets.timeReporting.running', 'Timer running')}
233
+ </p>
234
+ </div>
235
+ <Button
236
+ type="button"
237
+ variant="destructive"
238
+ className="w-full"
239
+ onClick={handleStop}
240
+ disabled={actionLoading}
241
+ >
242
+ {t('staff.timesheets.widgets.timeReporting.stop', 'Stop Timer')}
243
+ </Button>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ // Timer not running — show start form
249
+ return (
250
+ <div className="space-y-3">
251
+ <div className="space-y-1.5">
252
+ <label className="text-xs font-medium text-muted-foreground" htmlFor="timer-project">
253
+ {t('staff.timesheets.widgets.timeReporting.project', 'Project')}
254
+ </label>
255
+ <select
256
+ id="timer-project"
257
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm"
258
+ value={selectedProjectId ?? ''}
259
+ onChange={(e) => setSelectedProjectId(e.target.value || null)}
260
+ >
261
+ <option value="">{t('staff.timesheets.widgets.timeReporting.selectProject', 'Select project...')}</option>
262
+ {projects.map((project) => (
263
+ <option key={project.id} value={project.id}>
264
+ {project.name}{project.code ? ` (${project.code})` : ''}
265
+ </option>
266
+ ))}
267
+ </select>
268
+ </div>
269
+ <div className="space-y-1.5">
270
+ <label className="text-xs font-medium text-muted-foreground" htmlFor="timer-notes">
271
+ {t('staff.timesheets.widgets.timeReporting.taskNote', 'Task / Note')}
272
+ </label>
273
+ <Input
274
+ id="timer-notes"
275
+ value={notes}
276
+ onChange={(e) => setNotes(e.target.value)}
277
+ placeholder={t('staff.timesheets.widgets.timeReporting.notesPlaceholder', 'What are you working on?')}
278
+ />
279
+ </div>
280
+ <div className="text-center">
281
+ <p className="text-sm text-muted-foreground">
282
+ {t('staff.timesheets.widgets.timeReporting.notRunning', 'Not running')}
283
+ </p>
284
+ </div>
285
+ <Button
286
+ type="button"
287
+ className="w-full"
288
+ onClick={handleStart}
289
+ disabled={actionLoading || !selectedProjectId}
290
+ >
291
+ {t('staff.timesheets.widgets.timeReporting.start', 'Start Timer')}
292
+ </Button>
293
+ </div>
294
+ )
295
+ }
296
+
297
+ export default TimeReportingWidget
@@ -0,0 +1,25 @@
1
+ import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
2
+ import { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'
3
+
4
+ const TimeReportingWidget = lazyDashboardWidget(() => import('./widget.client'))
5
+
6
+ const widget: DashboardWidgetModule<TimeReportingSettings> = {
7
+ metadata: {
8
+ id: 'staff.timesheets.timeReporting',
9
+ title: 'Time Reporting',
10
+ description: 'Quick start/stop timer for the current work item',
11
+ features: ['dashboards.view', 'staff.timesheets.manage_own'],
12
+ defaultSize: 'sm',
13
+ defaultEnabled: false,
14
+ defaultSettings: DEFAULT_SETTINGS,
15
+ tags: ['staff', 'timesheets', 'timer'],
16
+ category: 'productivity',
17
+ icon: 'timer',
18
+ supportsRefresh: true,
19
+ },
20
+ Widget: TimeReportingWidget,
21
+ hydrateSettings,
22
+ dehydrateSettings: (s) => ({ lastProjectId: s.lastProjectId }),
23
+ }
24
+
25
+ export default widget
@@ -0,0 +1,161 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
7
+ import { ProjectColorDot } from '../../../lib/timesheets-ui/ProjectColorDot'
8
+
9
+ type TimerState = {
10
+ entryId: string
11
+ projectName: string
12
+ projectColor: string | null
13
+ startedAt: string
14
+ }
15
+
16
+ const STORAGE_KEY = 'om:timesheets:activeTimer'
17
+
18
+ function saveToSession(state: TimerState | null) {
19
+ try {
20
+ if (state) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))
21
+ else sessionStorage.removeItem(STORAGE_KEY)
22
+ } catch { /* private browsing */ }
23
+ }
24
+
25
+ function loadFromSession(): TimerState | null {
26
+ try {
27
+ const raw = sessionStorage.getItem(STORAGE_KEY)
28
+ if (!raw) return null
29
+ const parsed = JSON.parse(raw)
30
+ if (parsed?.startedAt && parsed?.entryId) return parsed as TimerState
31
+ } catch { /* corrupt or private browsing */ }
32
+ return null
33
+ }
34
+
35
+ function formatElapsed(seconds: number): string {
36
+ const h = Math.floor(seconds / 3600)
37
+ const m = Math.floor((seconds % 3600) / 60)
38
+ const s = seconds % 60
39
+ return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
40
+ }
41
+
42
+ function getToday(): string {
43
+ const now = new Date()
44
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
45
+ }
46
+
47
+ function TimerSidebarIndicator() {
48
+ const t = useT()
49
+ // Initialise from sessionStorage so the indicator doesn't flash away on navigation
50
+ const [timer, setTimer] = React.useState<TimerState | null>(loadFromSession)
51
+ const [elapsed, setElapsed] = React.useState(0)
52
+ const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(null)
53
+
54
+ // Wrap setTimer to also persist to sessionStorage
55
+ const updateTimer = React.useCallback((next: TimerState | null) => {
56
+ setTimer(next)
57
+ saveToSession(next)
58
+ }, [])
59
+
60
+ const pollTimer = React.useCallback(async () => {
61
+ try {
62
+ const selfRes = await apiCall<{ member?: { id: string } | null }>('/api/staff/team-members/self')
63
+ const memberId = selfRes.result?.member?.id
64
+ if (!memberId) { updateTimer(null); return }
65
+
66
+ const today = getToday()
67
+ const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(
68
+ `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=50`,
69
+ )
70
+ const items = (res.result?.items ?? []) as Array<Record<string, unknown>>
71
+ const active = items.find((e) => e.started_at && !e.ended_at)
72
+
73
+ if (!active) {
74
+ updateTimer(null)
75
+ return
76
+ }
77
+
78
+ const projectId = String(active.time_project_id ?? '')
79
+ let projectName = ''
80
+ let projectColor: string | null = null
81
+ if (projectId) {
82
+ const projRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(
83
+ `/api/staff/timesheets/time-projects?ids=${projectId}&pageSize=1`,
84
+ )
85
+ const proj = (projRes.result?.items ?? [])[0]
86
+ if (proj) {
87
+ projectName = String(proj.name ?? '')
88
+ projectColor = typeof proj.color === 'string' ? proj.color : null
89
+ }
90
+ }
91
+
92
+ updateTimer({
93
+ entryId: String(active.id ?? ''),
94
+ projectName,
95
+ projectColor,
96
+ startedAt: String(active.started_at ?? ''),
97
+ })
98
+ } catch {
99
+ // Silent — don't crash the sidebar
100
+ }
101
+ }, [updateTimer])
102
+
103
+ // Poll for active timer on mount + every 30s
104
+ React.useEffect(() => {
105
+ void pollTimer()
106
+ const poll = setInterval(() => { void pollTimer() }, 30000)
107
+ return () => clearInterval(poll)
108
+ }, [pollTimer])
109
+
110
+ // Tick the elapsed counter locally every second
111
+ React.useEffect(() => {
112
+ if (!timer) {
113
+ if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }
114
+ setElapsed(0)
115
+ return
116
+ }
117
+
118
+ const startTime = new Date(timer.startedAt).getTime()
119
+ const calcElapsed = () => Math.max(0, Math.floor((Date.now() - startTime) / 1000))
120
+ setElapsed(calcElapsed())
121
+
122
+ intervalRef.current = setInterval(() => setElapsed(calcElapsed()), 1000)
123
+ return () => {
124
+ if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }
125
+ }
126
+ }, [timer])
127
+
128
+ if (!timer) return null
129
+
130
+ return (
131
+ <a
132
+ href="/backend/staff/timesheets"
133
+ className="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs hover:bg-muted transition-colors cursor-pointer"
134
+ title={t('staff.timesheets.sidebar.timerRunning', 'Timer running — click to view')}
135
+ >
136
+ <span className="relative flex h-2.5 w-2.5 shrink-0">
137
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
138
+ <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-red-500" />
139
+ </span>
140
+ {timer.projectName ? (
141
+ <span className="inline-flex items-center gap-1 truncate">
142
+ <ProjectColorDot colorKey={timer.projectColor} projectName={timer.projectName} size="xs" />
143
+ <span className="truncate">{timer.projectName}</span>
144
+ </span>
145
+ ) : null}
146
+ <span className="ml-auto font-mono tabular-nums shrink-0">{formatElapsed(elapsed)}</span>
147
+ </a>
148
+ )
149
+ }
150
+
151
+ const widget: InjectionWidgetModule = {
152
+ metadata: {
153
+ id: 'staff.injection.timer-sidebar-indicator',
154
+ title: 'Active timer indicator',
155
+ description: 'Shows a pulsing indicator in the sidebar when a timesheet timer is running.',
156
+ features: ['staff.timesheets.manage_own'],
157
+ },
158
+ Widget: TimerSidebarIndicator,
159
+ }
160
+
161
+ export default widget
@@ -0,0 +1,10 @@
1
+ import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
2
+
3
+ export const injectionTable: ModuleInjectionTable = {
4
+ 'backend:sidebar:nav:footer': {
5
+ widgetId: 'staff.injection.timer-sidebar-indicator',
6
+ priority: 90,
7
+ },
8
+ }
9
+
10
+ export default injectionTable
@@ -128,21 +128,25 @@ export async function POST(request: Request) {
128
128
  scope,
129
129
  )
130
130
 
131
- if (existingMapping) {
132
- existingMapping.mapping = parsedPayload.data.mapping
133
- } else {
134
- em.persist(em.create(SyncMapping, {
135
- integrationId: 'sync_excel',
136
- entityType: parsedPayload.data.entityType,
137
- mapping: parsedPayload.data.mapping,
138
- organizationId: scope.organizationId,
139
- tenantId: scope.tenantId,
140
- }))
141
- }
142
-
143
- await credentialsService.save('sync_excel', {}, scope)
144
- await integrationStateService.upsert('sync_excel', { isEnabled: true }, scope)
145
- await em.flush()
131
+ // Persist the mapping, credentials, and integration-state config atomically.
132
+ // credentialsService / integrationStateService are request-scoped and share
133
+ // this request `em`, so a single transaction covers all of their writes.
134
+ await em.transactional(async () => {
135
+ if (existingMapping) {
136
+ existingMapping.mapping = parsedPayload.data.mapping
137
+ } else {
138
+ em.persist(em.create(SyncMapping, {
139
+ integrationId: 'sync_excel',
140
+ entityType: parsedPayload.data.entityType,
141
+ mapping: parsedPayload.data.mapping,
142
+ organizationId: scope.organizationId,
143
+ tenantId: scope.tenantId,
144
+ }))
145
+ }
146
+
147
+ await credentialsService.save('sync_excel', {}, scope)
148
+ await integrationStateService.upsert('sync_excel', { isEnabled: true }, scope)
149
+ })
146
150
 
147
151
  const { run, progressJob } = await startDataSyncRun({
148
152
  syncRunService,
@@ -171,9 +175,10 @@ export async function POST(request: Request) {
171
175
  },
172
176
  })
173
177
 
174
- upload.syncRunId = run.id
175
- upload.status = 'importing'
176
- await em.flush()
178
+ await em.transactional(async () => {
179
+ upload.syncRunId = run.id
180
+ upload.status = 'importing'
181
+ })
177
182
 
178
183
  return NextResponse.json(syncExcelImportResponseSchema.parse({
179
184
  runId: run.id,
@@ -80,39 +80,56 @@ const saveTranslationCommand: CommandHandler<SaveInput, { rowId: string }> = {
80
80
 
81
81
  async execute(input, ctx) {
82
82
  const db = resolveDb(ctx) as any
83
- const existing = await db
84
- .selectFrom('entity_translations')
85
- .select(['id'])
86
- .where('entity_type', '=', input.entityType)
87
- .where('entity_id', '=', input.entityId)
88
- .where(sql<boolean>`tenant_id is not distinct from ${input.tenantId}`)
89
- .where(sql<boolean>`organization_id is not distinct from ${input.organizationId}`)
90
- .executeTakeFirst() as { id: string } | undefined
91
83
 
92
- if (existing) {
93
- await db
94
- .updateTable('entity_translations')
95
- .set({
96
- translations: sql`${JSON.stringify(input.translations)}::jsonb`,
97
- updated_at: sql`now()`,
98
- } as any)
99
- .where('id', '=', existing.id)
100
- .execute()
101
- } else {
102
- await db
103
- .insertInto('entity_translations')
104
- .values({
105
- entity_type: input.entityType,
106
- entity_id: input.entityId,
107
- organization_id: input.organizationId,
108
- tenant_id: input.tenantId,
109
- translations: sql`${JSON.stringify(input.translations)}::jsonb`,
110
- created_at: sql`now()`,
111
- updated_at: sql`now()`,
112
- } as any)
113
- .execute()
114
- }
84
+ // Run the lookup + upsert + id read in one transaction so a concurrent
85
+ // writer cannot slip between the existence check and the insert/update.
86
+ const rowId = await db.transaction().execute(async (trx: any) => {
87
+ const existing = await trx
88
+ .selectFrom('entity_translations')
89
+ .select(['id'])
90
+ .where('entity_type', '=', input.entityType)
91
+ .where('entity_id', '=', input.entityId)
92
+ .where(sql<boolean>`tenant_id is not distinct from ${input.tenantId}`)
93
+ .where(sql<boolean>`organization_id is not distinct from ${input.organizationId}`)
94
+ .executeTakeFirst() as { id: string } | undefined
115
95
 
96
+ if (existing) {
97
+ await trx
98
+ .updateTable('entity_translations')
99
+ .set({
100
+ translations: sql`${JSON.stringify(input.translations)}::jsonb`,
101
+ updated_at: sql`now()`,
102
+ } as any)
103
+ .where('id', '=', existing.id)
104
+ .execute()
105
+ } else {
106
+ await trx
107
+ .insertInto('entity_translations')
108
+ .values({
109
+ entity_type: input.entityType,
110
+ entity_id: input.entityId,
111
+ organization_id: input.organizationId,
112
+ tenant_id: input.tenantId,
113
+ translations: sql`${JSON.stringify(input.translations)}::jsonb`,
114
+ created_at: sql`now()`,
115
+ updated_at: sql`now()`,
116
+ } as any)
117
+ .execute()
118
+ }
119
+
120
+ const saved = await trx
121
+ .selectFrom('entity_translations')
122
+ .select(['id'])
123
+ .where('entity_type', '=', input.entityType)
124
+ .where('entity_id', '=', input.entityId)
125
+ .where(sql<boolean>`tenant_id is not distinct from ${input.tenantId}`)
126
+ .where(sql<boolean>`organization_id is not distinct from ${input.organizationId}`)
127
+ .executeTakeFirst() as { id: string } | undefined
128
+
129
+ return saved?.id ?? ''
130
+ })
131
+
132
+ // Emit AFTER the write commits — never announce a change that rolled back.
116
133
  await emitTranslationsEvent('translations.translation.updated', {
117
134
  entityType: input.entityType,
118
135
  entityId: input.entityId,
@@ -120,16 +137,7 @@ const saveTranslationCommand: CommandHandler<SaveInput, { rowId: string }> = {
120
137
  tenantId: input.tenantId,
121
138
  }, { persistent: true }).catch(() => undefined)
122
139
 
123
- const saved = await db
124
- .selectFrom('entity_translations')
125
- .select(['id'])
126
- .where('entity_type', '=', input.entityType)
127
- .where('entity_id', '=', input.entityId)
128
- .where(sql<boolean>`tenant_id is not distinct from ${input.tenantId}`)
129
- .where(sql<boolean>`organization_id is not distinct from ${input.organizationId}`)
130
- .executeTakeFirst() as { id: string } | undefined
131
-
132
- return { rowId: saved?.id ?? '' }
140
+ return { rowId }
133
141
  },
134
142
 
135
143
  async captureAfter(input, _result, ctx) {