@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,229 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Button } from '@open-mercato/ui/primitives/button'
5
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'
8
+
9
+ type CalendarPickerProps = {
10
+ selectedWeekStart: Date
11
+ onWeekSelect: (weekStart: Date) => void
12
+ }
13
+
14
+ function getMonday(date: Date): Date {
15
+ const result = new Date(date)
16
+ const day = result.getDay()
17
+ const diff = day === 0 ? -6 : 1 - day
18
+ result.setDate(result.getDate() + diff)
19
+ result.setHours(0, 0, 0, 0)
20
+ return result
21
+ }
22
+
23
+ function getWeekNumber(date: Date): number {
24
+ const target = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
25
+ target.setUTCDate(target.getUTCDate() + 4 - (target.getUTCDay() || 7))
26
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
27
+ return Math.ceil(((target.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
28
+ }
29
+
30
+ function isSameDay(first: Date, second: Date): boolean {
31
+ return first.getFullYear() === second.getFullYear() && first.getMonth() === second.getMonth() && first.getDate() === second.getDate()
32
+ }
33
+
34
+ function isSameWeek(first: Date, second: Date): boolean {
35
+ return isSameDay(getMonday(first), getMonday(second))
36
+ }
37
+
38
+ function buildWeeks(year: number, month: number): Date[][] {
39
+ const firstDay = new Date(year, month, 1)
40
+ const start = getMonday(firstDay)
41
+ const weeks: Date[][] = []
42
+ const current = new Date(start)
43
+
44
+ for (let weekIdx = 0; weekIdx < 6; weekIdx++) {
45
+ const week: Date[] = []
46
+ for (let dayIdx = 0; dayIdx < 7; dayIdx++) {
47
+ week.push(new Date(current))
48
+ current.setDate(current.getDate() + 1)
49
+ }
50
+ weeks.push(week)
51
+ if (current.getMonth() !== month && weekIdx >= 3) break
52
+ }
53
+ return weeks
54
+ }
55
+
56
+ function getLocalizedDayHeaders(): string[] {
57
+ const baseMonday = new Date(2024, 0, 1) // Known Monday
58
+ return Array.from({ length: 7 }, (_, idx) => {
59
+ const date = new Date(baseMonday)
60
+ date.setDate(date.getDate() + idx)
61
+ return date.toLocaleDateString(undefined, { weekday: 'narrow' })
62
+ })
63
+ }
64
+
65
+ export function CalendarPicker({ selectedWeekStart, onWeekSelect }: CalendarPickerProps) {
66
+ const t = useT()
67
+ const [open, setOpen] = React.useState(false)
68
+ const [viewYear, setViewYear] = React.useState(selectedWeekStart.getFullYear())
69
+ const [viewMonth, setViewMonth] = React.useState(selectedWeekStart.getMonth())
70
+ const containerRef = React.useRef<HTMLDivElement>(null)
71
+
72
+ const dayHeaders = React.useMemo(() => getLocalizedDayHeaders(), [])
73
+
74
+ React.useEffect(() => {
75
+ function handleClickOutside(event: MouseEvent) {
76
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
77
+ setOpen(false)
78
+ }
79
+ }
80
+ if (open) document.addEventListener('mousedown', handleClickOutside)
81
+ return () => document.removeEventListener('mousedown', handleClickOutside)
82
+ }, [open])
83
+
84
+ React.useEffect(() => {
85
+ setViewYear(selectedWeekStart.getFullYear())
86
+ setViewMonth(selectedWeekStart.getMonth())
87
+ }, [selectedWeekStart])
88
+
89
+ const weeks = React.useMemo(() => buildWeeks(viewYear, viewMonth), [viewYear, viewMonth])
90
+
91
+ const monthLabel = React.useMemo(() => {
92
+ return new Date(viewYear, viewMonth, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' })
93
+ }, [viewYear, viewMonth])
94
+
95
+ const today = React.useMemo(() => new Date(), [])
96
+
97
+ const handleWeekClick = React.useCallback((monday: Date) => {
98
+ onWeekSelect(monday)
99
+ setOpen(false)
100
+ }, [onWeekSelect])
101
+
102
+ const goToPrevMonth = React.useCallback(() => {
103
+ setViewMonth((prev) => {
104
+ if (prev === 0) { setViewYear((yr) => yr - 1); return 11 }
105
+ return prev - 1
106
+ })
107
+ }, [])
108
+
109
+ const goToNextMonth = React.useCallback(() => {
110
+ setViewMonth((prev) => {
111
+ if (prev === 11) { setViewYear((yr) => yr + 1); return 0 }
112
+ return prev + 1
113
+ })
114
+ }, [])
115
+
116
+ return (
117
+ <div ref={containerRef} className="relative inline-block">
118
+ <IconButton
119
+ variant="outline"
120
+ size="sm"
121
+ type="button"
122
+ onClick={() => setOpen((prev) => !prev)}
123
+ aria-label={t('staff.timesheets.my.calendar.open', 'Open calendar')}
124
+ >
125
+ <CalendarDays className="size-4" />
126
+ </IconButton>
127
+
128
+ {open && (
129
+ <div className="absolute left-0 z-20 mt-1 w-[280px] rounded-lg border bg-popover p-3 shadow-lg">
130
+ {/* Quick links */}
131
+ <div className="mb-3 flex gap-2 border-b pb-2">
132
+ <Button
133
+ type="button"
134
+ variant="ghost"
135
+ size="sm"
136
+ className="h-auto px-2 py-1 text-xs"
137
+ onClick={() => handleWeekClick(getMonday(new Date()))}
138
+ >
139
+ {t('staff.timesheets.my.calendar.thisWeek', 'This week')}
140
+ </Button>
141
+ <Button
142
+ type="button"
143
+ variant="ghost"
144
+ size="sm"
145
+ className="h-auto px-2 py-1 text-xs"
146
+ onClick={() => {
147
+ const lastWeek = new Date()
148
+ lastWeek.setDate(lastWeek.getDate() - 7)
149
+ handleWeekClick(getMonday(lastWeek))
150
+ }}
151
+ >
152
+ {t('staff.timesheets.my.calendar.lastWeek', 'Last week')}
153
+ </Button>
154
+ </div>
155
+
156
+ {/* Month navigation */}
157
+ <div className="mb-2 flex items-center justify-between">
158
+ <IconButton
159
+ variant="ghost"
160
+ size="xs"
161
+ type="button"
162
+ onClick={goToPrevMonth}
163
+ aria-label={t('staff.timesheets.my.calendar.prevMonth', 'Previous month')}
164
+ >
165
+ <ChevronLeft className="size-3.5" />
166
+ </IconButton>
167
+ <span className="text-sm font-medium">{monthLabel}</span>
168
+ <IconButton
169
+ variant="ghost"
170
+ size="xs"
171
+ type="button"
172
+ onClick={goToNextMonth}
173
+ aria-label={t('staff.timesheets.my.calendar.nextMonth', 'Next month')}
174
+ >
175
+ <ChevronRight className="size-3.5" />
176
+ </IconButton>
177
+ </div>
178
+
179
+ {/* Day headers */}
180
+ <div className="grid grid-cols-[32px_repeat(7,1fr)] gap-0 mb-1">
181
+ <div />
182
+ {dayHeaders.map((day, idx) => (
183
+ <div key={idx} className="text-center text-[11px] font-medium text-muted-foreground py-1">
184
+ {day}
185
+ </div>
186
+ ))}
187
+ </div>
188
+
189
+ {/* Week rows */}
190
+ {weeks.map((week) => {
191
+ const monday = week[0]
192
+ const weekNum = getWeekNumber(monday)
193
+ const isSelected = isSameWeek(monday, selectedWeekStart)
194
+
195
+ return (
196
+ <Button
197
+ key={monday.toISOString()}
198
+ type="button"
199
+ variant="ghost"
200
+ className={`grid grid-cols-[32px_repeat(7,1fr)] gap-0 w-full h-auto rounded-md px-0 py-0 hover:bg-muted
201
+ ${isSelected ? 'bg-primary text-primary-foreground hover:bg-primary/90' : ''}`}
202
+ onClick={() => handleWeekClick(monday)}
203
+ >
204
+ <span className={`text-[10px] font-medium py-1.5 text-center ${isSelected ? 'text-primary-foreground/70' : 'text-muted-foreground'}`}>
205
+ {weekNum}
206
+ </span>
207
+ {week.map((date) => {
208
+ const inMonth = date.getMonth() === viewMonth
209
+ const isTodayDate = isSameDay(date, today)
210
+ return (
211
+ <span
212
+ key={date.toISOString()}
213
+ className={`text-xs py-1.5 text-center
214
+ ${!inMonth && !isSelected ? 'text-muted-foreground/40' : ''}
215
+ ${isTodayDate && !isSelected ? 'font-bold text-primary' : ''}
216
+ ${isTodayDate && isSelected ? 'font-bold underline' : ''}`}
217
+ >
218
+ {date.getDate()}
219
+ </span>
220
+ )
221
+ })}
222
+ </Button>
223
+ )
224
+ })}
225
+ </div>
226
+ )}
227
+ </div>
228
+ )
229
+ }
@@ -0,0 +1,65 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Check } from 'lucide-react'
5
+ import { PROJECT_COLORS } from './colors'
6
+
7
+ export type ColorPickerProps = {
8
+ value: string | null | undefined
9
+ onChange: (value: string | null) => void
10
+ allowReset?: boolean
11
+ resetLabel?: string
12
+ id?: string
13
+ disabled?: boolean
14
+ }
15
+
16
+ export function ColorPicker({
17
+ value,
18
+ onChange,
19
+ allowReset = true,
20
+ resetLabel = 'Auto',
21
+ id,
22
+ disabled = false,
23
+ }: ColorPickerProps) {
24
+ return (
25
+ <div id={id} className="flex flex-wrap items-center gap-2" role="radiogroup">
26
+ {PROJECT_COLORS.map((color) => {
27
+ const selected = value === color.key
28
+ return (
29
+ <button
30
+ key={color.key}
31
+ type="button"
32
+ role="radio"
33
+ aria-checked={selected}
34
+ aria-label={color.key}
35
+ title={color.key}
36
+ disabled={disabled}
37
+ onClick={() => onChange(color.key)}
38
+ className={`inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
39
+ selected ? 'ring-2 ring-offset-1 ring-ring' : ''
40
+ }`}
41
+ style={{ backgroundColor: color.hex }}
42
+ >
43
+ {selected ? <Check className="h-3.5 w-3.5 text-white" strokeWidth={3} /> : null}
44
+ </button>
45
+ )
46
+ })}
47
+ {allowReset ? (
48
+ <button
49
+ type="button"
50
+ role="radio"
51
+ aria-checked={value == null}
52
+ aria-label={resetLabel}
53
+ title={resetLabel}
54
+ disabled={disabled}
55
+ onClick={() => onChange(null)}
56
+ className={`inline-flex h-6 items-center cursor-pointer rounded-full border border-dashed border-muted-foreground/50 bg-transparent px-2 text-[11px] text-muted-foreground transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
57
+ value == null ? 'ring-2 ring-offset-1 ring-ring' : ''
58
+ }`}
59
+ >
60
+ {resetLabel}
61
+ </button>
62
+ ) : null}
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,99 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
7
+ import { createCrud } from '@open-mercato/ui/backend/utils/crud'
8
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
10
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
12
+ import { E } from '#generated/entities.ids.generated'
13
+ import {
14
+ buildProjectPayload,
15
+ createProjectFormFields,
16
+ createProjectFormGroups,
17
+ createProjectFormSchema,
18
+ type ProjectFormValues,
19
+ } from '../../backend/staff/timesheets/projects/projectFormConfig'
20
+
21
+ type CreateProjectDialogProps = {
22
+ open: boolean
23
+ onOpenChange: (open: boolean) => void
24
+ onProjectCreated: (project: { id: string; name: string; code: string | null }) => void
25
+ }
26
+
27
+ export function CreateProjectDialog({ open, onOpenChange, onProjectCreated }: CreateProjectDialogProps) {
28
+ const t = useT()
29
+
30
+ const formSchema = React.useMemo(() => createProjectFormSchema(), [])
31
+ const fields = React.useMemo(() => createProjectFormFields(t), [t])
32
+ const groups = React.useMemo(() => createProjectFormGroups(t), [t])
33
+
34
+ return (
35
+ <Dialog open={open} onOpenChange={onOpenChange}>
36
+ <DialogContent className="sm:max-w-lg">
37
+ <DialogHeader>
38
+ <DialogTitle>
39
+ {t('staff.timesheets.projects.form.createTitle', 'Create project')}
40
+ </DialogTitle>
41
+ </DialogHeader>
42
+ <CrudForm<ProjectFormValues>
43
+ embedded
44
+ fields={fields}
45
+ groups={groups}
46
+ schema={formSchema}
47
+ initialValues={{}}
48
+ entityIds={[E.staff.staff_time_project]}
49
+ submitLabel={t('staff.timesheets.projects.form.actions.create', 'Create')}
50
+ extraActions={(
51
+ <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
52
+ {t('staff.timesheets.projects.form.actions.cancel', 'Cancel')}
53
+ </Button>
54
+ )}
55
+ onSubmit={async (values) => {
56
+ const payload = buildProjectPayload(values)
57
+
58
+ const { result: created } = await createCrud<{ id?: string; name?: string; code?: string | null }>(
59
+ 'staff/timesheets/time-projects',
60
+ payload,
61
+ { errorMessage: t('staff.timesheets.projects.errors.save', 'Failed to save project.') },
62
+ )
63
+
64
+ const newId = created?.id
65
+ if (!newId) {
66
+ throw createCrudFormError(
67
+ t('staff.timesheets.projects.errors.save', 'Failed to save project.'),
68
+ )
69
+ }
70
+
71
+ // Auto-assign creator to the project (best-effort)
72
+ try {
73
+ const selfRes = await apiCall<{ member?: { id?: string } | null }>('/api/staff/team-members/self')
74
+ const staffMemberId = selfRes.result?.member?.id
75
+ if (staffMemberId) {
76
+ await createCrud('staff/timesheets/time-projects/' + newId + '/employees', {
77
+ staffMemberId,
78
+ assignedStartDate: new Date().toISOString().slice(0, 10),
79
+ status: 'active',
80
+ }, { errorMessage: '' })
81
+ }
82
+ } catch {
83
+ // non-critical — project created, self-assignment is best-effort
84
+ }
85
+
86
+ onProjectCreated({
87
+ id: newId,
88
+ name: created?.name ?? values.name.trim(),
89
+ code: created?.code ?? values.code?.trim() ?? null,
90
+ })
91
+
92
+ flash(t('staff.timesheets.projects.messages.saved', 'Project saved.'), 'success')
93
+ onOpenChange(false)
94
+ }}
95
+ />
96
+ </DialogContent>
97
+ </Dialog>
98
+ )
99
+ }
@@ -0,0 +1,230 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
6
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { ProjectColorDot } from './ProjectColorDot'
8
+
9
+ type TimeEntry = {
10
+ id: string
11
+ date: string
12
+ durationMinutes: number
13
+ projectId: string
14
+ projectName: string
15
+ projectCode: string | null
16
+ projectColor: string | null
17
+ notes: string | null
18
+ source: string
19
+ startedAt: string | null
20
+ endedAt: string | null
21
+ }
22
+
23
+ type ListViewProps = {
24
+ entries: TimeEntry[]
25
+ onEntryUpdated?: () => void
26
+ }
27
+
28
+ function getLocalDateStr(date: Date): string {
29
+ const year = date.getFullYear()
30
+ const month = String(date.getMonth() + 1).padStart(2, '0')
31
+ const day = String(date.getDate()).padStart(2, '0')
32
+ return `${year}-${month}-${day}`
33
+ }
34
+
35
+ function formatDayLabel(dateStr: string, translate: TranslateFn): string {
36
+ const today = new Date()
37
+ const todayStr = getLocalDateStr(today)
38
+
39
+ const yesterday = new Date(today)
40
+ yesterday.setDate(yesterday.getDate() - 1)
41
+ const yesterdayStr = getLocalDateStr(yesterday)
42
+
43
+ if (dateStr === todayStr) return translate('staff.timesheets.my.list.today', 'Today')
44
+ if (dateStr === yesterdayStr) return translate('staff.timesheets.my.list.yesterday', 'Yesterday')
45
+
46
+ const date = new Date(dateStr + 'T00:00:00')
47
+ const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })
48
+ const day = date.getDate()
49
+ const month = date.toLocaleDateString(undefined, { month: 'short' })
50
+
51
+ return `${dayName}, ${day} ${month}`
52
+ }
53
+
54
+ function formatDuration(minutes: number): string {
55
+ if (minutes < 60) return `${minutes}m`
56
+
57
+ const hours = Math.floor(minutes / 60)
58
+ const remainingMinutes = minutes % 60
59
+
60
+ if (remainingMinutes === 0) return `${hours}h`
61
+ return `${hours}h ${remainingMinutes}m`
62
+ }
63
+
64
+ function formatTimeRange(startedAt: string, endedAt: string): string {
65
+ const start = new Date(startedAt)
66
+ const end = new Date(endedAt)
67
+
68
+ const format = (date: Date) =>
69
+ date.toLocaleTimeString(undefined, {
70
+ hour: 'numeric',
71
+ minute: '2-digit',
72
+ hour12: true,
73
+ })
74
+
75
+ return `${format(start)} - ${format(end)}`
76
+ }
77
+
78
+ function InlineDescription({
79
+ entryId,
80
+ initialValue,
81
+ placeholder,
82
+ onSaved,
83
+ }: {
84
+ entryId: string
85
+ initialValue: string | null
86
+ placeholder: string
87
+ onSaved?: () => void
88
+ }) {
89
+ const [editing, setEditing] = React.useState(false)
90
+ const [value, setValue] = React.useState(initialValue ?? '')
91
+ const [saving, setSaving] = React.useState(false)
92
+ const inputRef = React.useRef<HTMLInputElement>(null)
93
+
94
+ React.useEffect(() => {
95
+ if (editing && inputRef.current) inputRef.current.focus()
96
+ }, [editing])
97
+
98
+ const save = React.useCallback(async () => {
99
+ const trimmed = value.trim()
100
+ if (trimmed === (initialValue ?? '')) {
101
+ setEditing(false)
102
+ return
103
+ }
104
+ setSaving(true)
105
+ try {
106
+ await apiCall(`/api/staff/timesheets/time-entries`, {
107
+ method: 'PUT',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ id: entryId, notes: trimmed || null }),
110
+ })
111
+ onSaved?.()
112
+ } catch {
113
+ // silent
114
+ } finally {
115
+ setSaving(false)
116
+ setEditing(false)
117
+ }
118
+ }, [entryId, value, initialValue, onSaved])
119
+
120
+ if (!editing) {
121
+ return (
122
+ <button
123
+ type="button"
124
+ onClick={() => setEditing(true)}
125
+ className={`text-sm text-left cursor-pointer hover:underline ${
126
+ initialValue ? '' : 'text-muted-foreground italic'
127
+ }`}
128
+ >
129
+ {initialValue || placeholder}
130
+ </button>
131
+ )
132
+ }
133
+
134
+ return (
135
+ <input
136
+ ref={inputRef}
137
+ type="text"
138
+ value={value}
139
+ disabled={saving}
140
+ onChange={(e) => setValue(e.target.value)}
141
+ onBlur={() => { void save() }}
142
+ onKeyDown={(e) => {
143
+ if (e.key === 'Enter') { void save() }
144
+ if (e.key === 'Escape') { setValue(initialValue ?? ''); setEditing(false) }
145
+ }}
146
+ placeholder={placeholder}
147
+ className="text-sm w-full bg-transparent border-b border-primary/40 outline-none py-0.5 placeholder:text-muted-foreground/50"
148
+ />
149
+ )
150
+ }
151
+
152
+ export function ListView({ entries, onEntryUpdated }: ListViewProps) {
153
+ const t = useT()
154
+
155
+ if (entries.length === 0) {
156
+ return (
157
+ <div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
158
+ {t('staff.timesheets.my.list.noEntries', 'No entries for this period.')}
159
+ </div>
160
+ )
161
+ }
162
+
163
+ const grouped = new Map<string, TimeEntry[]>()
164
+ for (const entry of entries) {
165
+ const existing = grouped.get(entry.date)
166
+ if (existing) {
167
+ existing.push(entry)
168
+ } else {
169
+ grouped.set(entry.date, [entry])
170
+ }
171
+ }
172
+
173
+ const sortedDays = [...grouped.keys()].sort((a, b) => b.localeCompare(a))
174
+
175
+ return (
176
+ <div>
177
+ {sortedDays.map((dateStr) => {
178
+ const dayEntries = grouped.get(dateStr)!
179
+ const dailyTotal = dayEntries.reduce(
180
+ (sum, entry) => sum + entry.durationMinutes,
181
+ 0,
182
+ )
183
+
184
+ return (
185
+ <div key={dateStr} className="rounded-lg border mb-4">
186
+ <div className="flex justify-between items-center p-4 border-b bg-muted/30 font-medium text-sm">
187
+ <span>{formatDayLabel(dateStr, t)}</span>
188
+ <span className="font-mono tabular-nums">
189
+ {formatDuration(dailyTotal)}
190
+ </span>
191
+ </div>
192
+
193
+ {dayEntries.map((entry) => (
194
+ <div
195
+ key={entry.id}
196
+ className="flex items-center justify-between p-3 border-b last:border-0 hover:bg-muted/20"
197
+ >
198
+ <div className="flex flex-col gap-1 min-w-0 flex-1 mr-4">
199
+ <InlineDescription
200
+ entryId={entry.id}
201
+ initialValue={entry.notes}
202
+ placeholder={t('staff.timesheets.my.list.addDescription', 'Add description')}
203
+ onSaved={onEntryUpdated}
204
+ />
205
+ <span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">
206
+ <ProjectColorDot colorKey={entry.projectColor} projectName={entry.projectName} size="xs" />
207
+ {entry.projectName}
208
+ </span>
209
+ </div>
210
+
211
+ <div className="flex flex-col items-end gap-1 shrink-0">
212
+ {entry.source === 'timer' &&
213
+ entry.startedAt &&
214
+ entry.endedAt ? (
215
+ <span className="text-xs text-muted-foreground">
216
+ {formatTimeRange(entry.startedAt, entry.endedAt)}
217
+ </span>
218
+ ) : null}
219
+ <span className="text-sm font-mono tabular-nums">
220
+ {formatDuration(entry.durationMinutes)}
221
+ </span>
222
+ </div>
223
+ </div>
224
+ ))}
225
+ </div>
226
+ )
227
+ })}
228
+ </div>
229
+ )
230
+ }
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { resolveProjectColorHex } from './colors'
5
+
6
+ export type ProjectColorDotSize = 'xs' | 'sm' | 'md'
7
+
8
+ export type ProjectColorDotProps = {
9
+ colorKey: string | null | undefined
10
+ projectName: string | null | undefined
11
+ size?: ProjectColorDotSize
12
+ className?: string
13
+ title?: string
14
+ }
15
+
16
+ const SIZE_CLASS: Record<ProjectColorDotSize, string> = {
17
+ xs: 'h-2 w-2',
18
+ sm: 'h-2.5 w-2.5',
19
+ md: 'h-3 w-3',
20
+ }
21
+
22
+ export function ProjectColorDot({
23
+ colorKey,
24
+ projectName,
25
+ size = 'sm',
26
+ className,
27
+ title,
28
+ }: ProjectColorDotProps) {
29
+ const hex = resolveProjectColorHex(colorKey, projectName)
30
+ const base = 'inline-block shrink-0 rounded-full'
31
+ const classes = [base, SIZE_CLASS[size], className].filter(Boolean).join(' ')
32
+ return (
33
+ <span
34
+ aria-hidden="true"
35
+ title={title}
36
+ className={classes}
37
+ style={{ backgroundColor: hex }}
38
+ />
39
+ )
40
+ }