@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,899 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
8
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
+ import { LoadingMessage } from '@open-mercato/ui/backend/detail'
10
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
11
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
12
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
13
+ import { ChevronLeft, ChevronRight, X } from 'lucide-react'
14
+ import Link from 'next/link'
15
+ import { ViewSwitcher } from '../../../lib/timesheets-ui/ViewSwitcher'
16
+ import { CalendarPicker } from '../../../lib/timesheets-ui/CalendarPicker'
17
+ import { ListView } from '../../../lib/timesheets-ui/ListView'
18
+ import { TimerBar } from '../../../lib/timesheets-ui/TimerBar'
19
+ import { AddRowDropdown } from '../../../lib/timesheets-ui/AddRowDropdown'
20
+ import { CreateProjectDialog } from '../../../lib/timesheets-ui/CreateProjectDialog'
21
+ import { ProjectColorDot } from '../../../lib/timesheets-ui/ProjectColorDot'
22
+
23
+ // --- Types ---
24
+
25
+ type ProjectRow = { id: string; name: string; code: string | null; color?: string | null }
26
+ type CellEntry = { id?: string; minutes: number }
27
+ type EntryMap = Record<string, Record<string, CellEntry[]>>
28
+ type DirtyMap = Record<string, Record<string, CellEntry>>
29
+ type RawTextMap = Record<string, Record<string, string>>
30
+ type ViewMode = 'weekly' | 'monthly'
31
+ type ViewType = 'timesheet' | 'list'
32
+
33
+ type RawTimeEntry = Record<string, unknown>
34
+
35
+ // --- Date Helpers ---
36
+
37
+ function getMonday(date: Date): Date {
38
+ const d = new Date(date)
39
+ const day = d.getDay()
40
+ const diff = day === 0 ? -6 : 1 - day
41
+ d.setDate(d.getDate() + diff)
42
+ d.setHours(0, 0, 0, 0)
43
+ return d
44
+ }
45
+
46
+ function getSunday(monday: Date): Date {
47
+ const d = new Date(monday)
48
+ d.setDate(d.getDate() + 6)
49
+ return d
50
+ }
51
+
52
+ function getWeekNumber(date: Date): number {
53
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
54
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7))
55
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
56
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
57
+ }
58
+
59
+ function formatDateKey(date: Date): string {
60
+ const y = date.getFullYear()
61
+ const m = String(date.getMonth() + 1).padStart(2, '0')
62
+ const d = String(date.getDate()).padStart(2, '0')
63
+ return `${y}-${m}-${d}`
64
+ }
65
+
66
+ function formatDateKeyFromParts(year: number, month: number, day: number): string {
67
+ const m = String(month + 1).padStart(2, '0')
68
+ const d = String(day).padStart(2, '0')
69
+ return `${year}-${m}-${d}`
70
+ }
71
+
72
+ function getDaysInMonth(year: number, month: number): number {
73
+ return new Date(year, month + 1, 0).getDate()
74
+ }
75
+
76
+ function isWeekendDay(date: Date): boolean {
77
+ const d = date.getDay()
78
+ return d === 0 || d === 6
79
+ }
80
+
81
+ function minutesToDecimal(minutes: number): string {
82
+ if (minutes === 0) return ''
83
+ const hours = minutes / 60
84
+ return hours % 1 === 0 ? String(hours) : hours.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')
85
+ }
86
+
87
+ function decimalToMinutes(value: string): number {
88
+ const trimmed = value.trim()
89
+ if (!trimmed) return 0
90
+ const num = parseFloat(trimmed)
91
+ if (isNaN(num) || num < 0) return 0
92
+ return Math.min(Math.round(num * 60), 1440)
93
+ }
94
+
95
+ function getLocalizedDayName(date: Date): string {
96
+ return date.toLocaleDateString(undefined, { weekday: 'short' })
97
+ }
98
+
99
+ // --- Derived date ranges ---
100
+
101
+ function getWeekDays(weekStart: Date): Date[] {
102
+ return Array.from({ length: 7 }, (_, i) => {
103
+ const d = new Date(weekStart)
104
+ d.setDate(d.getDate() + i)
105
+ return d
106
+ })
107
+ }
108
+
109
+ function getMonthDays(year: number, month: number): Date[] {
110
+ const count = getDaysInMonth(year, month)
111
+ return Array.from({ length: count }, (_, i) => new Date(year, month, i + 1))
112
+ }
113
+
114
+ // --- Week label ---
115
+
116
+ function formatWeekLabel(weekStart: Date): string {
117
+ const weekEnd = getSunday(weekStart)
118
+ const weekNum = getWeekNumber(weekStart)
119
+ const startDay = weekStart.getDate()
120
+ const endDay = weekEnd.getDate()
121
+ const startMonth = weekStart.toLocaleString(undefined, { month: 'short' })
122
+ const endMonth = weekEnd.toLocaleString(undefined, { month: 'short' })
123
+ const year = weekStart.getFullYear()
124
+
125
+ const dateRange = weekStart.getMonth() === weekEnd.getMonth()
126
+ ? `${startDay} - ${endDay} ${startMonth} ${year}`
127
+ : `${startDay} ${startMonth} - ${endDay} ${endMonth} ${year}`
128
+
129
+ return `${dateRange} \u00b7 W${weekNum}`
130
+ }
131
+
132
+ function formatMonthLabel(year: number, month: number): string {
133
+ const date = new Date(year, month, 1)
134
+ return date.toLocaleString(undefined, { month: 'long', year: 'numeric' })
135
+ }
136
+
137
+ // --- Component ---
138
+
139
+ export default function MyTimesheetsPage() {
140
+ const t = useT()
141
+ const scopeVersion = useOrganizationScopeVersion()
142
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
143
+
144
+ const now = new Date()
145
+ const [viewMode, setViewMode] = React.useState<ViewMode>('weekly')
146
+ const [viewType, setViewType] = React.useState<ViewType>('timesheet')
147
+ const [weekStart, setWeekStart] = React.useState<Date>(() => getMonday(now))
148
+ const [monthYear, setMonthYear] = React.useState(now.getFullYear())
149
+ const [monthIndex, setMonthIndex] = React.useState(now.getMonth())
150
+
151
+ const [projects, setProjects] = React.useState<ProjectRow[]>([])
152
+ const [entries, setEntries] = React.useState<EntryMap>({})
153
+ const [rawEntries, setRawEntries] = React.useState<RawTimeEntry[]>([])
154
+ const [dirty, setDirty] = React.useState<DirtyMap>({})
155
+ const [rawText, setRawText] = React.useState<RawTextMap>({})
156
+ const [staffMemberId, setStaffMemberId] = React.useState<string | null>(null)
157
+ const [staffMemberMissing, setStaffMemberMissing] = React.useState(false)
158
+ const [isInitialLoad, setIsInitialLoad] = React.useState(true)
159
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
160
+ const [isSaving, setIsSaving] = React.useState(false)
161
+ const [canManageProjects, setCanManageProjects] = React.useState(false)
162
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
163
+ const [allAssignedProjects, setAllAssignedProjects] = React.useState<ProjectRow[]>([])
164
+
165
+ const mutationContextId = React.useMemo(
166
+ () => (staffMemberId ? `staff-timesheets:${staffMemberId}` : 'staff-timesheets:pending'),
167
+ [staffMemberId],
168
+ )
169
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
170
+ formId: string
171
+ resourceKind: string
172
+ resourceId?: string
173
+ staffMemberId: string | null
174
+ retryLastMutation: () => Promise<boolean>
175
+ }>({
176
+ contextId: mutationContextId,
177
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
178
+ })
179
+
180
+ // --- Feature check ---
181
+ React.useEffect(() => {
182
+ let cancelled = false
183
+ void (async () => {
184
+ try {
185
+ const res = await apiCall<{ ok: boolean; granted: string[] }>('/api/auth/feature-check', {
186
+ method: 'POST',
187
+ headers: { 'content-type': 'application/json' },
188
+ body: JSON.stringify({ features: ['staff.timesheets.projects.manage'] }),
189
+ })
190
+ if (!cancelled) {
191
+ setCanManageProjects(new Set(res.result?.granted ?? []).has('staff.timesheets.projects.manage'))
192
+ }
193
+ } catch {
194
+ // default: no manage access
195
+ }
196
+ })()
197
+ return () => { cancelled = true }
198
+ }, [])
199
+
200
+ // --- Computed days for current view ---
201
+ const visibleDays = React.useMemo(() => {
202
+ if (viewMode === 'weekly') return getWeekDays(weekStart)
203
+ return getMonthDays(monthYear, monthIndex)
204
+ }, [viewMode, weekStart, monthYear, monthIndex])
205
+
206
+ const dateRange = React.useMemo(() => {
207
+ if (viewMode === 'weekly') {
208
+ return { from: formatDateKey(weekStart), to: formatDateKey(getSunday(weekStart)) }
209
+ }
210
+ const daysInMonth = getDaysInMonth(monthYear, monthIndex)
211
+ return {
212
+ from: formatDateKeyFromParts(monthYear, monthIndex, 1),
213
+ to: formatDateKeyFromParts(monthYear, monthIndex, daysInMonth),
214
+ }
215
+ }, [viewMode, weekStart, monthYear, monthIndex])
216
+
217
+ // --- Navigation ---
218
+ const goToPrev = React.useCallback(() => {
219
+ if (viewMode === 'weekly') {
220
+ setWeekStart((prev) => {
221
+ const d = new Date(prev)
222
+ d.setDate(d.getDate() - 7)
223
+ return d
224
+ })
225
+ } else {
226
+ setMonthIndex((prev) => {
227
+ if (prev === 0) { setMonthYear((y) => y - 1); return 11 }
228
+ return prev - 1
229
+ })
230
+ }
231
+ }, [viewMode])
232
+
233
+ const goToNext = React.useCallback(() => {
234
+ if (viewMode === 'weekly') {
235
+ setWeekStart((prev) => {
236
+ const d = new Date(prev)
237
+ d.setDate(d.getDate() + 7)
238
+ return d
239
+ })
240
+ } else {
241
+ setMonthIndex((prev) => {
242
+ if (prev === 11) { setMonthYear((y) => y + 1); return 0 }
243
+ return prev + 1
244
+ })
245
+ }
246
+ }, [viewMode])
247
+
248
+ const navigationLabel = React.useMemo(() => {
249
+ if (viewMode === 'weekly') return formatWeekLabel(weekStart)
250
+ return formatMonthLabel(monthYear, monthIndex)
251
+ }, [viewMode, weekStart, monthYear, monthIndex])
252
+
253
+ // --- Data loading ---
254
+ const isInitialLoadRef = React.useRef(true)
255
+
256
+ const loadData = React.useCallback(async () => {
257
+ if (!isInitialLoadRef.current) setIsRefreshing(true)
258
+ try {
259
+ const selfRes = await readApiResultOrThrow<{ member?: { id: string; displayName: string } | null }>(
260
+ '/api/staff/team-members/self',
261
+ undefined,
262
+ { errorMessage: t('staff.timesheets.my.errors.load', 'Failed to load timesheets.'), fallback: { member: null } },
263
+ )
264
+ const memberId = selfRes.member?.id ?? null
265
+ setStaffMemberId(memberId)
266
+ if (!memberId) {
267
+ setStaffMemberMissing(true)
268
+ setProjects([])
269
+ setEntries({})
270
+ setRawEntries([])
271
+ setIsInitialLoad(false)
272
+ setIsRefreshing(false)
273
+ return
274
+ }
275
+ setStaffMemberMissing(false)
276
+
277
+ const assignmentsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
278
+ '/api/staff/timesheets/my-projects?pageSize=100',
279
+ undefined,
280
+ { errorMessage: t('staff.timesheets.my.errors.load', 'Failed to load timesheets.'), fallback: { items: [] } },
281
+ )
282
+ const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []
283
+ const assignedProjectIds = assignmentItems
284
+ .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))
285
+ .filter((id) => id.length > 0)
286
+ const visibleProjectIdSet = new Set(
287
+ assignmentItems
288
+ .filter((item) => item.show_in_grid === true || item.showInGrid === true)
289
+ .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))
290
+ .filter((id) => id.length > 0),
291
+ )
292
+
293
+ const [projectsRes, entriesRes] = await Promise.all([
294
+ assignedProjectIds.length > 0
295
+ ? readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
296
+ `/api/staff/timesheets/time-projects?ids=${assignedProjectIds.join(',')}&pageSize=100`,
297
+ undefined,
298
+ { errorMessage: t('staff.timesheets.my.errors.load', 'Failed to load timesheets.'), fallback: { items: [] } },
299
+ )
300
+ : Promise.resolve({ items: [] as Array<Record<string, unknown>> }),
301
+ readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
302
+ `/api/staff/timesheets/time-entries?pageSize=100&staffMemberId=${memberId}&from=${dateRange.from}&to=${dateRange.to}`,
303
+ undefined,
304
+ { errorMessage: t('staff.timesheets.my.errors.load', 'Failed to load timesheets.'), fallback: { items: [] } },
305
+ ),
306
+ ])
307
+
308
+ const projectItems = Array.isArray(projectsRes.items) ? projectsRes.items : []
309
+ const mappedProjects = projectItems.map((item) => ({
310
+ id: String(item.id ?? ''),
311
+ name: String(item.name ?? ''),
312
+ code: typeof item.code === 'string' ? item.code : null,
313
+ color: typeof item.color === 'string' ? item.color : null,
314
+ }))
315
+ setAllAssignedProjects(mappedProjects)
316
+ const visibleProjects = mappedProjects.filter((p) => visibleProjectIdSet.has(p.id))
317
+ setProjects(visibleProjects)
318
+
319
+ const entryItems = Array.isArray(entriesRes.items) ? entriesRes.items : []
320
+ setRawEntries(entryItems)
321
+ const map: EntryMap = {}
322
+ for (const item of entryItems) {
323
+ const projectId = String(item.time_project_id ?? item.timeProjectId ?? '')
324
+ const rawDate = String(item.date ?? '')
325
+ const dateKey = rawDate.slice(0, 10)
326
+ const minutes = typeof item.duration_minutes === 'number'
327
+ ? item.duration_minutes
328
+ : typeof item.durationMinutes === 'number'
329
+ ? item.durationMinutes
330
+ : 0
331
+ const entryId = String(item.id ?? '')
332
+ if (!map[projectId]) map[projectId] = {}
333
+ if (!map[projectId][dateKey]) map[projectId][dateKey] = []
334
+ map[projectId][dateKey].push({ id: entryId || undefined, minutes })
335
+ }
336
+ setEntries(map)
337
+ setDirty({})
338
+ setRawText({})
339
+ } catch (error) {
340
+ console.error('staff.timesheets.my.load', error)
341
+ flash(t('staff.timesheets.my.errors.load', 'Failed to load timesheets.'), 'error')
342
+ } finally {
343
+ isInitialLoadRef.current = false
344
+ setIsInitialLoad(false)
345
+ setIsRefreshing(false)
346
+ }
347
+ }, [dateRange.from, dateRange.to, t])
348
+
349
+ React.useEffect(() => {
350
+ void loadData()
351
+ }, [loadData, scopeVersion])
352
+
353
+ // --- Cell handlers ---
354
+ const handleCellChange = React.useCallback((projectId: string, dateKey: string, value: string) => {
355
+ // Only allow digits, dots, and commas (decimal separators)
356
+ const sanitized = value.replace(/[^0-9.,]/g, '')
357
+ setRawText((prev) => {
358
+ const projectTexts = { ...(prev[projectId] ?? {}) }
359
+ projectTexts[dateKey] = sanitized
360
+ return { ...prev, [projectId]: projectTexts }
361
+ })
362
+ }, [])
363
+
364
+ const handleCellBlur = React.useCallback((projectId: string, dateKey: string) => {
365
+ const text = rawText[projectId]?.[dateKey]
366
+ if (text === undefined) return
367
+ const minutes = decimalToMinutes(text)
368
+ const cellEntries = entries[projectId]?.[dateKey] ?? []
369
+ const existingMinutes = cellEntries.reduce((sum, e) => sum + e.minutes, 0)
370
+
371
+ // Only mark dirty if the value actually changed
372
+ if (minutes !== existingMinutes || cellEntries.length > 0) {
373
+ setDirty((prev) => {
374
+ const projectEntries: Record<string, CellEntry> = { ...(prev[projectId] ?? {}) }
375
+ const firstId = cellEntries[0]?.id
376
+ projectEntries[dateKey] = { id: firstId, minutes }
377
+ return { ...prev, [projectId]: projectEntries }
378
+ })
379
+ }
380
+ setRawText((prev) => {
381
+ const projectTexts = { ...(prev[projectId] ?? {}) }
382
+ delete projectTexts[dateKey]
383
+ const hasKeys = Object.keys(projectTexts).length > 0
384
+ if (!hasKeys) {
385
+ const next = { ...prev }
386
+ delete next[projectId]
387
+ return next
388
+ }
389
+ return { ...prev, [projectId]: projectTexts }
390
+ })
391
+ }, [rawText, entries])
392
+
393
+ const getCellValue = React.useCallback((projectId: string, dateKey: string): number => {
394
+ const dirtyCell = dirty[projectId]?.[dateKey] as CellEntry | undefined
395
+ if (dirtyCell !== undefined) return dirtyCell.minutes
396
+ const cellEntries = entries[projectId]?.[dateKey] ?? []
397
+ return cellEntries.reduce((sum, e) => sum + e.minutes, 0)
398
+ }, [dirty, entries])
399
+
400
+ // --- Save ---
401
+ const hasChanges = Object.keys(dirty).length > 0 || Object.keys(rawText).length > 0
402
+
403
+ const handleSave = React.useCallback(async () => {
404
+ if (!hasChanges) return
405
+ const confirmed = await confirm({
406
+ title: t('staff.timesheets.my.confirm_save.title', 'Save changes?'),
407
+ text: t('staff.timesheets.my.confirm_save.body', 'Your timesheet entries will be saved.'),
408
+ })
409
+ if (!confirmed) return
410
+
411
+ setIsSaving(true)
412
+ try {
413
+ const bulkEntries: Array<{ id?: string; date: string; timeProjectId: string; durationMinutes: number }> = []
414
+ for (const [projectId, dateMap] of Object.entries(dirty)) {
415
+ for (const [dateKey, cellValue] of Object.entries(dateMap)) {
416
+ const cell = cellValue as CellEntry
417
+ const cellEntries = entries[projectId]?.[dateKey] ?? []
418
+ const firstId = cell.id ?? cellEntries[0]?.id
419
+ bulkEntries.push({ id: firstId, date: dateKey, timeProjectId: projectId, durationMinutes: cell.minutes })
420
+ }
421
+ }
422
+ if (bulkEntries.length === 0) return
423
+
424
+ const payload = { entries: bulkEntries }
425
+ await runMutation({
426
+ operation: () =>
427
+ apiCallOrThrow('/api/staff/timesheets/time-entries/bulk', {
428
+ method: 'POST',
429
+ headers: { 'Content-Type': 'application/json' },
430
+ body: JSON.stringify(payload),
431
+ }),
432
+ context: {
433
+ formId: mutationContextId,
434
+ resourceKind: 'staff.timesheets.time_entry',
435
+ resourceId: staffMemberId ?? undefined,
436
+ staffMemberId,
437
+ retryLastMutation,
438
+ },
439
+ mutationPayload: payload as unknown as Record<string, unknown>,
440
+ })
441
+
442
+ flash(t('staff.timesheets.my.saved', 'Timesheet saved.'), 'success')
443
+ await loadData()
444
+ } catch (error) {
445
+ console.error('staff.timesheets.my.save', error)
446
+ flash(t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'), 'error')
447
+ } finally {
448
+ setIsSaving(false)
449
+ }
450
+ }, [dirty, entries, hasChanges, confirm, t, loadData, runMutation, mutationContextId, staffMemberId, retryLastMutation])
451
+
452
+ // --- Totals ---
453
+ const getRowTotal = React.useCallback((projectId: string): number => {
454
+ let total = 0
455
+ for (const day of visibleDays) {
456
+ total += getCellValue(projectId, formatDateKey(day))
457
+ }
458
+ return total
459
+ }, [visibleDays, getCellValue])
460
+
461
+ const getDayTotal = React.useCallback((date: Date): number => {
462
+ const dateKey = formatDateKey(date)
463
+ let total = 0
464
+ for (const project of projects) {
465
+ total += getCellValue(project.id, dateKey)
466
+ }
467
+ return total
468
+ }, [projects, getCellValue])
469
+
470
+ const grandTotal = React.useMemo(() => {
471
+ let total = 0
472
+ for (const project of projects) {
473
+ total += getRowTotal(project.id)
474
+ }
475
+ return total
476
+ }, [projects, getRowTotal])
477
+
478
+ const workingDays = React.useMemo(() => {
479
+ let count = 0
480
+ for (const day of visibleDays) {
481
+ if (!isWeekendDay(day)) {
482
+ const dateKey = formatDateKey(day)
483
+ for (const project of projects) {
484
+ if (getCellValue(project.id, dateKey) > 0) { count++; break }
485
+ }
486
+ }
487
+ }
488
+ return count
489
+ }, [visibleDays, projects, getCellValue])
490
+
491
+ const dailyAverage = React.useMemo(() => {
492
+ if (workingDays === 0) return 0
493
+ return grandTotal / workingDays
494
+ }, [grandTotal, workingDays])
495
+
496
+ // --- List view entries ---
497
+ const listViewEntries = React.useMemo(() => {
498
+ return rawEntries.map((item) => ({
499
+ id: String(item.id ?? ''),
500
+ date: String(item.date ?? '').slice(0, 10),
501
+ durationMinutes: typeof item.duration_minutes === 'number' ? item.duration_minutes
502
+ : typeof item.durationMinutes === 'number' ? item.durationMinutes : 0,
503
+ projectId: String(item.time_project_id ?? item.timeProjectId ?? ''),
504
+ projectName: projects.find((p) => p.id === String(item.time_project_id ?? item.timeProjectId ?? ''))?.name ?? '',
505
+ projectCode: projects.find((p) => p.id === String(item.time_project_id ?? item.timeProjectId ?? ''))?.code ?? null,
506
+ projectColor: projects.find((p) => p.id === String(item.time_project_id ?? item.timeProjectId ?? ''))?.color ?? null,
507
+ notes: typeof item.notes === 'string' ? item.notes : null,
508
+ source: typeof item.source === 'string' ? item.source : 'manual',
509
+ startedAt: typeof item.started_at === 'string' ? item.started_at : typeof item.startedAt === 'string' ? item.startedAt : null,
510
+ endedAt: typeof item.ended_at === 'string' ? item.ended_at : typeof item.endedAt === 'string' ? item.endedAt : null,
511
+ }))
512
+ }, [rawEntries, projects])
513
+
514
+ // --- Handle view mode change ---
515
+ const handleViewModeChange = React.useCallback((mode: ViewMode) => {
516
+ setViewMode(mode)
517
+ if (mode === 'monthly') {
518
+ // Use Thursday of the week to determine month (handles cross-month weeks)
519
+ const thursday = new Date(weekStart)
520
+ thursday.setDate(thursday.getDate() + 3)
521
+ setMonthYear(thursday.getFullYear())
522
+ setMonthIndex(thursday.getMonth())
523
+ } else {
524
+ // Find the Monday of the week containing the 15th (mid-month, always stable)
525
+ const midMonth = new Date(monthYear, monthIndex, 15)
526
+ setWeekStart(getMonday(midMonth))
527
+ }
528
+ }, [weekStart, monthYear, monthIndex])
529
+
530
+ // --- Add row handler ---
531
+ const visibleProjectIds = React.useMemo(() => new Set(projects.map((p) => p.id)), [projects])
532
+
533
+ const handleAddProject = React.useCallback(async (project: ProjectRow) => {
534
+ try {
535
+ const payload = { showInGrid: true }
536
+ await runMutation({
537
+ operation: () =>
538
+ apiCallOrThrow(`/api/staff/timesheets/my-projects/${project.id}`, {
539
+ method: 'PATCH',
540
+ headers: { 'Content-Type': 'application/json' },
541
+ body: JSON.stringify(payload),
542
+ }),
543
+ context: {
544
+ formId: mutationContextId,
545
+ resourceKind: 'staff.timesheets.time_project_member',
546
+ resourceId: project.id,
547
+ staffMemberId,
548
+ retryLastMutation,
549
+ },
550
+ mutationPayload: payload,
551
+ })
552
+ setProjects((prev) => {
553
+ if (prev.some((p) => p.id === project.id)) return prev
554
+ return [...prev, project]
555
+ })
556
+ } catch (error) {
557
+ console.error('staff.timesheets.my.addRow', error)
558
+ flash(t('staff.timesheets.my.addRow.error', 'Could not add the project. Please try again.'), 'error')
559
+ }
560
+ }, [t, runMutation, mutationContextId, staffMemberId, retryLastMutation])
561
+
562
+ const handleRemoveProject = React.useCallback(async (project: ProjectRow) => {
563
+ const confirmed = await confirm({
564
+ title: t('staff.timesheets.my.removeRow', 'Remove from grid'),
565
+ text: t(
566
+ 'staff.timesheets.my.removeRow.confirm',
567
+ 'Remove {projectName} from your timesheet grid? You can re-add it anytime via "+ Add row".',
568
+ ).replace('{projectName}', project.name),
569
+ })
570
+ if (!confirmed) return
571
+
572
+ try {
573
+ const payload = { showInGrid: false }
574
+ await runMutation({
575
+ operation: () =>
576
+ apiCallOrThrow(`/api/staff/timesheets/my-projects/${project.id}`, {
577
+ method: 'PATCH',
578
+ headers: { 'Content-Type': 'application/json' },
579
+ body: JSON.stringify(payload),
580
+ }),
581
+ context: {
582
+ formId: mutationContextId,
583
+ resourceKind: 'staff.timesheets.time_project_member',
584
+ resourceId: project.id,
585
+ staffMemberId,
586
+ retryLastMutation,
587
+ },
588
+ mutationPayload: payload,
589
+ })
590
+ setProjects((prev) => prev.filter((p) => p.id !== project.id))
591
+ setDirty((prev) => {
592
+ if (!prev[project.id]) return prev
593
+ const next = { ...prev }
594
+ delete next[project.id]
595
+ return next
596
+ })
597
+ setRawText((prev) => {
598
+ if (!prev[project.id]) return prev
599
+ const next = { ...prev }
600
+ delete next[project.id]
601
+ return next
602
+ })
603
+ } catch (error) {
604
+ console.error('staff.timesheets.my.removeRow', error)
605
+ flash(t('staff.timesheets.my.removeRow.error', 'Could not remove the project. Please try again.'), 'error')
606
+ }
607
+ }, [confirm, t, runMutation, mutationContextId, staffMemberId, retryLastMutation])
608
+
609
+ const handleProjectCreated = React.useCallback(async (project: { id: string; name: string; code: string | null }) => {
610
+ setAllAssignedProjects((prev) => [...prev, project])
611
+ // New projects start hidden; immediately opt them into the grid for the creator.
612
+ try {
613
+ const payload = { showInGrid: true }
614
+ await runMutation({
615
+ operation: () =>
616
+ apiCallOrThrow(`/api/staff/timesheets/my-projects/${project.id}`, {
617
+ method: 'PATCH',
618
+ headers: { 'Content-Type': 'application/json' },
619
+ body: JSON.stringify(payload),
620
+ }),
621
+ context: {
622
+ formId: mutationContextId,
623
+ resourceKind: 'staff.timesheets.time_project_member',
624
+ resourceId: project.id,
625
+ staffMemberId,
626
+ retryLastMutation,
627
+ },
628
+ mutationPayload: payload,
629
+ })
630
+ } catch (error) {
631
+ console.error('staff.timesheets.my.createProject.visibility', error)
632
+ }
633
+ setProjects((prev) => [...prev, project])
634
+ setCreateDialogOpen(false)
635
+ }, [runMutation, mutationContextId, staffMemberId, retryLastMutation])
636
+
637
+ // --- Loading ---
638
+ if (isInitialLoad) {
639
+ return <Page><PageBody><LoadingMessage label={t('staff.timesheets.my.loading', 'Loading timesheets...')} /></PageBody></Page>
640
+ }
641
+
642
+ // --- No profile ---
643
+ if (staffMemberMissing) {
644
+ return (
645
+ <Page>
646
+ <PageBody>
647
+ <div className="py-12 text-center">
648
+ <p className="text-lg font-semibold mb-2">
649
+ {t('staff.timesheets.my.noProfile.title', 'Set up your profile to start tracking time')}
650
+ </p>
651
+ <p className="text-sm text-muted-foreground mb-6">
652
+ {t('staff.timesheets.my.noProfile', 'You need a Team Member profile to track time.')}
653
+ </p>
654
+ <Button asChild>
655
+ <Link href="/backend/staff/profile/create">
656
+ {t('staff.timesheets.my.createProfile', 'Create My Profile')}
657
+ </Link>
658
+ </Button>
659
+ </div>
660
+ </PageBody>
661
+ </Page>
662
+ )
663
+ }
664
+
665
+ return (
666
+ <Page>
667
+ <PageBody>
668
+ {/* Timer bar */}
669
+ <TimerBar
670
+ projects={allAssignedProjects}
671
+ staffMemberId={staffMemberId}
672
+ onTimerStopped={loadData}
673
+ />
674
+
675
+ {/* Summary cards */}
676
+ <div className="mb-4 grid grid-cols-1 gap-4 sm:grid-cols-4">
677
+ <div className="rounded-lg border bg-card p-4">
678
+ <p className="text-sm text-muted-foreground">
679
+ {viewMode === 'weekly'
680
+ ? t('staff.timesheets.my.weekTotal', 'Week Total')
681
+ : t('staff.timesheets.my.total_hours', 'Total Hours')}
682
+ </p>
683
+ <p className="text-2xl font-semibold">{minutesToDecimal(grandTotal) || '0'}</p>
684
+ </div>
685
+ <div className="rounded-lg border bg-card p-4">
686
+ <p className="text-sm text-muted-foreground">{t('staff.timesheets.my.working_days', 'Working Days')}</p>
687
+ <p className="text-2xl font-semibold">{workingDays}</p>
688
+ </div>
689
+ <div className="rounded-lg border bg-card p-4">
690
+ <p className="text-sm text-muted-foreground">{t('staff.timesheets.my.daily_average', 'Daily Average')}</p>
691
+ <p className="text-2xl font-semibold">{minutesToDecimal(Math.round(dailyAverage)) || '0'}</p>
692
+ </div>
693
+ <div className="rounded-lg border bg-card p-4">
694
+ <p className="text-sm text-muted-foreground">{t('staff.timesheets.my.status', 'Status')}</p>
695
+ <p className="text-2xl font-semibold">
696
+ <span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
697
+ {t('staff.timesheets.my.status_open', 'Open')}
698
+ </span>
699
+ </p>
700
+ </div>
701
+ </div>
702
+
703
+ {/* Navigation + controls */}
704
+ <div className="mb-4 flex flex-wrap items-center justify-between gap-3">
705
+ <div className="flex items-center gap-2">
706
+ <Button variant="outline" size="icon" type="button" onClick={goToPrev}>
707
+ <ChevronLeft className="size-4" />
708
+ </Button>
709
+ <span className="text-sm font-semibold min-w-[220px] text-center">{navigationLabel}</span>
710
+ <Button variant="outline" size="icon" type="button" onClick={goToNext}>
711
+ <ChevronRight className="size-4" />
712
+ </Button>
713
+ {viewMode === 'weekly' && (
714
+ <CalendarPicker selectedWeekStart={weekStart} onWeekSelect={setWeekStart} />
715
+ )}
716
+ </div>
717
+
718
+ <div className="flex items-center gap-3">
719
+ <ViewSwitcher
720
+ viewMode={viewMode}
721
+ onViewModeChange={handleViewModeChange}
722
+ viewType={viewType}
723
+ onViewTypeChange={setViewType}
724
+ />
725
+ <div className="flex items-center gap-2">
726
+ {hasChanges && (
727
+ <span className="text-xs text-amber-600 font-medium">
728
+ {t('staff.timesheets.my.unsaved', 'Unsaved changes')}
729
+ </span>
730
+ )}
731
+ <Button size="sm" type="button" onClick={handleSave} disabled={!hasChanges || isSaving}>
732
+ {isSaving ? t('staff.timesheets.my.saving', 'Saving...') : t('staff.timesheets.my.save_changes', 'Save Changes')}
733
+ </Button>
734
+ </div>
735
+ </div>
736
+ </div>
737
+
738
+ {/* Content: Grid or List */}
739
+ <div className={isRefreshing ? 'opacity-50 pointer-events-none transition-opacity' : 'transition-opacity'}>
740
+ {allAssignedProjects.length === 0 ? (
741
+ <div className="py-12 text-center">
742
+ <p className="text-lg font-semibold mb-2">
743
+ {t('staff.timesheets.my.noProjects.title', 'No projects assigned yet')}
744
+ </p>
745
+ <p className="text-sm text-muted-foreground mb-6">
746
+ {canManageProjects
747
+ ? t('staff.timesheets.my.noProjects.admin', 'Create a project and assign yourself to start tracking time.')
748
+ : t('staff.timesheets.my.noProjects.employee', 'Ask your manager to assign you to a project.')}
749
+ </p>
750
+ <div className="flex items-center justify-center gap-3">
751
+ {canManageProjects && (
752
+ <Button asChild>
753
+ <Link href="/backend/staff/timesheets/projects/create">
754
+ {t('staff.timesheets.my.noProjects.createProject', 'Create Project')}
755
+ </Link>
756
+ </Button>
757
+ )}
758
+ <Button variant="outline" asChild>
759
+ <Link href="/backend/staff/timesheets/projects">
760
+ {t('staff.timesheets.my.noProjects.viewProjects', 'View Projects')}
761
+ </Link>
762
+ </Button>
763
+ </div>
764
+ </div>
765
+ ) : viewType === 'list' ? (
766
+ <ListView entries={listViewEntries} onEntryUpdated={loadData} />
767
+ ) : (
768
+ <div className="overflow-x-auto rounded-lg border">
769
+ <table className="w-full text-sm table-fixed">
770
+ <colgroup>
771
+ <col className={viewMode === 'weekly' ? 'w-[35%]' : 'w-[140px] min-w-[140px]'} />
772
+ {visibleDays.map((date) => (
773
+ <col key={formatDateKey(date)} className={viewMode === 'weekly' ? '' : 'w-[36px] min-w-[36px]'} />
774
+ ))}
775
+ <col className={viewMode === 'weekly' ? 'w-[72px]' : 'w-[56px] min-w-[56px]'} />
776
+ </colgroup>
777
+ <thead>
778
+ <tr className="border-b bg-muted/50">
779
+ <th className="sticky left-0 z-10 bg-muted px-3 py-2 text-left font-medium">
780
+ {t('staff.timesheets.my.project', 'Project')}
781
+ </th>
782
+ {visibleDays.map((date) => {
783
+ const dayName = getLocalizedDayName(date)
784
+ const weekend = isWeekendDay(date)
785
+ return (
786
+ <th
787
+ key={formatDateKey(date)}
788
+ className={`py-2 text-center font-medium px-1 ${weekend ? 'bg-muted/80 text-muted-foreground' : ''}`}
789
+ >
790
+ <div className="text-[10px] uppercase text-muted-foreground">{dayName}</div>
791
+ <div className="text-xs">{date.getDate()}</div>
792
+ </th>
793
+ )
794
+ })}
795
+ <th className="px-3 py-2 text-right font-medium">
796
+ {t('staff.timesheets.my.total', 'Total')}
797
+ </th>
798
+ </tr>
799
+ </thead>
800
+ <tbody>
801
+ {projects.map((project) => (
802
+ <tr key={project.id} className="group border-b hover:bg-muted/30">
803
+ <td className="sticky left-0 z-10 bg-background px-3 py-1.5 font-medium text-foreground">
804
+ <div className="flex items-center justify-between gap-2">
805
+ <div className="min-w-0 flex-1">
806
+ <div className="flex items-center gap-1.5 truncate" title={project.name}>
807
+ <ProjectColorDot colorKey={project.color} projectName={project.name} size="sm" />
808
+ <span className="truncate">{project.name}</span>
809
+ </div>
810
+ {project.code && (
811
+ <div className="text-[10px] text-muted-foreground">{project.code}</div>
812
+ )}
813
+ </div>
814
+ <button
815
+ type="button"
816
+ onClick={() => { void handleRemoveProject(project) }}
817
+ aria-label={t('staff.timesheets.my.removeRow', 'Remove from grid')}
818
+ title={t('staff.timesheets.my.removeRow', 'Remove from grid')}
819
+ className="opacity-0 group-hover:opacity-100 focus:opacity-100 inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-opacity"
820
+ >
821
+ <X className="h-3.5 w-3.5" />
822
+ </button>
823
+ </div>
824
+ </td>
825
+ {visibleDays.map((date) => {
826
+ const dateKey = formatDateKey(date)
827
+ const weekend = isWeekendDay(date)
828
+ const cellMinutes = getCellValue(project.id, dateKey)
829
+ const isDirty = dirty[project.id]?.[dateKey] !== undefined
830
+ return (
831
+ <td key={dateKey} className={`px-0.5 py-0.5 ${weekend ? 'bg-muted/40' : ''}`}>
832
+ {weekend ? (
833
+ <div className="rounded px-1 py-1 text-center text-xs text-muted-foreground/50">-</div>
834
+ ) : (
835
+ <input
836
+ type="text"
837
+ inputMode="decimal"
838
+ className={`mx-auto block rounded border text-center tabular-nums transition-colors
839
+ ${viewMode === 'weekly' ? 'w-12 px-1 py-0.5 text-xs' : 'w-8 px-0 py-1 text-[10px]'}
840
+ ${isDirty ? 'border-amber-400 bg-amber-50' : 'border-muted-foreground/20 bg-transparent'}
841
+ ${cellMinutes > 0 ? 'font-semibold' : 'text-muted-foreground'}
842
+ hover:border-muted-foreground/40 focus:border-primary focus:bg-background focus:outline-none`}
843
+ value={rawText[project.id]?.[dateKey] ?? minutesToDecimal(cellMinutes)}
844
+ onChange={(e) => handleCellChange(project.id, dateKey, e.target.value)}
845
+ onBlur={() => handleCellBlur(project.id, dateKey)}
846
+ placeholder={t('staff.timesheets.my.durationPlaceholder', '0')}
847
+ />
848
+ )}
849
+ </td>
850
+ )
851
+ })}
852
+ <td className="px-3 py-1.5 text-right font-semibold text-xs tabular-nums">
853
+ {minutesToDecimal(getRowTotal(project.id)) || '0'}
854
+ </td>
855
+ </tr>
856
+ ))}
857
+ <tr className="border-b">
858
+ <td colSpan={visibleDays.length + 2} className="sticky left-0 bg-background px-1 py-0.5">
859
+ <AddRowDropdown
860
+ assignedProjects={allAssignedProjects}
861
+ visibleProjectIds={visibleProjectIds}
862
+ canCreateProject={canManageProjects}
863
+ onAddProject={handleAddProject}
864
+ onCreateProject={() => setCreateDialogOpen(true)}
865
+ />
866
+ </td>
867
+ </tr>
868
+ </tbody>
869
+ <tfoot>
870
+ <tr className="border-t bg-muted/50 font-semibold">
871
+ <td className="sticky left-0 z-10 bg-muted px-3 py-2">
872
+ {t('staff.timesheets.my.daily_total', 'Daily Total')}
873
+ </td>
874
+ {visibleDays.map((date) => {
875
+ const weekend = isWeekendDay(date)
876
+ const dayMinutes = getDayTotal(date)
877
+ return (
878
+ <td key={formatDateKey(date)} className={`py-2 text-center text-xs tabular-nums ${weekend ? 'text-muted-foreground/50' : ''}`}>
879
+ {weekend ? '-' : (minutesToDecimal(dayMinutes) || '-')}
880
+ </td>
881
+ )
882
+ })}
883
+ <td className="px-3 py-2 text-right tabular-nums font-semibold">{minutesToDecimal(grandTotal) || '0'}</td>
884
+ </tr>
885
+ </tfoot>
886
+ </table>
887
+ </div>
888
+ )}
889
+ </div>
890
+ </PageBody>
891
+ {ConfirmDialogElement}
892
+ <CreateProjectDialog
893
+ open={createDialogOpen}
894
+ onOpenChange={setCreateDialogOpen}
895
+ onProjectCreated={handleProjectCreated}
896
+ />
897
+ </Page>
898
+ )
899
+ }