@open-mercato/core 0.6.4-develop.4210.1.d412061cfe → 0.6.4-develop.4236.1.9fa6806b34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_time_entry/index.js +37 -0
  3. package/dist/generated/entities/staff_time_entry/index.js.map +7 -0
  4. package/dist/generated/entities/staff_time_entry_segment/index.js +23 -0
  5. package/dist/generated/entities/staff_time_entry_segment/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +35 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +7 -0
  8. package/dist/generated/entities/staff_time_project_member/index.js +29 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +7 -0
  10. package/dist/generated/entities.ids.generated.js +5 -1
  11. package/dist/generated/entities.ids.generated.js.map +2 -2
  12. package/dist/generated/entity-fields-registry.js +64 -0
  13. package/dist/generated/entity-fields-registry.js.map +2 -2
  14. package/dist/helpers/integration/timesheetFixtures.js +50 -0
  15. package/dist/helpers/integration/timesheetFixtures.js.map +7 -0
  16. package/dist/modules/attachments/api/library/[id]/route.js +20 -16
  17. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +18 -14
  19. package/dist/modules/attachments/api/route.js.map +2 -2
  20. package/dist/modules/auth/api/roles/acl/route.js +10 -4
  21. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  22. package/dist/modules/auth/api/sidebar/preferences/route.js +27 -20
  23. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  24. package/dist/modules/auth/api/users/acl/route.js +16 -11
  25. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +87 -71
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/services/sidebarPreferencesService.js +39 -30
  29. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  30. package/dist/modules/catalog/commands/categories.js +61 -12
  31. package/dist/modules/catalog/commands/categories.js.map +2 -2
  32. package/dist/modules/catalog/commands/products.js +79 -54
  33. package/dist/modules/catalog/commands/products.js.map +2 -2
  34. package/dist/modules/catalog/commands/variants.js +29 -16
  35. package/dist/modules/catalog/commands/variants.js.map +2 -2
  36. package/dist/modules/currencies/commands/currencies.js +15 -8
  37. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users.js +27 -26
  39. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/password/reset-confirm.js +5 -5
  41. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +11 -10
  43. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  44. package/dist/modules/customers/commands/addresses.js +35 -21
  45. package/dist/modules/customers/commands/addresses.js.map +2 -2
  46. package/dist/modules/customers/commands/companies.js +163 -162
  47. package/dist/modules/customers/commands/companies.js.map +2 -2
  48. package/dist/modules/customers/commands/deals.js +3 -4
  49. package/dist/modules/customers/commands/deals.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +19 -22
  51. package/dist/modules/customers/commands/interactions.js.map +2 -2
  52. package/dist/modules/customers/commands/people.js +18 -15
  53. package/dist/modules/customers/commands/people.js.map +2 -2
  54. package/dist/modules/customers/commands/personCompanyLinks.js +105 -94
  55. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  56. package/dist/modules/customers/commands/pipeline-stages.js +30 -23
  57. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  58. package/dist/modules/customers/commands/pipelines.js +27 -20
  59. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  60. package/dist/modules/customers/commands/tags.js +13 -5
  61. package/dist/modules/customers/commands/tags.js.map +2 -2
  62. package/dist/modules/dashboards/api/users/widgets/route.js +0 -1
  63. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  64. package/dist/modules/dashboards/api/widgets/data/route.js +29 -1
  65. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  66. package/dist/modules/data_sync/lib/sync-engine.js +4 -4
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  68. package/dist/modules/data_sync/lib/sync-run-service.js +51 -27
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  70. package/dist/modules/directory/commands/organizations.js +192 -158
  71. package/dist/modules/directory/commands/organizations.js.map +3 -3
  72. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +22 -16
  73. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  74. package/dist/modules/messages/commands/messages.js +77 -75
  75. package/dist/modules/messages/commands/messages.js.map +2 -2
  76. package/dist/modules/messages/commands/shared.js +132 -132
  77. package/dist/modules/messages/commands/shared.js.map +2 -2
  78. package/dist/modules/perspectives/api/[tableId]/route.js +37 -26
  79. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  80. package/dist/modules/resources/commands/resources.js +125 -117
  81. package/dist/modules/resources/commands/resources.js.map +2 -2
  82. package/dist/modules/resources/commands/tags.js +7 -3
  83. package/dist/modules/resources/commands/tags.js.map +2 -2
  84. package/dist/modules/sales/api/quotes/send/route.js +12 -11
  85. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  86. package/dist/modules/sales/commands/documents.js +629 -478
  87. package/dist/modules/sales/commands/documents.js.map +2 -2
  88. package/dist/modules/sales/commands/payments.js +146 -146
  89. package/dist/modules/sales/commands/payments.js.map +2 -2
  90. package/dist/modules/sales/commands/returns.js +68 -60
  91. package/dist/modules/sales/commands/returns.js.map +2 -2
  92. package/dist/modules/staff/acl.js +10 -1
  93. package/dist/modules/staff/acl.js.map +2 -2
  94. package/dist/modules/staff/analytics.js +33 -0
  95. package/dist/modules/staff/analytics.js.map +7 -0
  96. package/dist/modules/staff/api/guards.js +31 -0
  97. package/dist/modules/staff/api/guards.js.map +7 -0
  98. package/dist/modules/staff/api/interceptors.js +96 -0
  99. package/dist/modules/staff/api/interceptors.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +170 -0
  101. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/my-projects/route.js +103 -0
  103. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +147 -0
  105. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +171 -0
  107. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +180 -0
  109. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +155 -0
  111. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +173 -0
  113. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +260 -0
  115. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/time-entries/route.js +188 -0
  117. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +159 -0
  119. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/time-projects/route.js +230 -0
  121. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +7 -0
  122. package/dist/modules/staff/backend/staff/timesheets/page.js +710 -0
  123. package/dist/modules/staff/backend/staff/timesheets/page.js.map +7 -0
  124. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +22 -0
  125. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +7 -0
  126. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +125 -0
  127. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +7 -0
  128. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +16 -0
  129. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +7 -0
  130. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +418 -0
  131. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +7 -0
  132. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +16 -0
  133. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +7 -0
  134. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +79 -0
  135. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +7 -0
  136. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +16 -0
  137. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +7 -0
  138. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +602 -0
  139. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +7 -0
  140. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +25 -0
  141. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +7 -0
  142. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +123 -0
  143. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +7 -0
  144. package/dist/modules/staff/cli.js +38 -1
  145. package/dist/modules/staff/cli.js.map +2 -2
  146. package/dist/modules/staff/commands/index.js +2 -0
  147. package/dist/modules/staff/commands/index.js.map +2 -2
  148. package/dist/modules/staff/commands/leave-requests.js +30 -28
  149. package/dist/modules/staff/commands/leave-requests.js.map +3 -3
  150. package/dist/modules/staff/commands/team-members.js +21 -20
  151. package/dist/modules/staff/commands/team-members.js.map +2 -2
  152. package/dist/modules/staff/commands/timesheets-entries.js +409 -0
  153. package/dist/modules/staff/commands/timesheets-entries.js.map +7 -0
  154. package/dist/modules/staff/commands/timesheets-projects.js +618 -0
  155. package/dist/modules/staff/commands/timesheets-projects.js.map +7 -0
  156. package/dist/modules/staff/data/enrichers.js +104 -0
  157. package/dist/modules/staff/data/enrichers.js.map +7 -0
  158. package/dist/modules/staff/data/entities.js +226 -1
  159. package/dist/modules/staff/data/entities.js.map +2 -2
  160. package/dist/modules/staff/data/validators.js +113 -1
  161. package/dist/modules/staff/data/validators.js.map +2 -2
  162. package/dist/modules/staff/events.js +13 -1
  163. package/dist/modules/staff/events.js.map +2 -2
  164. package/dist/modules/staff/lib/crud.js +7 -1
  165. package/dist/modules/staff/lib/crud.js.map +2 -2
  166. package/dist/modules/staff/lib/staffMemberResolver.js +15 -0
  167. package/dist/modules/staff/lib/staffMemberResolver.js.map +7 -0
  168. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js +60 -0
  169. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js.map +7 -0
  170. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js +260 -0
  171. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js.map +7 -0
  172. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js +41 -0
  173. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js.map +7 -0
  174. package/dist/modules/staff/lib/timesheets-projects/initials.js +10 -0
  175. package/dist/modules/staff/lib/timesheets-projects/initials.js.map +7 -0
  176. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js +12 -0
  177. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js.map +7 -0
  178. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js +55 -0
  179. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js.map +7 -0
  180. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js +66 -0
  181. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js.map +7 -0
  182. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +81 -0
  183. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +7 -0
  184. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js +58 -0
  185. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js.map +7 -0
  186. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js +152 -0
  187. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js.map +7 -0
  188. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +37 -0
  189. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +7 -0
  190. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js +57 -0
  191. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js.map +7 -0
  192. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js +50 -0
  193. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js.map +7 -0
  194. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js +163 -0
  195. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js.map +7 -0
  196. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js +209 -0
  197. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js.map +7 -0
  198. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js +52 -0
  199. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js.map +7 -0
  200. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +77 -0
  201. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +7 -0
  202. package/dist/modules/staff/lib/timesheets-ui/ListView.js +173 -0
  203. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +7 -0
  204. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js +32 -0
  205. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js.map +7 -0
  206. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +270 -0
  207. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +7 -0
  208. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js +57 -0
  209. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js.map +7 -0
  210. package/dist/modules/staff/lib/timesheets-ui/colors.js +43 -0
  211. package/dist/modules/staff/lib/timesheets-ui/colors.js.map +7 -0
  212. package/dist/modules/staff/migrations/Migration20260326135612.js +24 -0
  213. package/dist/modules/staff/migrations/Migration20260326135612.js.map +7 -0
  214. package/dist/modules/staff/migrations/Migration20260413102715.js +23 -0
  215. package/dist/modules/staff/migrations/Migration20260413102715.js.map +7 -0
  216. package/dist/modules/staff/migrations/Migration20260413111602.js +13 -0
  217. package/dist/modules/staff/migrations/Migration20260413111602.js.map +7 -0
  218. package/dist/modules/staff/migrations/Migration20260511112759.js +19 -0
  219. package/dist/modules/staff/migrations/Migration20260511112759.js.map +7 -0
  220. package/dist/modules/staff/search.js +35 -0
  221. package/dist/modules/staff/search.js.map +2 -2
  222. package/dist/modules/staff/setup.js +15 -1
  223. package/dist/modules/staff/setup.js.map +2 -2
  224. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js +16 -0
  225. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js.map +7 -0
  226. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js +126 -0
  227. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js.map +7 -0
  228. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js +26 -0
  229. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js.map +7 -0
  230. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js +15 -0
  231. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js.map +7 -0
  232. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +238 -0
  233. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +7 -0
  234. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js +26 -0
  235. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +145 -0
  237. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection-table.js +12 -0
  239. package/dist/modules/staff/widgets/injection-table.js.map +7 -0
  240. package/dist/modules/sync_excel/api/import/route.js +19 -17
  241. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  242. package/dist/modules/translations/commands/translations.js +22 -19
  243. package/dist/modules/translations/commands/translations.js.map +2 -2
  244. package/generated/entities/staff_time_entry/index.ts +17 -0
  245. package/generated/entities/staff_time_entry_segment/index.ts +10 -0
  246. package/generated/entities/staff_time_project/index.ts +16 -0
  247. package/generated/entities/staff_time_project_member/index.ts +13 -0
  248. package/generated/entities.ids.generated.ts +5 -1
  249. package/generated/entity-fields-registry.ts +64 -0
  250. package/package.json +7 -7
  251. package/src/helpers/integration/timesheetFixtures.ts +61 -0
  252. package/src/modules/attachments/api/library/[id]/route.ts +24 -17
  253. package/src/modules/attachments/api/route.ts +20 -14
  254. package/src/modules/auth/api/roles/acl/route.ts +11 -5
  255. package/src/modules/auth/api/sidebar/preferences/route.ts +33 -24
  256. package/src/modules/auth/api/users/acl/route.ts +17 -12
  257. package/src/modules/auth/commands/users.ts +96 -80
  258. package/src/modules/auth/services/sidebarPreferencesService.ts +40 -32
  259. package/src/modules/catalog/commands/categories.ts +61 -12
  260. package/src/modules/catalog/commands/products.ts +93 -60
  261. package/src/modules/catalog/commands/variants.ts +29 -16
  262. package/src/modules/currencies/commands/currencies.ts +27 -14
  263. package/src/modules/customer_accounts/api/admin/users.ts +31 -26
  264. package/src/modules/customer_accounts/api/password/reset-confirm.ts +5 -6
  265. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +14 -13
  266. package/src/modules/customers/commands/addresses.ts +35 -23
  267. package/src/modules/customers/commands/companies.ts +166 -165
  268. package/src/modules/customers/commands/deals.ts +2 -4
  269. package/src/modules/customers/commands/interactions.ts +20 -26
  270. package/src/modules/customers/commands/people.ts +18 -15
  271. package/src/modules/customers/commands/personCompanyLinks.ts +109 -100
  272. package/src/modules/customers/commands/pipeline-stages.ts +31 -27
  273. package/src/modules/customers/commands/pipelines.ts +29 -23
  274. package/src/modules/customers/commands/tags.ts +13 -5
  275. package/src/modules/dashboards/api/users/widgets/route.ts +0 -1
  276. package/src/modules/dashboards/api/widgets/data/route.ts +36 -1
  277. package/src/modules/data_sync/lib/sync-engine.ts +4 -5
  278. package/src/modules/data_sync/lib/sync-run-service.ts +57 -28
  279. package/src/modules/directory/commands/organizations.ts +203 -166
  280. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +26 -18
  281. package/src/modules/messages/commands/messages.ts +82 -80
  282. package/src/modules/messages/commands/shared.ts +138 -133
  283. package/src/modules/perspectives/api/[tableId]/route.ts +38 -27
  284. package/src/modules/resources/commands/resources.ts +127 -117
  285. package/src/modules/resources/commands/tags.ts +7 -3
  286. package/src/modules/sales/api/quotes/send/route.ts +17 -12
  287. package/src/modules/sales/commands/documents.ts +673 -481
  288. package/src/modules/sales/commands/payments.ts +158 -152
  289. package/src/modules/sales/commands/returns.ts +74 -63
  290. package/src/modules/staff/acl.ts +11 -0
  291. package/src/modules/staff/analytics.ts +30 -0
  292. package/src/modules/staff/api/guards.ts +59 -0
  293. package/src/modules/staff/api/interceptors.ts +122 -0
  294. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +191 -0
  295. package/src/modules/staff/api/timesheets/my-projects/route.ts +115 -0
  296. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +159 -0
  297. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +187 -0
  298. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +191 -0
  299. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +168 -0
  300. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +191 -0
  301. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +292 -0
  302. package/src/modules/staff/api/timesheets/time-entries/route.ts +193 -0
  303. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +167 -0
  304. package/src/modules/staff/api/timesheets/time-projects/route.ts +244 -0
  305. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +20 -0
  306. package/src/modules/staff/backend/staff/timesheets/page.tsx +899 -0
  307. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +12 -0
  308. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +141 -0
  309. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +12 -0
  310. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +579 -0
  311. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +12 -0
  312. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +90 -0
  313. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +23 -0
  314. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +765 -0
  315. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +138 -0
  316. package/src/modules/staff/cli.ts +40 -1
  317. package/src/modules/staff/commands/index.ts +2 -0
  318. package/src/modules/staff/commands/leave-requests.ts +37 -29
  319. package/src/modules/staff/commands/team-members.ts +25 -20
  320. package/src/modules/staff/commands/timesheets-entries.ts +504 -0
  321. package/src/modules/staff/commands/timesheets-projects.ts +699 -0
  322. package/src/modules/staff/data/enrichers.ts +134 -0
  323. package/src/modules/staff/data/entities.ts +198 -0
  324. package/src/modules/staff/data/validators.ts +129 -0
  325. package/src/modules/staff/events.ts +13 -0
  326. package/src/modules/staff/i18n/de.json +209 -1
  327. package/src/modules/staff/i18n/en.json +209 -1
  328. package/src/modules/staff/i18n/es.json +209 -1
  329. package/src/modules/staff/i18n/pl.json +209 -1
  330. package/src/modules/staff/lib/crud.ts +8 -0
  331. package/src/modules/staff/lib/staffMemberResolver.ts +22 -0
  332. package/src/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.ts +89 -0
  333. package/src/modules/staff/lib/timesheets-projects/computeProjectsKpis.ts +311 -0
  334. package/src/modules/staff/lib/timesheets-projects/dateBuckets.ts +37 -0
  335. package/src/modules/staff/lib/timesheets-projects/initials.ts +6 -0
  336. package/src/modules/staff/lib/timesheets-projects/kpiMath.ts +8 -0
  337. package/src/modules/staff/lib/timesheets-projects/listProjectMembersPreview.ts +83 -0
  338. package/src/modules/staff/lib/timesheets-projects-ui/HoursSparkline.tsx +75 -0
  339. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +110 -0
  340. package/src/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.tsx +73 -0
  341. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.tsx +185 -0
  342. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +53 -0
  343. package/src/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.tsx +63 -0
  344. package/src/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.ts +63 -0
  345. package/src/modules/staff/lib/timesheets-ui/AddRowDropdown.tsx +188 -0
  346. package/src/modules/staff/lib/timesheets-ui/CalendarPicker.tsx +229 -0
  347. package/src/modules/staff/lib/timesheets-ui/ColorPicker.tsx +65 -0
  348. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +99 -0
  349. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +230 -0
  350. package/src/modules/staff/lib/timesheets-ui/ProjectColorDot.tsx +40 -0
  351. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +327 -0
  352. package/src/modules/staff/lib/timesheets-ui/ViewSwitcher.tsx +60 -0
  353. package/src/modules/staff/lib/timesheets-ui/colors.ts +58 -0
  354. package/src/modules/staff/migrations/.snapshot-open-mercato.json +1148 -0
  355. package/src/modules/staff/migrations/Migration20260326135612.ts +26 -0
  356. package/src/modules/staff/migrations/Migration20260413102715.ts +25 -0
  357. package/src/modules/staff/migrations/Migration20260413111602.ts +13 -0
  358. package/src/modules/staff/migrations/Migration20260511112759.ts +21 -0
  359. package/src/modules/staff/search.ts +35 -0
  360. package/src/modules/staff/setup.ts +15 -0
  361. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.ts +17 -0
  362. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.tsx +158 -0
  363. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.ts +25 -0
  364. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/config.ts +15 -0
  365. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +297 -0
  366. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.ts +25 -0
  367. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +161 -0
  368. package/src/modules/staff/widgets/injection-table.ts +10 -0
  369. package/src/modules/sync_excel/api/import/route.ts +23 -18
  370. package/src/modules/translations/commands/translations.ts +49 -41
@@ -0,0 +1,311 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { StaffTimeProject, StaffTimeProjectMember } from '../../data/entities'
3
+ import {
4
+ addUtcDays,
5
+ getFirstDayOfMonthUtc,
6
+ getFirstDayOfNextMonthUtc,
7
+ getMondayUtc,
8
+ toDateOnlyString,
9
+ } from './dateBuckets'
10
+ import { deltaPct, minutesToHours } from './kpiMath'
11
+
12
+ type WeekRow = { bucket: 'current' | 'previous'; total_minutes: string | number }
13
+
14
+ export type KpiScope = {
15
+ em: EntityManager
16
+ organizationId: string
17
+ tenantId: string
18
+ now?: Date
19
+ }
20
+
21
+ export type KpiDelta = {
22
+ current: number
23
+ previous: number
24
+ deltaPct: number | null
25
+ }
26
+
27
+ export type ProjectKpisPmResult = {
28
+ role: 'pm'
29
+ totals: { total: number; active: number; onHold: number; completed: number }
30
+ hoursWeek: KpiDelta
31
+ hoursMonth: KpiDelta
32
+ teamActive: { count: number }
33
+ assignedToMe: { total: number; active: number }
34
+ }
35
+
36
+ export type ProjectKpisCollabResult = {
37
+ role: 'collab'
38
+ myProjects: { total: number; active: number }
39
+ myHoursWeek: KpiDelta
40
+ myHoursMonth: KpiDelta
41
+ }
42
+
43
+ export async function computePmProjectsKpis(
44
+ scope: KpiScope & { callerStaffMemberId?: string | null },
45
+ ): Promise<ProjectKpisPmResult> {
46
+ const em = scope.em.fork()
47
+ const now = scope.now ?? new Date()
48
+
49
+ const projects = await em.find(StaffTimeProject, {
50
+ tenantId: scope.tenantId,
51
+ organizationId: scope.organizationId,
52
+ deletedAt: null,
53
+ })
54
+
55
+ const totals = {
56
+ total: projects.length,
57
+ active: 0,
58
+ onHold: 0,
59
+ completed: 0,
60
+ }
61
+ for (const p of projects) {
62
+ if (p.status === 'active') totals.active += 1
63
+ else if (p.status === 'on_hold') totals.onHold += 1
64
+ else if (p.status === 'completed') totals.completed += 1
65
+ }
66
+
67
+ const monthStart = getFirstDayOfMonthUtc(now)
68
+ const nextMonthStart = getFirstDayOfNextMonthUtc(now)
69
+ const prevMonthStart = getFirstDayOfMonthUtc(addUtcDays(monthStart, -1))
70
+ const weekMonday = getMondayUtc(now)
71
+ const nextMonday = addUtcDays(weekMonday, 7)
72
+ const prevMonday = addUtcDays(weekMonday, -7)
73
+
74
+ const hoursRows = (await em.getConnection().execute(
75
+ `
76
+ SELECT bucket, COALESCE(SUM(duration_minutes), 0)::bigint AS total_minutes
77
+ FROM (
78
+ SELECT
79
+ CASE
80
+ WHEN date >= ?::date AND date < ?::date THEN 'current'
81
+ WHEN date >= ?::date AND date < ?::date THEN 'previous'
82
+ END AS bucket,
83
+ duration_minutes
84
+ FROM staff_time_entries
85
+ WHERE organization_id = ?
86
+ AND tenant_id = ?
87
+ AND deleted_at IS NULL
88
+ AND date >= ?::date
89
+ AND date < ?::date
90
+ ) t
91
+ WHERE bucket IS NOT NULL
92
+ GROUP BY bucket
93
+ `,
94
+ [
95
+ toDateOnlyString(monthStart),
96
+ toDateOnlyString(nextMonthStart),
97
+ toDateOnlyString(prevMonthStart),
98
+ toDateOnlyString(monthStart),
99
+ scope.organizationId,
100
+ scope.tenantId,
101
+ toDateOnlyString(prevMonthStart),
102
+ toDateOnlyString(nextMonthStart),
103
+ ],
104
+ )) as WeekRow[]
105
+
106
+ const weekRows = (await em.getConnection().execute(
107
+ `
108
+ SELECT bucket, COALESCE(SUM(duration_minutes), 0)::bigint AS total_minutes
109
+ FROM (
110
+ SELECT
111
+ CASE
112
+ WHEN date >= ?::date AND date < ?::date THEN 'current'
113
+ WHEN date >= ?::date AND date < ?::date THEN 'previous'
114
+ END AS bucket,
115
+ duration_minutes
116
+ FROM staff_time_entries
117
+ WHERE organization_id = ?
118
+ AND tenant_id = ?
119
+ AND deleted_at IS NULL
120
+ AND date >= ?::date
121
+ AND date < ?::date
122
+ ) t
123
+ WHERE bucket IS NOT NULL
124
+ GROUP BY bucket
125
+ `,
126
+ [
127
+ toDateOnlyString(weekMonday),
128
+ toDateOnlyString(nextMonday),
129
+ toDateOnlyString(prevMonday),
130
+ toDateOnlyString(weekMonday),
131
+ scope.organizationId,
132
+ scope.tenantId,
133
+ toDateOnlyString(prevMonday),
134
+ toDateOnlyString(nextMonday),
135
+ ],
136
+ )) as WeekRow[]
137
+
138
+ let currentHours = 0
139
+ let previousHours = 0
140
+ for (const row of hoursRows) {
141
+ const minutes = typeof row.total_minutes === 'string' ? Number(row.total_minutes) : row.total_minutes
142
+ if (row.bucket === 'current') currentHours = minutesToHours(minutes)
143
+ else previousHours = minutesToHours(minutes)
144
+ }
145
+
146
+ let weekCurrentHours = 0
147
+ let weekPreviousHours = 0
148
+ for (const row of weekRows) {
149
+ const minutes = typeof row.total_minutes === 'string' ? Number(row.total_minutes) : row.total_minutes
150
+ if (row.bucket === 'current') weekCurrentHours = minutesToHours(minutes)
151
+ else weekPreviousHours = minutesToHours(minutes)
152
+ }
153
+
154
+ const teamRows = (await em.getConnection().execute(
155
+ `
156
+ SELECT COUNT(DISTINCT staff_member_id)::bigint AS count
157
+ FROM staff_time_entries
158
+ WHERE organization_id = ?
159
+ AND tenant_id = ?
160
+ AND deleted_at IS NULL
161
+ AND date >= ?::date
162
+ AND date < ?::date
163
+ `,
164
+ [
165
+ scope.organizationId,
166
+ scope.tenantId,
167
+ toDateOnlyString(monthStart),
168
+ toDateOnlyString(nextMonthStart),
169
+ ],
170
+ )) as Array<{ count: string | number }>
171
+ const teamCount = teamRows.length ? Number(teamRows[0].count) : 0
172
+
173
+ const assignedToMe = { total: 0, active: 0 }
174
+ if (scope.callerStaffMemberId) {
175
+ const memberships = await em.find(StaffTimeProjectMember, {
176
+ staffMemberId: scope.callerStaffMemberId,
177
+ tenantId: scope.tenantId,
178
+ organizationId: scope.organizationId,
179
+ status: 'active',
180
+ deletedAt: null,
181
+ })
182
+ const assignedProjectIds = memberships.map((m) => m.timeProjectId)
183
+ const assignedProjects = projects.filter((p) => assignedProjectIds.includes(p.id))
184
+ assignedToMe.total = assignedProjects.length
185
+ assignedToMe.active = assignedProjects.filter((p) => p.status === 'active').length
186
+ }
187
+
188
+ return {
189
+ role: 'pm',
190
+ totals,
191
+ hoursWeek: {
192
+ current: weekCurrentHours,
193
+ previous: weekPreviousHours,
194
+ deltaPct: deltaPct(weekCurrentHours, weekPreviousHours),
195
+ },
196
+ hoursMonth: {
197
+ current: currentHours,
198
+ previous: previousHours,
199
+ deltaPct: deltaPct(currentHours, previousHours),
200
+ },
201
+ teamActive: { count: teamCount },
202
+ assignedToMe,
203
+ }
204
+ }
205
+
206
+ export async function computeCollabProjectsKpis(
207
+ scope: KpiScope & { staffMemberId: string },
208
+ ): Promise<ProjectKpisCollabResult> {
209
+ const em = scope.em.fork()
210
+ const now = scope.now ?? new Date()
211
+
212
+ const memberships = await em.find(StaffTimeProjectMember, {
213
+ staffMemberId: scope.staffMemberId,
214
+ tenantId: scope.tenantId,
215
+ organizationId: scope.organizationId,
216
+ status: 'active',
217
+ deletedAt: null,
218
+ })
219
+
220
+ const projectIds = memberships.map((m) => m.timeProjectId)
221
+ const myProjects = { total: projectIds.length, active: 0 }
222
+
223
+ if (projectIds.length > 0) {
224
+ const projects = await em.find(StaffTimeProject, {
225
+ id: { $in: projectIds },
226
+ tenantId: scope.tenantId,
227
+ organizationId: scope.organizationId,
228
+ status: 'active',
229
+ deletedAt: null,
230
+ })
231
+ myProjects.active = projects.length
232
+ }
233
+
234
+ const weekMonday = getMondayUtc(now)
235
+ const nextMonday = addUtcDays(weekMonday, 7)
236
+ const prevMonday = addUtcDays(weekMonday, -7)
237
+
238
+ const monthStart = getFirstDayOfMonthUtc(now)
239
+ const nextMonthStart = getFirstDayOfNextMonthUtc(now)
240
+ const prevMonthStart = getFirstDayOfMonthUtc(addUtcDays(monthStart, -1))
241
+
242
+ const rows = (await em.getConnection().execute(
243
+ `
244
+ SELECT bucket, COALESCE(SUM(duration_minutes), 0)::bigint AS total_minutes
245
+ FROM (
246
+ SELECT
247
+ CASE
248
+ WHEN date >= ?::date AND date < ?::date THEN 'week_current'
249
+ WHEN date >= ?::date AND date < ?::date THEN 'week_previous'
250
+ WHEN date >= ?::date AND date < ?::date THEN 'month_current'
251
+ WHEN date >= ?::date AND date < ?::date THEN 'month_previous'
252
+ END AS bucket,
253
+ duration_minutes
254
+ FROM staff_time_entries
255
+ WHERE organization_id = ?
256
+ AND tenant_id = ?
257
+ AND staff_member_id = ?
258
+ AND deleted_at IS NULL
259
+ AND date >= LEAST(?::date, ?::date)
260
+ AND date < GREATEST(?::date, ?::date)
261
+ ) t
262
+ WHERE bucket IS NOT NULL
263
+ GROUP BY bucket
264
+ `,
265
+ [
266
+ toDateOnlyString(weekMonday),
267
+ toDateOnlyString(nextMonday),
268
+ toDateOnlyString(prevMonday),
269
+ toDateOnlyString(weekMonday),
270
+ toDateOnlyString(monthStart),
271
+ toDateOnlyString(nextMonthStart),
272
+ toDateOnlyString(prevMonthStart),
273
+ toDateOnlyString(monthStart),
274
+ scope.organizationId,
275
+ scope.tenantId,
276
+ scope.staffMemberId,
277
+ toDateOnlyString(prevMonday),
278
+ toDateOnlyString(prevMonthStart),
279
+ toDateOnlyString(nextMonday),
280
+ toDateOnlyString(nextMonthStart),
281
+ ],
282
+ )) as Array<{ bucket: string; total_minutes: string | number }>
283
+
284
+ let weekCurrent = 0
285
+ let weekPrevious = 0
286
+ let monthCurrent = 0
287
+ let monthPrevious = 0
288
+ for (const row of rows) {
289
+ const minutes = typeof row.total_minutes === 'string' ? Number(row.total_minutes) : row.total_minutes
290
+ const hours = minutesToHours(minutes)
291
+ if (row.bucket === 'week_current') weekCurrent = hours
292
+ else if (row.bucket === 'week_previous') weekPrevious = hours
293
+ else if (row.bucket === 'month_current') monthCurrent = hours
294
+ else if (row.bucket === 'month_previous') monthPrevious = hours
295
+ }
296
+
297
+ return {
298
+ role: 'collab',
299
+ myProjects,
300
+ myHoursWeek: {
301
+ current: weekCurrent,
302
+ previous: weekPrevious,
303
+ deltaPct: deltaPct(weekCurrent, weekPrevious),
304
+ },
305
+ myHoursMonth: {
306
+ current: monthCurrent,
307
+ previous: monthPrevious,
308
+ deltaPct: deltaPct(monthCurrent, monthPrevious),
309
+ },
310
+ }
311
+ }
@@ -0,0 +1,37 @@
1
+ export function getMondayUtc(input: Date): Date {
2
+ const d = new Date(Date.UTC(input.getUTCFullYear(), input.getUTCMonth(), input.getUTCDate()))
3
+ const day = d.getUTCDay()
4
+ const diff = day === 0 ? -6 : 1 - day
5
+ d.setUTCDate(d.getUTCDate() + diff)
6
+ return d
7
+ }
8
+
9
+ export function addUtcDays(input: Date, days: number): Date {
10
+ const d = new Date(input)
11
+ d.setUTCDate(d.getUTCDate() + days)
12
+ return d
13
+ }
14
+
15
+ export function getFirstDayOfMonthUtc(input: Date): Date {
16
+ return new Date(Date.UTC(input.getUTCFullYear(), input.getUTCMonth(), 1))
17
+ }
18
+
19
+ export function getFirstDayOfNextMonthUtc(input: Date): Date {
20
+ return new Date(Date.UTC(input.getUTCFullYear(), input.getUTCMonth() + 1, 1))
21
+ }
22
+
23
+ export function getLastNWeekStarts(n: number, now: Date = new Date()): Date[] {
24
+ const currentMonday = getMondayUtc(now)
25
+ const starts: Date[] = []
26
+ for (let i = n - 1; i >= 0; i--) {
27
+ starts.push(addUtcDays(currentMonday, -i * 7))
28
+ }
29
+ return starts
30
+ }
31
+
32
+ export function toDateOnlyString(date: Date): string {
33
+ const y = date.getUTCFullYear()
34
+ const m = String(date.getUTCMonth() + 1).padStart(2, '0')
35
+ const d = String(date.getUTCDate()).padStart(2, '0')
36
+ return `${y}-${m}-${d}`
37
+ }
@@ -0,0 +1,6 @@
1
+ export function computeInitials(displayName: string): string {
2
+ const parts = displayName.trim().split(/\s+/).filter(Boolean)
3
+ if (parts.length === 0) return '?'
4
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
5
+ return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()
6
+ }
@@ -0,0 +1,8 @@
1
+ export function deltaPct(current: number, previous: number): number | null {
2
+ if (previous <= 0) return null
3
+ return Math.round(((current - previous) / previous) * 100 * 10) / 10
4
+ }
5
+
6
+ export function minutesToHours(minutes: number): number {
7
+ return Math.round((minutes / 60) * 10) / 10
8
+ }
@@ -0,0 +1,83 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { StaffTeamMember, StaffTimeProjectMember } from '../../data/entities'
3
+ import { computeInitials } from './initials'
4
+
5
+ export { computeInitials }
6
+
7
+ export type MembersPreviewScope = {
8
+ em: EntityManager
9
+ organizationId: string
10
+ tenantId: string
11
+ projectIds: string[]
12
+ maxPerProject?: number
13
+ }
14
+
15
+ export type MemberPreview = {
16
+ id: string
17
+ name: string
18
+ initials: string
19
+ avatarUrl: string | null
20
+ }
21
+
22
+ export type ProjectMembersPreview = {
23
+ total: number
24
+ preview: MemberPreview[]
25
+ myRole: string | null
26
+ }
27
+
28
+ const DEFAULT_MAX = 4
29
+
30
+ export async function listProjectMembersPreview(
31
+ scope: MembersPreviewScope & { callerStaffMemberId?: string | null },
32
+ ): Promise<Map<string, ProjectMembersPreview>> {
33
+ const maxPerProject = scope.maxPerProject ?? DEFAULT_MAX
34
+ const result = new Map<string, ProjectMembersPreview>()
35
+ for (const id of scope.projectIds) {
36
+ result.set(id, { total: 0, preview: [], myRole: null })
37
+ }
38
+ if (scope.projectIds.length === 0) return result
39
+
40
+ const em = scope.em.fork()
41
+ const memberships = await em.find(
42
+ StaffTimeProjectMember,
43
+ {
44
+ timeProjectId: { $in: scope.projectIds },
45
+ tenantId: scope.tenantId,
46
+ organizationId: scope.organizationId,
47
+ status: 'active',
48
+ deletedAt: null,
49
+ },
50
+ { orderBy: { createdAt: 'asc' } },
51
+ )
52
+
53
+ const staffMemberIds = Array.from(new Set(memberships.map((m) => m.staffMemberId)))
54
+ if (staffMemberIds.length === 0) return result
55
+
56
+ const teamMembers = await em.find(StaffTeamMember, {
57
+ id: { $in: staffMemberIds },
58
+ tenantId: scope.tenantId,
59
+ organizationId: scope.organizationId,
60
+ deletedAt: null,
61
+ })
62
+ const teamById = new Map(teamMembers.map((tm) => [tm.id, tm]))
63
+
64
+ for (const membership of memberships) {
65
+ const bucket = result.get(membership.timeProjectId)
66
+ if (!bucket) continue
67
+ bucket.total += 1
68
+ if (scope.callerStaffMemberId && membership.staffMemberId === scope.callerStaffMemberId) {
69
+ bucket.myRole = membership.role ?? null
70
+ }
71
+ if (bucket.preview.length >= maxPerProject) continue
72
+ const team = teamById.get(membership.staffMemberId)
73
+ if (!team) continue
74
+ bucket.preview.push({
75
+ id: team.id,
76
+ name: team.displayName,
77
+ initials: computeInitials(team.displayName),
78
+ avatarUrl: null,
79
+ })
80
+ }
81
+
82
+ return result
83
+ }
@@ -0,0 +1,75 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+
5
+ export type HoursSparklineProps = {
6
+ values: number[]
7
+ color?: string
8
+ width?: number
9
+ height?: number
10
+ ariaLabel: string
11
+ className?: string
12
+ }
13
+
14
+ const DEFAULT_COLOR = '#6366f1'
15
+
16
+ export function HoursSparkline({
17
+ values,
18
+ color = DEFAULT_COLOR,
19
+ width = 54,
20
+ height = 14,
21
+ ariaLabel,
22
+ className,
23
+ }: HoursSparklineProps) {
24
+ const nonEmpty = values.length > 0
25
+ const max = nonEmpty ? Math.max(...values, 0) : 0
26
+ const allZero = !nonEmpty || max === 0
27
+
28
+ if (allZero) {
29
+ return (
30
+ <svg
31
+ role="img"
32
+ aria-label={ariaLabel}
33
+ width={width}
34
+ height={height}
35
+ viewBox={`0 0 ${width} ${height}`}
36
+ className={className}
37
+ >
38
+ <line
39
+ x1={0}
40
+ x2={width}
41
+ y1={height / 2}
42
+ y2={height / 2}
43
+ className="stroke-border"
44
+ strokeWidth={1}
45
+ />
46
+ </svg>
47
+ )
48
+ }
49
+
50
+ const stepX = values.length === 1 ? 0 : width / (values.length - 1)
51
+ const points = values.map((value, idx) => {
52
+ const x = values.length === 1 ? width / 2 : idx * stepX
53
+ const y = height - (value / max) * (height - 2) - 1
54
+ return { x, y }
55
+ })
56
+
57
+ const linePath = points
58
+ .map((p, idx) => (idx === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`))
59
+ .join(' ')
60
+ const areaPath = `${linePath} L${points[points.length - 1].x},${height} L${points[0].x},${height} Z`
61
+
62
+ return (
63
+ <svg
64
+ role="img"
65
+ aria-label={ariaLabel}
66
+ width={width}
67
+ height={height}
68
+ viewBox={`0 0 ${width} ${height}`}
69
+ className={className}
70
+ >
71
+ <path d={areaPath} fill={color} opacity={0.15} />
72
+ <path d={linePath} fill="none" stroke={color} strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
73
+ </svg>
74
+ )
75
+ }
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { ProjectColorDot } from '../timesheets-ui/ProjectColorDot'
6
+ import { resolveProjectColorHex } from '../timesheets-ui/colors'
7
+ import { HoursSparkline } from './HoursSparkline'
8
+ import { ProjectMembersAvatarStack, type AvatarMember } from './ProjectMembersAvatarStack'
9
+
10
+ export type ProjectCardData = {
11
+ id: string
12
+ name: string
13
+ code: string | null
14
+ customerName: string | null
15
+ color: string | null
16
+ status: string
17
+ hoursWeek: number
18
+ hoursTrend: number[]
19
+ members: AvatarMember[]
20
+ memberCount: number
21
+ myRole: string | null
22
+ updatedAt: string | null
23
+ }
24
+
25
+ export type ProjectCardLabels = {
26
+ hoursPanelPm: string
27
+ hoursPanelCollab: string
28
+ sparklineAria: string
29
+ peopleCount: (count: number) => string
30
+ role: string
31
+ noCustomer: string
32
+ statuses: Record<string, string>
33
+ }
34
+
35
+ export type ProjectCardProps = {
36
+ data: ProjectCardData
37
+ labels: ProjectCardLabels
38
+ showTeam: boolean
39
+ href: string
40
+ }
41
+
42
+ const STATUS_BADGE_CLASSES: Record<string, string> = {
43
+ active: 'bg-lime-100 text-lime-800 dark:bg-lime-900/30 dark:text-lime-300',
44
+ on_hold: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
45
+ completed: 'bg-muted text-muted-foreground',
46
+ }
47
+
48
+ export function ProjectCard({ data, labels, showTeam, href }: ProjectCardProps) {
49
+ const badgeClass = STATUS_BADGE_CLASSES[data.status] ?? 'bg-muted text-muted-foreground'
50
+ const statusLabel = labels.statuses[data.status] ?? data.status
51
+ const stripeColor = resolveProjectColorHex(data.color, data.name)
52
+ const hoursPanelLabel = showTeam ? labels.hoursPanelPm : labels.hoursPanelCollab
53
+
54
+ return (
55
+ <Link
56
+ href={href}
57
+ className="group relative flex flex-col overflow-hidden rounded-lg border border-border bg-card transition-colors hover:border-foreground/30"
58
+ >
59
+ <div className="h-[3px] w-full" style={{ backgroundColor: stripeColor }} aria-hidden="true" />
60
+ <div className="flex flex-1 flex-col gap-3 p-4">
61
+ <div className="flex items-center justify-between gap-2">
62
+ <span
63
+ className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${badgeClass}`}
64
+ >
65
+ {statusLabel}
66
+ </span>
67
+ <ProjectColorDot colorKey={data.color} projectName={data.name} size="sm" />
68
+ </div>
69
+ <div className="flex flex-col gap-0.5">
70
+ <h3 className="truncate text-sm font-semibold text-foreground" title={data.name}>
71
+ {data.name}
72
+ </h3>
73
+ <p className="truncate font-mono text-[11px] text-muted-foreground">
74
+ {data.code ?? '—'}
75
+ {data.customerName ? ` · ${data.customerName}` : ''}
76
+ </p>
77
+ </div>
78
+ <div className="rounded-md border border-border/50 bg-muted/40 p-3">
79
+ <p className="text-[10px] uppercase tracking-wide text-muted-foreground">{hoursPanelLabel}</p>
80
+ <div className="mt-1 flex items-end justify-between gap-3">
81
+ <p className="text-xl font-semibold tabular-nums text-foreground">
82
+ {data.hoursWeek > 0 ? `${data.hoursWeek}h` : '—'}
83
+ </p>
84
+ <HoursSparkline
85
+ values={data.hoursTrend}
86
+ color={stripeColor}
87
+ width={80}
88
+ height={26}
89
+ ariaLabel={labels.sparklineAria}
90
+ />
91
+ </div>
92
+ </div>
93
+ <div className="mt-auto flex items-center justify-between pt-1">
94
+ {showTeam ? (
95
+ <ProjectMembersAvatarStack
96
+ members={data.members}
97
+ total={data.memberCount}
98
+ peopleCountLabel={labels.peopleCount(data.memberCount)}
99
+ />
100
+ ) : (
101
+ <p className="text-xs text-muted-foreground">
102
+ <span className="text-muted-foreground/70">{labels.role}: </span>
103
+ {data.myRole ?? '—'}
104
+ </p>
105
+ )}
106
+ </div>
107
+ </div>
108
+ </Link>
109
+ )
110
+ }