@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/bootstrap.js +46 -6
  4. package/dist/bootstrap.js.map +2 -2
  5. package/dist/generated/entities/organization/index.js +2 -0
  6. package/dist/generated/entities/organization/index.js.map +2 -2
  7. package/dist/generated/entity-fields-registry.js +1 -0
  8. package/dist/generated/entity-fields-registry.js.map +2 -2
  9. package/dist/helpers/integration/crmFixtures.js +4 -0
  10. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  11. package/dist/modules/attachments/api/library/route.js +2 -2
  12. package/dist/modules/attachments/api/library/route.js.map +2 -2
  13. package/dist/modules/attachments/api/route.js +2 -0
  14. package/dist/modules/attachments/api/route.js.map +2 -2
  15. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  16. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  17. package/dist/modules/attachments/lib/access.js +18 -0
  18. package/dist/modules/attachments/lib/access.js.map +2 -2
  19. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  20. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  21. package/dist/modules/audit_logs/data/entities.js +2 -1
  22. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  23. package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
  24. package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
  25. package/dist/modules/audit_logs/services/accessLogService.js +10 -0
  26. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  27. package/dist/modules/auth/api/admin/nav.js +9 -0
  28. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  29. package/dist/modules/auth/api/login.js +4 -13
  30. package/dist/modules/auth/api/login.js.map +2 -2
  31. package/dist/modules/auth/commands/users.js +20 -14
  32. package/dist/modules/auth/commands/users.js.map +2 -2
  33. package/dist/modules/auth/data/entities.js +4 -2
  34. package/dist/modules/auth/data/entities.js.map +2 -2
  35. package/dist/modules/auth/lib/backendChrome.js +35 -2
  36. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  37. package/dist/modules/auth/lib/consentIntegrity.js +3 -3
  38. package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
  39. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  40. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  41. package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
  42. package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
  43. package/dist/modules/auth/services/authService.js +5 -3
  44. package/dist/modules/auth/services/authService.js.map +2 -2
  45. package/dist/modules/auth/services/rbacService.js +3 -2
  46. package/dist/modules/auth/services/rbacService.js.map +2 -2
  47. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  48. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  49. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  50. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  51. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  52. package/dist/modules/communication_channels/encryption.js.map +1 -1
  53. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  54. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  55. package/dist/modules/currencies/api/currencies/route.js +4 -3
  56. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  57. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  58. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  59. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  60. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  61. package/dist/modules/customer_accounts/events.js +1 -1
  62. package/dist/modules/customer_accounts/events.js.map +1 -1
  63. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  64. package/dist/modules/customers/acl.js +1 -1
  65. package/dist/modules/customers/acl.js.map +1 -1
  66. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  67. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  68. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  69. package/dist/modules/customers/api/companies/route.js +4 -4
  70. package/dist/modules/customers/api/companies/route.js.map +2 -2
  71. package/dist/modules/customers/api/deals/route.js +43 -2
  72. package/dist/modules/customers/api/deals/route.js.map +2 -2
  73. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  74. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  75. package/dist/modules/customers/api/people/route.js +4 -4
  76. package/dist/modules/customers/api/people/route.js.map +2 -2
  77. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  78. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  79. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  80. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  81. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  82. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  83. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  84. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  85. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  86. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  87. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
  88. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  89. package/dist/modules/customers/cli.js +15 -9
  90. package/dist/modules/customers/cli.js.map +2 -2
  91. package/dist/modules/customers/commands/addresses.js +5 -5
  92. package/dist/modules/customers/commands/addresses.js.map +2 -2
  93. package/dist/modules/customers/commands/comments.js +5 -5
  94. package/dist/modules/customers/commands/comments.js.map +2 -2
  95. package/dist/modules/customers/commands/deals.js +2 -2
  96. package/dist/modules/customers/commands/deals.js.map +2 -2
  97. package/dist/modules/customers/commands/entity-roles.js +2 -1
  98. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  99. package/dist/modules/customers/commands/interactions.js +8 -5
  100. package/dist/modules/customers/commands/interactions.js.map +2 -2
  101. package/dist/modules/customers/commands/shared.js +21 -6
  102. package/dist/modules/customers/commands/shared.js.map +2 -2
  103. package/dist/modules/customers/commands/tags.js +3 -3
  104. package/dist/modules/customers/commands/tags.js.map +2 -2
  105. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  106. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  107. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  108. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  109. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  110. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  111. package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
  112. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  113. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  114. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  115. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  116. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  117. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  118. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  119. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  120. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  121. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  122. package/dist/modules/data_sync/api/run.js +1 -1
  123. package/dist/modules/data_sync/api/run.js.map +2 -2
  124. package/dist/modules/directory/api/organization-branding/route.js +214 -0
  125. package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
  126. package/dist/modules/directory/api/organizations/route.js +7 -0
  127. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  128. package/dist/modules/directory/backend/directory/branding/page.js +214 -0
  129. package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
  130. package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
  131. package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
  132. package/dist/modules/directory/commands/organizations.js +8 -1
  133. package/dist/modules/directory/commands/organizations.js.map +2 -2
  134. package/dist/modules/directory/data/entities.js +3 -0
  135. package/dist/modules/directory/data/entities.js.map +2 -2
  136. package/dist/modules/directory/data/validators.js +9 -0
  137. package/dist/modules/directory/data/validators.js.map +2 -2
  138. package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
  139. package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
  140. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  141. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  142. package/dist/modules/directory/utils/organizationScope.js +59 -27
  143. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  144. package/dist/modules/entities/api/definitions.batch.js +2 -1
  145. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  146. package/dist/modules/entities/api/entities.js +7 -0
  147. package/dist/modules/entities/api/entities.js.map +2 -2
  148. package/dist/modules/entities/api/records.js +26 -15
  149. package/dist/modules/entities/api/records.js.map +2 -2
  150. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  151. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  152. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  153. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  154. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  155. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  156. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  157. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  158. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  159. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  160. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  161. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  162. package/dist/modules/progress/api/jobs/route.js +1 -1
  163. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  164. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  165. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  166. package/dist/modules/query_index/data/entities.js +2 -1
  167. package/dist/modules/query_index/data/entities.js.map +2 -2
  168. package/dist/modules/query_index/lib/engine.js +4 -2
  169. package/dist/modules/query_index/lib/engine.js.map +2 -2
  170. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
  171. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
  172. package/dist/modules/resources/api/resources.js +2 -3
  173. package/dist/modules/resources/api/resources.js.map +2 -2
  174. package/dist/modules/sales/api/documents/factory.js +2 -2
  175. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  176. package/dist/modules/sales/commands/documents.js +7 -5
  177. package/dist/modules/sales/commands/documents.js.map +2 -2
  178. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
  179. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  180. package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
  181. package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
  182. package/dist/modules/staff/api/team-members.js +9 -2
  183. package/dist/modules/staff/api/team-members.js.map +2 -2
  184. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  185. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  186. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  187. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  188. package/dist/modules/staff/commands/team-members.js +1 -1
  189. package/dist/modules/staff/commands/team-members.js.map +2 -2
  190. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  191. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  192. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  193. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  194. package/dist/modules/sync_excel/api/import/route.js +1 -1
  195. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  196. package/dist/modules/workflows/api/definitions/route.js +3 -2
  197. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  198. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  199. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  200. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  201. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  202. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  203. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  204. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  205. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  206. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  207. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  208. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  209. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  210. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  211. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  212. package/generated/entities/organization/index.ts +1 -0
  213. package/generated/entity-fields-registry.ts +1 -0
  214. package/package.json +11 -12
  215. package/src/bootstrap.ts +65 -7
  216. package/src/helpers/integration/crmFixtures.ts +21 -1
  217. package/src/modules/attachments/AGENTS.md +79 -0
  218. package/src/modules/attachments/api/library/route.ts +2 -2
  219. package/src/modules/attachments/api/route.ts +2 -0
  220. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  221. package/src/modules/attachments/lib/access.ts +36 -0
  222. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  223. package/src/modules/audit_logs/data/entities.ts +1 -0
  224. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
  225. package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
  226. package/src/modules/audit_logs/services/accessLogService.ts +15 -0
  227. package/src/modules/auth/api/admin/nav.ts +9 -0
  228. package/src/modules/auth/api/login.ts +13 -13
  229. package/src/modules/auth/commands/users.ts +32 -15
  230. package/src/modules/auth/data/entities.ts +13 -1
  231. package/src/modules/auth/i18n/de.json +0 -1
  232. package/src/modules/auth/i18n/en.json +0 -1
  233. package/src/modules/auth/i18n/es.json +0 -1
  234. package/src/modules/auth/i18n/pl.json +0 -1
  235. package/src/modules/auth/lib/backendChrome.tsx +37 -1
  236. package/src/modules/auth/lib/consentIntegrity.ts +6 -3
  237. package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -10
  238. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  239. package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
  240. package/src/modules/auth/services/authService.ts +24 -4
  241. package/src/modules/auth/services/rbacService.ts +11 -2
  242. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  243. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  244. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  245. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  246. package/src/modules/communication_channels/data/entities.ts +2 -2
  247. package/src/modules/communication_channels/encryption.ts +1 -1
  248. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  249. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  250. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  251. package/src/modules/currencies/api/currencies/route.ts +4 -3
  252. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  253. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  254. package/src/modules/customer_accounts/events.ts +1 -1
  255. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  256. package/src/modules/customers/acl.ts +1 -1
  257. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  258. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  259. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  260. package/src/modules/customers/api/companies/route.ts +4 -4
  261. package/src/modules/customers/api/deals/route.ts +51 -2
  262. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  263. package/src/modules/customers/api/people/route.ts +4 -4
  264. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  265. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  266. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  267. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  268. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  269. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
  270. package/src/modules/customers/cli.ts +15 -15
  271. package/src/modules/customers/commands/addresses.ts +5 -5
  272. package/src/modules/customers/commands/comments.ts +5 -5
  273. package/src/modules/customers/commands/deals.ts +2 -2
  274. package/src/modules/customers/commands/entity-roles.ts +2 -1
  275. package/src/modules/customers/commands/interactions.ts +8 -5
  276. package/src/modules/customers/commands/shared.ts +26 -4
  277. package/src/modules/customers/commands/tags.ts +3 -3
  278. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  279. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  280. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  281. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
  282. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  283. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  284. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  285. package/src/modules/customers/i18n/de.json +43 -0
  286. package/src/modules/customers/i18n/en.json +43 -0
  287. package/src/modules/customers/i18n/es.json +43 -0
  288. package/src/modules/customers/i18n/pl.json +43 -0
  289. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  290. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  291. package/src/modules/data_sync/api/run.ts +1 -1
  292. package/src/modules/directory/api/organization-branding/route.ts +238 -0
  293. package/src/modules/directory/api/organizations/route.ts +7 -0
  294. package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
  295. package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
  296. package/src/modules/directory/commands/organizations.ts +9 -1
  297. package/src/modules/directory/data/entities.ts +3 -0
  298. package/src/modules/directory/data/validators.ts +12 -0
  299. package/src/modules/directory/i18n/de.json +21 -0
  300. package/src/modules/directory/i18n/en.json +21 -0
  301. package/src/modules/directory/i18n/es.json +21 -0
  302. package/src/modules/directory/i18n/pl.json +21 -0
  303. package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
  304. package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
  305. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  306. package/src/modules/directory/utils/organizationScope.ts +85 -30
  307. package/src/modules/entities/api/definitions.batch.ts +11 -7
  308. package/src/modules/entities/api/entities.ts +11 -0
  309. package/src/modules/entities/api/records.ts +46 -25
  310. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  311. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  312. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  313. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  314. package/src/modules/entities/i18n/de.json +1 -0
  315. package/src/modules/entities/i18n/en.json +1 -0
  316. package/src/modules/entities/i18n/es.json +1 -0
  317. package/src/modules/entities/i18n/pl.json +1 -0
  318. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  319. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  320. package/src/modules/progress/api/jobs/route.ts +1 -1
  321. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  322. package/src/modules/query_index/data/entities.ts +1 -0
  323. package/src/modules/query_index/lib/engine.ts +11 -5
  324. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
  325. package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
  326. package/src/modules/resources/api/resources.ts +2 -3
  327. package/src/modules/sales/api/documents/factory.ts +2 -2
  328. package/src/modules/sales/commands/documents.ts +7 -5
  329. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
  330. package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
  331. package/src/modules/staff/AGENTS.md +1 -1
  332. package/src/modules/staff/api/team-members.ts +9 -2
  333. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  334. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  335. package/src/modules/staff/commands/team-members.ts +5 -2
  336. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  337. package/src/modules/staff/i18n/de.json +1 -0
  338. package/src/modules/staff/i18n/en.json +1 -0
  339. package/src/modules/staff/i18n/es.json +1 -0
  340. package/src/modules/staff/i18n/pl.json +1 -0
  341. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  342. package/src/modules/sync_excel/api/import/route.ts +1 -1
  343. package/src/modules/workflows/api/definitions/route.ts +3 -2
  344. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  345. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  346. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  347. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  348. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  349. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  350. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -0,0 +1,389 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { CheckCircle } from 'lucide-react'
5
+ import { cn } from '@open-mercato/shared/lib/utils'
6
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
7
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
8
+ import { KpiCard, DeltaBadge, Sparkline } from '@open-mercato/ui/backend/charts'
9
+ import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
10
+ import { Button } from '@open-mercato/ui/primitives/button'
11
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
12
+ import type { DictionaryMap } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
13
+ import { PipelineStageBar } from './kpi/PipelineStageBar'
14
+
15
+ type DeltaDirection = 'up' | 'down' | 'unchanged'
16
+
17
+ type SummaryDelta = {
18
+ value: number
19
+ direction: DeltaDirection
20
+ }
21
+
22
+ type DealsSummaryResponse = {
23
+ baseCurrencyCode: string | null
24
+ convertedAll: boolean
25
+ missingRateCurrencies: string[]
26
+ pipelineValue: {
27
+ value: number
28
+ delta: SummaryDelta
29
+ stages: { stage: string | null; count: number; value: number }[]
30
+ }
31
+ activeDeals: {
32
+ value: number
33
+ delta: SummaryDelta
34
+ ownersCount: number
35
+ needAttention: number
36
+ owners: { id: string; count: number }[]
37
+ ownersOverflow: number
38
+ }
39
+ wonThisQuarter: {
40
+ value: number
41
+ delta: SummaryDelta
42
+ dealsClosed: number
43
+ avgDeal: number
44
+ }
45
+ winRate: {
46
+ value: number
47
+ deltaPp: number
48
+ direction: DeltaDirection
49
+ previousValue: number
50
+ series: { period: string; rate: number }[]
51
+ }
52
+ }
53
+
54
+ export type DealsKpiStripProps = {
55
+ ownerNames: Record<string, string>
56
+ stageDictionary: DictionaryMap
57
+ pipelineCount: number
58
+ className?: string
59
+ /** Bumped by the host when the active org scope changes — forces a KPI refetch so the cards never show another org's data. */
60
+ scopeVersion?: number
61
+ /** Bumped by the host on manual refresh / after mutations — forces a KPI refetch so totals stay in sync with the table. */
62
+ reloadToken?: number
63
+ onNeedsAttentionClick?: () => void
64
+ }
65
+
66
+ const compactNumberFormatter = new Intl.NumberFormat(undefined, {
67
+ notation: 'compact',
68
+ maximumFractionDigits: 1,
69
+ })
70
+
71
+ function formatCompact(value: number): string {
72
+ return compactNumberFormatter.format(value)
73
+ }
74
+
75
+ function buildCurrencySuffix(code: string | null, convertedAll: boolean): string {
76
+ if (!code) return convertedAll ? '' : '≈'
77
+ return convertedAll ? code : `≈ ${code}`
78
+ }
79
+
80
+ const KPI_TITLE_CLASS = 'text-xs font-semibold uppercase tracking-wide text-muted-foreground'
81
+
82
+ function DealKpiCard(props: React.ComponentProps<typeof KpiCard>) {
83
+ return <KpiCard titleClassName={KPI_TITLE_CLASS} {...props} />
84
+ }
85
+
86
+ function KpiDeltaBadge({
87
+ direction,
88
+ value,
89
+ unit,
90
+ title,
91
+ }: {
92
+ direction: DeltaDirection
93
+ value: number
94
+ unit?: string
95
+ title: string
96
+ }) {
97
+ if (direction === 'unchanged' && value === 0) {
98
+ return (
99
+ <span
100
+ className="inline-flex items-center rounded-md bg-status-neutral-bg px-2 py-0.5 text-xs font-medium text-status-neutral-text"
101
+ title={title}
102
+ >
103
+ --
104
+ </span>
105
+ )
106
+ }
107
+ return <DeltaBadge direction={direction} value={value} unit={unit} title={title} />
108
+ }
109
+
110
+ function isObject(value: unknown): value is Record<string, unknown> {
111
+ return typeof value === 'object' && value !== null
112
+ }
113
+
114
+ // Guard the summary payload before rendering: a non-conforming response (an unrelated
115
+ // endpoint mock, an error body, or a future contract drift) must surface the error card,
116
+ // never crash the whole deals page by dereferencing missing sections/arrays.
117
+ function isDealsSummaryResponse(value: unknown): value is DealsSummaryResponse {
118
+ if (!isObject(value)) return false
119
+ const { pipelineValue, activeDeals, wonThisQuarter, winRate } = value
120
+ return (
121
+ isObject(pipelineValue) && Array.isArray(pipelineValue.stages) &&
122
+ isObject(activeDeals) && Array.isArray(activeDeals.owners) &&
123
+ isObject(wonThisQuarter) &&
124
+ isObject(winRate) && Array.isArray(winRate.series)
125
+ )
126
+ }
127
+
128
+ export function DealsKpiStrip({
129
+ ownerNames,
130
+ stageDictionary,
131
+ pipelineCount,
132
+ className,
133
+ scopeVersion,
134
+ reloadToken,
135
+ onNeedsAttentionClick,
136
+ }: DealsKpiStripProps) {
137
+ const t = useT()
138
+ const locale = useLocale()
139
+ const pluralCat = React.useCallback((count: number): string => {
140
+ try {
141
+ return new Intl.PluralRules(locale).select(count)
142
+ } catch {
143
+ return count === 1 ? 'one' : 'other'
144
+ }
145
+ }, [locale])
146
+ const pf = React.useCallback((base: string, count: number): string => {
147
+ const cat = pluralCat(count)
148
+ const key = `${base}.${cat}`
149
+ const out = t(key, { count })
150
+ return out === key ? t(`${base}.other`, { count }) : out
151
+ }, [t, pluralCat])
152
+ const [data, setData] = React.useState<DealsSummaryResponse | null>(null)
153
+ const [loading, setLoading] = React.useState(true)
154
+ const [error, setError] = React.useState<string | null>(null)
155
+ const [retryToken, setRetryToken] = React.useState(0)
156
+ const previousScopeVersionRef = React.useRef(scopeVersion)
157
+
158
+ const retry = React.useCallback(() => {
159
+ setRetryToken((token) => token + 1)
160
+ }, [])
161
+
162
+ React.useEffect(() => {
163
+ let cancelled = false
164
+ const scopeChanged = previousScopeVersionRef.current !== scopeVersion
165
+ previousScopeVersionRef.current = scopeVersion
166
+ if (scopeChanged) setData(null)
167
+ setLoading(true)
168
+ setError(null)
169
+ apiCall<DealsSummaryResponse>('/api/customers/deals/summary')
170
+ .then((call) => {
171
+ if (cancelled) return
172
+ if (!call.ok || !isDealsSummaryResponse(call.result)) {
173
+ setError(t('customers.deals.list.kpi.error'))
174
+ return
175
+ }
176
+ setData(call.result)
177
+ })
178
+ .catch(() => {
179
+ if (cancelled) return
180
+ setError(t('customers.deals.list.kpi.error'))
181
+ })
182
+ .finally(() => {
183
+ if (!cancelled) setLoading(false)
184
+ })
185
+ return () => {
186
+ cancelled = true
187
+ }
188
+ }, [t, scopeVersion, reloadToken, retryToken])
189
+
190
+ const wrapperClassName = cn('space-y-2', className)
191
+ const gridClassName = 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4'
192
+
193
+ if (loading && !data) {
194
+ return (
195
+ <div className={wrapperClassName}>
196
+ <div className={gridClassName}>
197
+ <DealKpiCard loading title={t('customers.deals.list.kpi.pipelineValue')} value={null} />
198
+ <DealKpiCard loading title={t('customers.deals.list.kpi.activeDeals')} value={null} />
199
+ <DealKpiCard loading title={t('customers.deals.list.kpi.wonThisQuarter')} value={null} />
200
+ <DealKpiCard loading title={t('customers.deals.list.kpi.winRate')} value={null} />
201
+ </div>
202
+ </div>
203
+ )
204
+ }
205
+
206
+ if (!data) {
207
+ const errorMessage = error ?? t('customers.deals.list.kpi.error')
208
+ return (
209
+ <div className={wrapperClassName}>
210
+ <div className="flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
211
+ <p className="text-sm text-destructive">{errorMessage}</p>
212
+ <Button type="button" variant="destructive-outline" size="sm" onClick={retry}>
213
+ {t('customers.deals.list.kpi.retry')}
214
+ </Button>
215
+ </div>
216
+ </div>
217
+ )
218
+ }
219
+
220
+ const currencySuffix = buildCurrencySuffix(data.baseCurrencyCode, data.convertedAll)
221
+ const unassignedLabel = t('customers.deals.list.kpi.unassignedStage')
222
+ const deltaTooltip = t('customers.deals.list.kpi.deltaTooltip')
223
+ const deltaUnavailableTooltip = t('customers.deals.list.kpi.deltaUnavailable')
224
+ const scopeLabel = t('customers.deals.list.kpi.scopeAllPipelinesThisQuarter')
225
+ const unknownOwner = t('customers.deals.list.unknownOwner')
226
+ const currencyHint = !data.convertedAll
227
+ ? data.baseCurrencyCode
228
+ ? t('customers.deals.list.kpi.currencyApproxMissing', {
229
+ currencies: data.missingRateCurrencies.length ? data.missingRateCurrencies.join(', ') : currencySuffix,
230
+ })
231
+ : t('customers.deals.list.kpi.currencyApproxNoBase')
232
+ : null
233
+ const attentionLabel = pf('customers.deals.list.kpi.frag.needAttention', data.activeDeals.needAttention)
234
+
235
+ return (
236
+ <div className={wrapperClassName}>
237
+ {error ? (
238
+ <div className="flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
239
+ <p className="text-xs text-destructive">{error}</p>
240
+ <Button type="button" variant="destructive-outline" size="2xs" onClick={retry}>
241
+ {t('customers.deals.list.kpi.retry')}
242
+ </Button>
243
+ </div>
244
+ ) : null}
245
+ {loading ? (
246
+ <div className="flex items-center justify-end gap-2 text-xs text-muted-foreground">
247
+ <Spinner className="h-3 w-3" />
248
+ <span>{t('customers.deals.list.kpi.updating')}</span>
249
+ </div>
250
+ ) : null}
251
+ <div className={gridClassName}>
252
+ <DealKpiCard
253
+ title={t('customers.deals.list.kpi.pipelineValue')}
254
+ value={data.pipelineValue.value}
255
+ formatValue={formatCompact}
256
+ suffix={currencySuffix}
257
+ headerAction={
258
+ <KpiDeltaBadge
259
+ direction={data.pipelineValue.delta.direction}
260
+ value={data.pipelineValue.delta.value}
261
+ title={data.pipelineValue.delta.direction === 'unchanged' && data.pipelineValue.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}
262
+ />
263
+ }
264
+ footer={
265
+ <div className="space-y-2">
266
+ <p className="text-xs text-muted-foreground">{scopeLabel}</p>
267
+ <p className="text-xs text-muted-foreground">
268
+ {t('customers.deals.list.kpi.activeAcrossPipelines', {
269
+ deals: pf('customers.deals.list.kpi.frag.activeDeals', data.activeDeals.value),
270
+ pipelines: pf('customers.deals.list.kpi.frag.pipelines', pipelineCount),
271
+ })}
272
+ </p>
273
+ {currencyHint ? <p className="text-xs text-muted-foreground">{currencyHint}</p> : null}
274
+ <PipelineStageBar
275
+ stages={data.pipelineValue.stages}
276
+ stageDictionary={stageDictionary}
277
+ unassignedLabel={unassignedLabel}
278
+ />
279
+ </div>
280
+ }
281
+ />
282
+
283
+ <DealKpiCard
284
+ title={t('customers.deals.list.kpi.activeDeals')}
285
+ value={data.activeDeals.value}
286
+ formatValue={formatCompact}
287
+ headerAction={
288
+ <KpiDeltaBadge
289
+ direction={data.activeDeals.delta.direction}
290
+ value={data.activeDeals.delta.value}
291
+ title={data.activeDeals.delta.direction === 'unchanged' && data.activeDeals.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}
292
+ />
293
+ }
294
+ footer={
295
+ <div className="space-y-2">
296
+ <p className="text-xs text-muted-foreground">{scopeLabel}</p>
297
+ <div className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground">
298
+ <span>{pf('customers.deals.list.kpi.frag.owners', data.activeDeals.ownersCount)}</span>
299
+ <span aria-hidden="true">·</span>
300
+ {onNeedsAttentionClick && data.activeDeals.needAttention > 0 ? (
301
+ <Button
302
+ type="button"
303
+ variant="link"
304
+ size="2xs"
305
+ className="h-auto p-0 text-xs"
306
+ onClick={onNeedsAttentionClick}
307
+ >
308
+ {attentionLabel}
309
+ </Button>
310
+ ) : (
311
+ <span>{attentionLabel}</span>
312
+ )}
313
+ </div>
314
+ {data.activeDeals.owners.length > 0 ? (
315
+ <AvatarStack max={4} size="sm" overflowCount={data.activeDeals.ownersOverflow}>
316
+ {data.activeDeals.owners.map((owner) => {
317
+ const ownerLabel = ownerNames[owner.id]?.trim() || unknownOwner
318
+ return <Avatar key={owner.id} label={ownerLabel} size="sm" />
319
+ })}
320
+ </AvatarStack>
321
+ ) : null}
322
+ </div>
323
+ }
324
+ />
325
+
326
+ <DealKpiCard
327
+ title={t('customers.deals.list.kpi.wonThisQuarter')}
328
+ value={data.wonThisQuarter.value}
329
+ formatValue={formatCompact}
330
+ suffix={currencySuffix}
331
+ headerAction={
332
+ <KpiDeltaBadge
333
+ direction={data.wonThisQuarter.delta.direction}
334
+ value={data.wonThisQuarter.delta.value}
335
+ title={data.wonThisQuarter.delta.direction === 'unchanged' && data.wonThisQuarter.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}
336
+ />
337
+ }
338
+ footer={
339
+ <div className="space-y-1">
340
+ <p className="text-xs text-muted-foreground">{scopeLabel}</p>
341
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
342
+ <CheckCircle className="h-4 w-4 text-status-success-text" aria-hidden />
343
+ <span>
344
+ {pf('customers.deals.list.kpi.frag.dealsClosed', data.wonThisQuarter.dealsClosed)}
345
+ </span>
346
+ </div>
347
+ <p className="text-xs text-muted-foreground">
348
+ {t('customers.deals.list.kpi.avgDeal', {
349
+ value: `${formatCompact(data.wonThisQuarter.avgDeal)}${currencySuffix ? ` ${currencySuffix}` : ''}`,
350
+ })}
351
+ </p>
352
+ {currencyHint ? <p className="text-xs text-muted-foreground">{currencyHint}</p> : null}
353
+ </div>
354
+ }
355
+ />
356
+
357
+ <DealKpiCard
358
+ title={t('customers.deals.list.kpi.winRate')}
359
+ value={data.winRate.value}
360
+ suffix="%"
361
+ headerAction={
362
+ <KpiDeltaBadge
363
+ direction={data.winRate.direction}
364
+ value={Math.abs(data.winRate.deltaPp)}
365
+ unit="pp"
366
+ title={data.winRate.previousValue === 0 ? deltaUnavailableTooltip : deltaTooltip}
367
+ />
368
+ }
369
+ footer={
370
+ <div className="space-y-2">
371
+ <p className="text-xs text-muted-foreground">{scopeLabel}</p>
372
+ <p className="text-xs text-muted-foreground">
373
+ {t('customers.deals.list.kpi.fromLastQuarter', { value: data.winRate.previousValue })}
374
+ </p>
375
+ <div className="text-primary">
376
+ <Sparkline
377
+ values={data.winRate.series.map((point) => point.rate)}
378
+ ariaLabel={t('customers.deals.list.kpi.winRate')}
379
+ />
380
+ </div>
381
+ </div>
382
+ }
383
+ />
384
+ </div>
385
+ </div>
386
+ )
387
+ }
388
+
389
+ export default DealsKpiStrip
@@ -116,7 +116,6 @@ export function ConfirmDealLostDialog({
116
116
 
117
117
  <div className="space-y-6 px-7 py-6">
118
118
  <Alert variant="warning" className="rounded-md">
119
- <AlertTriangle className="size-4" />
120
119
  <AlertTitle>
121
120
  {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}
122
121
  </AlertTitle>
@@ -38,6 +38,9 @@ export type DealFormBaseValues = {
38
38
  companyIds?: string[]
39
39
  }
40
40
 
41
+ type PipelineOption = { id: string; name: string; isDefault: boolean }
42
+ type PipelineStageOption = { id: string; label: string; order: number }
43
+
41
44
  export type DealFormSubmitPayload = {
42
45
  base: DealFormBaseValues
43
46
  custom: Record<string, unknown>
@@ -64,6 +67,8 @@ export type DealFormProps = {
64
67
  showAssociationsGroup?: boolean
65
68
  showVersionHistory?: boolean
66
69
  showCancelAction?: boolean
70
+ initialPipelineOptions?: PipelineOption[]
71
+ initialPipelineStageOptions?: PipelineStageOption[]
67
72
  }
68
73
 
69
74
  type EntityOption = {
@@ -92,6 +97,84 @@ type EntityMultiSelectProps = {
92
97
 
93
98
  const DEAL_ENTITY_IDS = [E.customers.customer_deal]
94
99
  const CURRENCY_PRIORITY = ['EUR', 'USD', 'GBP', 'PLN'] as const
100
+ const PIPELINE_OPTIONS_TTL_MS = 60_000
101
+ const PIPELINE_STAGE_OPTIONS_TTL_MS = 30_000
102
+
103
+ type MetadataCacheEntry<T> = {
104
+ expiresAt: number
105
+ promise: Promise<T>
106
+ }
107
+
108
+ let pipelineOptionsCache: MetadataCacheEntry<PipelineOption[]> | null = null
109
+ const pipelineStageOptionsCache = new Map<string, MetadataCacheEntry<PipelineStageOption[]>>()
110
+
111
+ function isFreshCacheEntry<T>(entry: MetadataCacheEntry<T> | null | undefined): entry is MetadataCacheEntry<T> {
112
+ return Boolean(entry && entry.expiresAt > Date.now())
113
+ }
114
+
115
+ function normalizePipelineOptions(options: PipelineOption[] | undefined): PipelineOption[] {
116
+ const byId = new Map<string, PipelineOption>()
117
+ for (const option of options ?? []) {
118
+ if (!option.id) continue
119
+ byId.set(option.id, {
120
+ id: option.id,
121
+ name: option.name,
122
+ isDefault: option.isDefault === true,
123
+ })
124
+ }
125
+ return Array.from(byId.values())
126
+ }
127
+
128
+ function mergePipelineOptions(seed: PipelineOption[], loaded: PipelineOption[]): PipelineOption[] {
129
+ const byId = new Map<string, PipelineOption>()
130
+ for (const option of seed) byId.set(option.id, option)
131
+ for (const option of loaded) byId.set(option.id, option)
132
+ return Array.from(byId.values())
133
+ }
134
+
135
+ function normalizePipelineStageOptions(options: PipelineStageOption[] | undefined): PipelineStageOption[] {
136
+ return [...(options ?? [])]
137
+ .filter((option) => option.id)
138
+ .sort((left, right) => left.order - right.order)
139
+ }
140
+
141
+ async function fetchPipelineOptions(): Promise<PipelineOption[]> {
142
+ if (isFreshCacheEntry(pipelineOptionsCache)) return pipelineOptionsCache.promise
143
+ const entry: MetadataCacheEntry<PipelineOption[]> = {
144
+ expiresAt: Date.now() + PIPELINE_OPTIONS_TTL_MS,
145
+ promise: apiCall<{ items: PipelineOption[] }>('/api/customers/pipelines')
146
+ .then((call) => (call.ok && call.result?.items ? normalizePipelineOptions(call.result.items) : [])),
147
+ }
148
+ pipelineOptionsCache = entry
149
+ try {
150
+ return await entry.promise
151
+ } catch (error) {
152
+ if (pipelineOptionsCache === entry) pipelineOptionsCache = null
153
+ throw error
154
+ }
155
+ }
156
+
157
+ async function fetchPipelineStageOptions(pipelineId: string): Promise<PipelineStageOption[]> {
158
+ const cached = pipelineStageOptionsCache.get(pipelineId)
159
+ if (isFreshCacheEntry(cached)) return cached.promise
160
+ const entry: MetadataCacheEntry<PipelineStageOption[]> = {
161
+ expiresAt: Date.now() + PIPELINE_STAGE_OPTIONS_TTL_MS,
162
+ promise: apiCall<{ items: PipelineStageOption[] }>(`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`)
163
+ .then((call) => (call.ok && call.result?.items ? normalizePipelineStageOptions(call.result.items) : [])),
164
+ }
165
+ pipelineStageOptionsCache.set(pipelineId, entry)
166
+ try {
167
+ return await entry.promise
168
+ } catch (error) {
169
+ if (pipelineStageOptionsCache.get(pipelineId) === entry) pipelineStageOptionsCache.delete(pipelineId)
170
+ throw error
171
+ }
172
+ }
173
+
174
+ export function resetDealPipelineMetadataCacheForTests() {
175
+ pipelineOptionsCache = null
176
+ pipelineStageOptionsCache.clear()
177
+ }
95
178
 
96
179
  const schema = z.object({
97
180
  title: z
@@ -647,6 +730,8 @@ export function DealForm({
647
730
  showAssociationsGroup = true,
648
731
  showVersionHistory = true,
649
732
  showCancelAction = true,
733
+ initialPipelineOptions,
734
+ initialPipelineStageOptions,
650
735
  }: DealFormProps) {
651
736
  const t = useT()
652
737
  const [pending, setPending] = React.useState(false)
@@ -733,25 +818,36 @@ export function DealForm({
733
818
  const disabled = pending || isSubmitting
734
819
  const canDelete = mode === 'edit' && typeof onDelete === 'function'
735
820
 
736
- type PipelineOption = { id: string; name: string; isDefault: boolean }
737
- type PipelineStageOption = { id: string; label: string; order: number }
821
+ const mountedRef = React.useRef(false)
822
+ const seedPipelineOptions = React.useMemo(
823
+ () => normalizePipelineOptions(initialPipelineOptions),
824
+ [initialPipelineOptions],
825
+ )
826
+ const seedPipelineStageOptions = React.useMemo(
827
+ () => Array.isArray(initialPipelineStageOptions) ? normalizePipelineStageOptions(initialPipelineStageOptions) : null,
828
+ [initialPipelineStageOptions],
829
+ )
830
+
831
+ const [pipelines, setPipelines] = React.useState<PipelineOption[]>(() => seedPipelineOptions)
832
+ const [pipelineStages, setPipelineStages] = React.useState<PipelineStageOption[]>(() => seedPipelineStageOptions ?? [])
738
833
 
739
- const [pipelines, setPipelines] = React.useState<PipelineOption[]>([])
740
- const [pipelineStages, setPipelineStages] = React.useState<PipelineStageOption[]>([])
834
+ React.useEffect(() => {
835
+ mountedRef.current = true
836
+ return () => {
837
+ mountedRef.current = false
838
+ }
839
+ }, [])
741
840
 
742
841
  const loadStagesForPipeline = React.useCallback(async (pipelineId: string) => {
743
842
  if (!pipelineId) {
744
- setPipelineStages([])
843
+ if (mountedRef.current) setPipelineStages([])
745
844
  return
746
845
  }
747
846
  try {
748
- const call = await apiCall<{ items: PipelineStageOption[] }>(`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`)
749
- if (call.ok && call.result?.items) {
750
- const sorted = [...call.result.items].sort((a, b) => a.order - b.order)
751
- setPipelineStages(sorted)
752
- }
847
+ const stages = await fetchPipelineStageOptions(pipelineId)
848
+ if (mountedRef.current) setPipelineStages(stages)
753
849
  } catch {
754
- setPipelineStages([])
850
+ if (mountedRef.current) setPipelineStages([])
755
851
  }
756
852
  }, [])
757
853
 
@@ -759,24 +855,30 @@ export function DealForm({
759
855
  let cancelled = false
760
856
  ;(async () => {
761
857
  try {
762
- const call = await apiCall<{ items: PipelineOption[] }>('/api/customers/pipelines')
763
- if (cancelled) return
764
- if (call.ok && call.result?.items) {
765
- setPipelines(call.result.items)
766
- }
858
+ const loaded = await fetchPipelineOptions()
859
+ if (cancelled || !mountedRef.current) return
860
+ setPipelines(mergePipelineOptions(seedPipelineOptions, loaded))
767
861
  } catch {
768
- // ignore
862
+ if (!cancelled && mountedRef.current && seedPipelineOptions.length > 0) {
863
+ setPipelines(seedPipelineOptions)
864
+ }
769
865
  }
770
866
  })().catch(() => {})
771
867
  return () => { cancelled = true }
772
- }, [])
868
+ }, [seedPipelineOptions])
773
869
 
774
870
  React.useEffect(() => {
775
871
  const pid = initialValues?.pipelineId
776
872
  if (typeof pid === 'string' && pid.length) {
873
+ if (seedPipelineStageOptions) {
874
+ setPipelineStages(seedPipelineStageOptions)
875
+ return
876
+ }
777
877
  loadStagesForPipeline(pid).catch(() => {})
878
+ } else {
879
+ setPipelineStages([])
778
880
  }
779
- }, [initialValues?.pipelineId, loadStagesForPipeline])
881
+ }, [initialValues?.pipelineId, loadStagesForPipeline, seedPipelineStageOptions])
780
882
 
781
883
  const baseFields = React.useMemo<CrudField[]>(() => [
782
884
  {
@@ -13,6 +13,7 @@ import {
13
13
  History,
14
14
  Paperclip,
15
15
  Plus,
16
+ MapPin,
16
17
  } from 'lucide-react'
17
18
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
18
19
 
@@ -21,6 +22,7 @@ export type PersonTabId =
21
22
  | 'emails'
22
23
  | 'deals'
23
24
  | 'companies'
25
+ | 'addresses'
24
26
  | 'tasks'
25
27
  | 'changelog'
26
28
  | 'files'
@@ -40,13 +42,14 @@ type PersonDetailTabsProps = {
40
42
  activitiesCount?: number
41
43
  dealsCount?: number
42
44
  companiesCount?: number
45
+ addressesCount?: number
43
46
  tasksCount?: number
44
47
  filesCount?: number
45
48
  sectionAction?: SectionAction | null
46
49
  children: React.ReactNode
47
50
  }
48
51
 
49
- const SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'tasks', 'changelog', 'files'])
52
+ const SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'])
50
53
 
51
54
  export function resolveLegacyTab(tab: string | null | undefined): PersonTabId {
52
55
  if (!tab) return 'activities'
@@ -77,6 +80,7 @@ export function PersonDetailTabs({
77
80
  activitiesCount = 0,
78
81
  dealsCount = 0,
79
82
  companiesCount = 0,
83
+ addressesCount = 0,
80
84
  tasksCount = 0,
81
85
  filesCount = 0,
82
86
  sectionAction = null,
@@ -109,6 +113,12 @@ export function PersonDetailTabs({
109
113
  icon: <Building2 className="size-4" />,
110
114
  badge: <CountBadge count={companiesCount} />,
111
115
  },
116
+ {
117
+ id: 'addresses',
118
+ label: t('customers.people.detail.tabs.addresses', 'Addresses'),
119
+ icon: <MapPin className="size-4" />,
120
+ badge: <CountBadge count={addressesCount} />,
121
+ },
112
122
  {
113
123
  id: 'tasks',
114
124
  label: t('customers.people.detail.tabs.tasks', 'Tasks'),
@@ -128,7 +138,7 @@ export function PersonDetailTabs({
128
138
  badge: <CountBadge count={filesCount} />,
129
139
  },
130
140
  ],
131
- [t, activitiesCount, dealsCount, companiesCount, tasksCount, filesCount],
141
+ [t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],
132
142
  )
133
143
 
134
144
  const allTabs: TabDef[] = React.useMemo(
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Users, Phone, Check, Mail, Calendar, AlertTriangle, X, StickyNote } from 'lucide-react'
4
+ import { Users, Phone, Check, Mail, Calendar, X, StickyNote } from 'lucide-react'
5
5
  import { cn } from '@open-mercato/shared/lib/utils'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { validatePhoneNumber } from '@open-mercato/shared/lib/phone'
@@ -498,7 +498,6 @@ export function ScheduleActivityDialog({
498
498
  {/* Conflict warning */}
499
499
  {state.conflict && (
500
500
  <Alert variant="warning" className="rounded-lg">
501
- <AlertTriangle className="size-5" />
502
501
  <AlertTitle>
503
502
  {t('customers.schedule.conflict.title', 'Calendar conflict')}
504
503
  </AlertTitle>