@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3287.1.450f4ffb56

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 (266) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/domain_mapping/index.js +31 -0
  3. package/dist/generated/entities/domain_mapping/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +16 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/auth.js +1 -1
  9. package/dist/helpers/integration/auth.js.map +2 -2
  10. package/dist/helpers/integration/authUi.js +1 -1
  11. package/dist/helpers/integration/authUi.js.map +2 -2
  12. package/dist/helpers/integration/salesUi.js +18 -17
  13. package/dist/helpers/integration/salesUi.js.map +2 -2
  14. package/dist/modules/auth/frontend/login.js +4 -4
  15. package/dist/modules/auth/frontend/login.js.map +2 -2
  16. package/dist/modules/auth/frontend/reset/[token]/page.js +5 -5
  17. package/dist/modules/auth/frontend/reset/[token]/page.js.map +2 -2
  18. package/dist/modules/auth/frontend/reset.js +2 -2
  19. package/dist/modules/auth/frontend/reset.js.map +2 -2
  20. package/dist/modules/customer_accounts/acl.js +2 -1
  21. package/dist/modules/customer_accounts/acl.js.map +2 -2
  22. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  23. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  25. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  26. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  27. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  28. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  29. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  30. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  31. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  32. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  33. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  34. package/dist/modules/customer_accounts/api/login.js +14 -3
  35. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  37. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  39. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/signup.js +32 -5
  41. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  56. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  57. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  58. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  59. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  60. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  61. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  62. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  63. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  64. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  65. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  66. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +4 -3
  67. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  68. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +6 -5
  69. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  70. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  71. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  72. package/dist/modules/customer_accounts/data/entities.js +85 -2
  73. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  74. package/dist/modules/customer_accounts/data/guards.js +130 -0
  75. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  76. package/dist/modules/customer_accounts/data/validators.js +19 -0
  77. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  78. package/dist/modules/customer_accounts/di.js +23 -1
  79. package/dist/modules/customer_accounts/di.js.map +2 -2
  80. package/dist/modules/customer_accounts/events.js +9 -2
  81. package/dist/modules/customer_accounts/events.js.map +2 -2
  82. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  83. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  84. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  85. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  86. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  87. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  88. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  89. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  90. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  91. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  92. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  93. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  94. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  95. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  96. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  97. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  98. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  99. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  100. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  101. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  102. package/dist/modules/customer_accounts/notifications.js +76 -0
  103. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  104. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  105. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  106. package/dist/modules/customer_accounts/setup.js +48 -1
  107. package/dist/modules/customer_accounts/setup.js.map +2 -2
  108. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  109. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  110. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  111. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  112. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  113. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  114. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  115. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  116. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  117. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  118. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  119. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  120. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  121. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  122. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  123. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  124. package/dist/modules/customers/api/companies/route.js +4 -9
  125. package/dist/modules/customers/api/companies/route.js.map +2 -2
  126. package/dist/modules/customers/api/deals/route.js +4 -9
  127. package/dist/modules/customers/api/deals/route.js.map +2 -2
  128. package/dist/modules/customers/api/people/route.js +4 -9
  129. package/dist/modules/customers/api/people/route.js.map +2 -2
  130. package/dist/modules/customers/api/utils.js +0 -12
  131. package/dist/modules/customers/api/utils.js.map +2 -2
  132. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  133. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  134. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  135. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  136. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  137. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  138. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  139. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  140. package/dist/modules/customers/components/formConfig.js +2 -2
  141. package/dist/modules/customers/components/formConfig.js.map +2 -2
  142. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  143. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  144. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  145. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  146. package/dist/modules/customers/widgets/injection-table.js +6 -0
  147. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  148. package/dist/modules/integrations/backend/integrations/[id]/page.js +2 -3
  149. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  150. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  151. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  152. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +5 -3
  153. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  154. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +3 -3
  155. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +2 -2
  156. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +4 -2
  157. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +2 -2
  158. package/dist/modules/query_index/lib/engine.js +31 -8
  159. package/dist/modules/query_index/lib/engine.js.map +2 -2
  160. package/dist/modules/sales/backend/sales/documents/[id]/page.js +3 -5
  161. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  162. package/dist/modules/sales/components/ProviderFieldInput.js +2 -2
  163. package/dist/modules/sales/components/ProviderFieldInput.js.map +2 -2
  164. package/dist/modules/sales/components/documents/SalesDocumentForm.js +3 -4
  165. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  166. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js +3 -4
  167. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js.map +2 -2
  168. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -2
  169. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  170. package/dist/modules/workflows/frontend/checkout-demo/page.js +2 -2
  171. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  172. package/generated/entities/domain_mapping/index.ts +14 -0
  173. package/generated/entities.ids.generated.ts +2 -1
  174. package/generated/entity-fields-registry.ts +16 -0
  175. package/package.json +5 -5
  176. package/src/helpers/integration/auth.ts +1 -1
  177. package/src/helpers/integration/authUi.ts +1 -1
  178. package/src/helpers/integration/salesUi.ts +27 -23
  179. package/src/modules/auth/frontend/login.tsx +4 -3
  180. package/src/modules/auth/frontend/reset/[token]/page.tsx +4 -4
  181. package/src/modules/auth/frontend/reset.tsx +2 -2
  182. package/src/modules/customer_accounts/acl.ts +1 -0
  183. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  184. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  185. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  186. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  187. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  188. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  189. package/src/modules/customer_accounts/api/login.ts +14 -3
  190. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  191. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  192. package/src/modules/customer_accounts/api/signup.ts +36 -5
  193. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  194. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  195. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  196. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  197. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  198. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  199. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  200. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  201. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  202. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  203. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  204. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  205. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +3 -2
  206. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +5 -4
  207. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  208. package/src/modules/customer_accounts/data/entities.ts +80 -1
  209. package/src/modules/customer_accounts/data/guards.ts +155 -0
  210. package/src/modules/customer_accounts/data/validators.ts +26 -0
  211. package/src/modules/customer_accounts/di.ts +35 -1
  212. package/src/modules/customer_accounts/events.ts +8 -1
  213. package/src/modules/customer_accounts/i18n/de.json +78 -0
  214. package/src/modules/customer_accounts/i18n/en.json +78 -0
  215. package/src/modules/customer_accounts/i18n/es.json +78 -0
  216. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  217. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  218. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  219. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  220. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  221. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  222. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  223. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  224. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  225. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  226. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  227. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  228. package/src/modules/customer_accounts/notifications.ts +76 -0
  229. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  230. package/src/modules/customer_accounts/setup.ts +81 -1
  231. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  232. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  233. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  234. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  235. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  236. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  237. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  238. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  239. package/src/modules/customers/api/companies/route.ts +4 -9
  240. package/src/modules/customers/api/deals/route.ts +4 -9
  241. package/src/modules/customers/api/people/route.ts +4 -9
  242. package/src/modules/customers/api/utils.ts +0 -14
  243. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  244. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  245. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  246. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  247. package/src/modules/customers/components/formConfig.tsx +2 -2
  248. package/src/modules/customers/i18n/de.json +31 -0
  249. package/src/modules/customers/i18n/en.json +31 -0
  250. package/src/modules/customers/i18n/es.json +31 -0
  251. package/src/modules/customers/i18n/pl.json +31 -0
  252. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  253. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  254. package/src/modules/customers/widgets/injection-table.ts +6 -0
  255. package/src/modules/integrations/backend/integrations/[id]/page.tsx +2 -2
  256. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  257. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +5 -3
  258. package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +3 -3
  259. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +4 -2
  260. package/src/modules/query_index/lib/engine.ts +46 -8
  261. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +3 -5
  262. package/src/modules/sales/components/ProviderFieldInput.tsx +2 -2
  263. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +2 -3
  264. package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.tsx +3 -4
  265. package/src/modules/workflows/backend/tasks/[id]/page.tsx +2 -2
  266. package/src/modules/workflows/frontend/checkout-demo/page.tsx +2 -2
@@ -2,13 +2,13 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useRouter } from 'next/navigation'
5
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { DataTable, type DataTableExportFormat, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
8
8
  import type { ColumnDef } from '@tanstack/react-table'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
10
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
- import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'
13
13
  import { groupBulkDeleteFailures, runBulkDelete } from '@open-mercato/ui/backend/utils/bulkDelete'
14
14
  import { coalesceLastOperations } from '@open-mercato/ui/backend/operations/store'
@@ -18,10 +18,12 @@ import { E } from '#generated/entities.ids.generated'
18
18
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
19
  import { useT } from '@open-mercato/shared/lib/i18n/context'
20
20
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
21
- import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
22
21
  import type { FilterOption } from '@open-mercato/ui/backend/FilterOverlay'
23
- import type { AdvancedFilterState } from '@open-mercato/shared/lib/query/advanced-filter'
24
- import { serializeAdvancedFilter } from '@open-mercato/shared/lib/query/advanced-filter'
22
+ import type { FilterFieldDef, FilterOption as AdvancedFilterOption } from '@open-mercato/shared/lib/query/advanced-filter'
23
+ import type { AdvancedFilterTree } from '@open-mercato/shared/lib/query/advanced-filter-tree'
24
+ import { createEmptyTree, makeRuleTree } from '@open-mercato/shared/lib/query/advanced-filter-tree'
25
+ import { deserializeAdvancedFilter, deserializeTree, flatToTree, mapDictionaryColorToTone, serializeTree } from '@open-mercato/shared/lib/query/advanced-filter'
26
+ import { useCurrentUserId } from '@open-mercato/ui/backend/utils/useCurrentUserId'
25
27
  import {
26
28
  DictionaryValue,
27
29
  createEmptyCustomerDictionaryMaps,
@@ -38,10 +40,55 @@ import {
38
40
  normalizeCustomFieldFilterOptions,
39
41
  supportsCustomFieldColumn,
40
42
  } from '@open-mercato/ui/backend/utils/customFieldColumns'
43
+ import { useAutoDiscoveredFields } from '@open-mercato/ui/backend/utils/useAutoDiscoveredFields'
44
+ import { useAdvancedFilterTree } from '@open-mercato/ui/backend/hooks/useAdvancedFilter'
45
+ import { AdvancedFilterPanel } from '@open-mercato/ui/backend/filters/AdvancedFilterPanel'
46
+ import { ActiveFilterChips } from '@open-mercato/ui/backend/filters/ActiveFilterChips'
47
+ import type { FilterPreset } from '@open-mercato/ui/backend/filters/QuickFilters'
41
48
  import { useQueryClient } from '@tanstack/react-query'
42
49
  import { ensureCustomerDictionary } from '../../../components/detail/hooks/useCustomerDictionary'
50
+ import {
51
+ ensureCurrentUserFilterOption,
52
+ fetchAssignableStaffMembers,
53
+ mapAssignableStaffToFilterOptions,
54
+ } from '../../../components/detail/assignableStaff'
43
55
  import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
44
56
 
57
+ type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
58
+
59
+ function makePeoplePresets(): FilterPreset[] {
60
+ return [
61
+ {
62
+ id: 'recently-active',
63
+ labelKey: 'customers.people.presets.recentlyActive',
64
+ iconName: 'clock',
65
+ build: ({ now }) => {
66
+ const cutoff = new Date(now.getTime() - 7 * 24 * 3600 * 1000).toISOString().slice(0, 10)
67
+ return makeRuleTree({ field: 'next_interaction_at', operator: 'is_after', value: cutoff })
68
+ },
69
+ },
70
+ {
71
+ id: 'my-contacts',
72
+ labelKey: 'customers.people.presets.myContacts',
73
+ requiresUser: true,
74
+ build: ({ userId }) => makeRuleTree({ field: 'owner_user_id', operator: 'is', value: userId }),
75
+ },
76
+ {
77
+ id: 'hot-leads',
78
+ labelKey: 'customers.people.presets.hotLeads',
79
+ build: () => makeRuleTree({ field: 'lifecycle_stage', operator: 'is', value: 'lead' }),
80
+ },
81
+ {
82
+ id: 'stale-30',
83
+ labelKey: 'customers.people.presets.stale30',
84
+ build: ({ now }) => {
85
+ const cutoff = new Date(now.getTime() - 30 * 24 * 3600 * 1000).toISOString().slice(0, 10)
86
+ return makeRuleTree({ field: 'next_interaction_at', operator: 'is_before', value: cutoff })
87
+ },
88
+ },
89
+ ]
90
+ }
91
+
45
92
  type PersonRow = {
46
93
  id: string
47
94
  name: string
@@ -66,6 +113,7 @@ type PersonRow = {
66
113
  nextInteractionColor?: string | null
67
114
  organizationId?: string | null
68
115
  source?: string | null
116
+ ownerUserId?: string | null
69
117
  } & Record<string, unknown>
70
118
 
71
119
  type PeopleResponse = {
@@ -155,13 +203,51 @@ export default function CustomersPeoplePage() {
155
203
  const [total, setTotal] = React.useState(0)
156
204
  const [totalPages, setTotalPages] = React.useState(1)
157
205
  const [search, setSearch] = React.useState('')
158
- const [filterValues, setFilterValues] = React.useState<FilterValues>({})
159
- const [advancedFilterState, setAdvancedFilterState] = React.useState<AdvancedFilterState>({ logic: 'and', conditions: [] })
206
+ const pathname = usePathname()
207
+ const searchParams = useSearchParams()
208
+ // One-shot URL hydration used as the hook's initial value. The hook is the
209
+ // single source of truth from this point on — the page MUST NOT keep a
210
+ // parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
211
+ // Compatibility" → state ownership).
212
+ const initialFilterTree = React.useMemo<AdvancedFilterTree>(() => {
213
+ if (!searchParams) return createEmptyTree()
214
+ const record: Record<string, string> = {}
215
+ searchParams.forEach((value, key) => {
216
+ if (key.startsWith('filter[')) record[key] = value
217
+ })
218
+ const v2 = deserializeTree(record)
219
+ if (v2) return v2
220
+ const flat = deserializeAdvancedFilter(record)
221
+ if (flat) return flatToTree(flat)
222
+ return createEmptyTree()
223
+ // searchParams is intentionally evaluated once on mount — subsequent URL
224
+ // changes flow through the hook, not back through hydration.
225
+ // eslint-disable-next-line react-hooks/exhaustive-deps
226
+ }, [])
227
+ // `filterPanel` lives at the top of the component so derived state below
228
+ // (URL params, data fetch, export config) can read `filterPanel.appliedTree`
229
+ // directly. Real `FilterFieldDef[]` arrives later from `useAutoDiscoveredFields`
230
+ // (it depends on columns) and is synced into the hook via a small effect at
231
+ // the bottom of the component. The hook reads fields through a ref at
232
+ // validation time only — first validation cannot fire before user input, by
233
+ // which point fields have settled, so the empty initial value is safe.
234
+ const [panelFields, setPanelFields] = React.useState<FilterFieldDef[]>([])
235
+ const [filtersOpen, setFiltersOpen] = React.useState(false)
236
+ const filtersTriggerRef = React.useRef<HTMLButtonElement | null>(null)
237
+ const filterPanel = useAdvancedFilterTree({
238
+ initial: initialFilterTree,
239
+ fields: panelFields,
240
+ onApply: () => setPage(1),
241
+ })
242
+ const advancedFilterState = filterPanel.appliedTree
243
+ const handleAdvancedFilterClear = React.useCallback(() => {
244
+ filterPanel.clear()
245
+ setPage(1)
246
+ }, [filterPanel])
160
247
  const [isLoading, setIsLoading] = React.useState(true)
161
248
  const [reloadToken, setReloadToken] = React.useState(0)
162
249
  const [cacheStatus, setCacheStatus] = React.useState<'hit' | 'miss' | null>(null)
163
250
  const [dictionaryMaps, setDictionaryMaps] = React.useState<Record<DictionaryKindKey, DictionaryMap>>(createEmptyCustomerDictionaryMaps())
164
- const [tagIdToLabel, setTagIdToLabel] = React.useState<Record<string, string>>({})
165
251
  const scopeVersion = useOrganizationScopeVersion()
166
252
  const queryClient = useQueryClient()
167
253
  const t = useT()
@@ -203,15 +289,15 @@ export default function CustomersPeoplePage() {
203
289
  return []
204
290
  }
205
291
  }, [queryClient, scopeVersion])
206
- const loadDictionaryOptions = React.useCallback(async (kind: 'statuses' | 'sources' | 'lifecycle-stages') => {
207
- const entries = await fetchDictionaryEntries(kind)
208
- return entries.map((entry) => ({ value: entry.value, label: entry.label }))
209
- }, [fetchDictionaryEntries])
210
-
211
292
  const dictionaryOptions = React.useMemo(() => {
212
- const toOptions = (map?: DictionaryMap | null): FilterOption[] =>
293
+ const toOptions = (map?: DictionaryMap | null): DictionaryOptionWithTone[] =>
213
294
  Object.values(map ?? {})
214
- .map((entry) => ({ value: entry.value, label: entry.label }))
295
+ .map((entry) => {
296
+ const tone = mapDictionaryColorToTone(entry.color)
297
+ const option: DictionaryOptionWithTone = { value: entry.value, label: entry.label }
298
+ if (tone) option.tone = tone
299
+ return option
300
+ })
215
301
  .sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
216
302
  return {
217
303
  statuses: toOptions(dictionaryMaps.statuses),
@@ -220,63 +306,6 @@ export default function CustomersPeoplePage() {
220
306
  }
221
307
  }, [dictionaryMaps])
222
308
 
223
- const loadTagOptions = React.useCallback(async (query?: string): Promise<FilterOption[]> => {
224
- try {
225
- const params = new URLSearchParams({ pageSize: '100' })
226
- const trimmedQuery = typeof query === 'string' ? query.trim() : ''
227
- if (trimmedQuery) params.set('search', trimmedQuery)
228
- const payload = await readApiResultOrThrow<{ items?: unknown[] }>(
229
- `/api/customers/tags?${params.toString()}`,
230
- undefined,
231
- { errorMessage: t('customers.people.detail.tags.loadError', 'Failed to load tags.') },
232
- )
233
- const items = Array.isArray(payload?.items) ? payload.items : []
234
- const options: FilterOption[] = []
235
- for (const item of items) {
236
- if (!item || typeof item !== 'object') continue
237
- const raw = item as { id?: unknown; tagId?: unknown; label?: unknown; slug?: unknown }
238
- const rawId = typeof raw.id === 'string'
239
- ? raw.id
240
- : typeof raw.tagId === 'string'
241
- ? raw.tagId
242
- : null
243
- if (!rawId) continue
244
- const label = typeof raw.label === 'string' && raw.label.trim().length
245
- ? raw.label.trim()
246
- : typeof raw.slug === 'string' && raw.slug.trim().length
247
- ? raw.slug.trim()
248
- : rawId
249
- options.push({ value: rawId, label })
250
- }
251
- if (options.length) {
252
- setTagIdToLabel((prev) => {
253
- let changed = false
254
- const next = { ...prev }
255
- for (const option of options) {
256
- if (next[option.value] !== option.label) {
257
- next[option.value] = option.label
258
- changed = true
259
- }
260
- }
261
- return changed ? next : prev
262
- })
263
- }
264
- return options
265
- } catch (err) {
266
- console.error('customers.people.list.loadTagOptions', err)
267
- return []
268
- }
269
- }, [setTagIdToLabel, t])
270
-
271
- const tagLabelToId = React.useMemo(() => {
272
- const map: Record<string, string> = {}
273
- for (const [id, label] of Object.entries(tagIdToLabel)) {
274
- if (!label) continue
275
- map[label] = id
276
- }
277
- return map
278
- }, [tagIdToLabel])
279
-
280
309
  React.useEffect(() => {
281
310
  let cancelled = false
282
311
  async function loadAll() {
@@ -298,63 +327,35 @@ export default function CustomersPeoplePage() {
298
327
  [E.customers.customer_entity, E.customers.customer_person_profile],
299
328
  { keyExtras: [scopeVersion, reloadToken] },
300
329
  )
301
-
302
- const filters = React.useMemo<FilterDef[]>(() => [
303
- {
304
- id: 'status',
305
- label: t('customers.people.list.filters.status'),
306
- type: 'select',
307
- options: dictionaryOptions.statuses,
308
- loadOptions: () => loadDictionaryOptions('statuses'),
309
- },
310
- {
311
- id: 'source',
312
- label: t('customers.people.list.filters.source'),
313
- type: 'select',
314
- options: dictionaryOptions.sources,
315
- loadOptions: () => loadDictionaryOptions('sources'),
316
- },
317
- {
318
- id: 'lifecycleStage',
319
- label: t('customers.people.list.filters.lifecycleStage'),
320
- type: 'select',
321
- options: dictionaryOptions.lifecycleStages,
322
- loadOptions: () => loadDictionaryOptions('lifecycle-stages'),
323
- },
324
- {
325
- id: 'tagIds',
326
- label: t('customers.people.list.filters.tags'),
327
- type: 'tags',
328
- loadOptions: loadTagOptions,
329
- formatValue: (value: string) => tagIdToLabel[value] ?? value,
330
- },
331
- {
332
- id: 'createdAt',
333
- label: t('customers.people.list.filters.createdAt'),
334
- type: 'dateRange',
335
- },
336
- {
337
- id: 'emailContains',
338
- label: t('customers.people.list.filters.emailContains'),
339
- type: 'text',
340
- placeholder: t('customers.people.list.filters.emailContainsPlaceholder'),
341
- },
342
- {
343
- id: 'hasEmail',
344
- label: t('customers.people.list.filters.hasEmail'),
345
- type: 'checkbox',
346
- },
347
- {
348
- id: 'hasPhone',
349
- label: t('customers.people.list.filters.hasPhone'),
350
- type: 'checkbox',
351
- },
352
- {
353
- id: 'hasNextInteraction',
354
- label: t('customers.people.list.filters.hasNextInteraction'),
355
- type: 'checkbox',
356
- },
357
- ], [dictionaryOptions.lifecycleStages, dictionaryOptions.sources, dictionaryOptions.statuses, loadDictionaryOptions, loadTagOptions, tagIdToLabel, t])
330
+ const currentUserId = useCurrentUserId()
331
+ const [ownerFilterOptions, setOwnerFilterOptions] = React.useState<AdvancedFilterOption[]>([])
332
+ React.useEffect(() => {
333
+ const controller = new AbortController()
334
+ let cancelled = false
335
+ void fetchAssignableStaffMembers('', { pageSize: 100, signal: controller.signal })
336
+ .then((items) => {
337
+ if (!cancelled) setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items))
338
+ })
339
+ .catch(() => {
340
+ if (!cancelled) setOwnerFilterOptions([])
341
+ })
342
+ return () => {
343
+ cancelled = true
344
+ controller.abort()
345
+ }
346
+ }, [scopeVersion])
347
+ const resolvedOwnerFilterOptions = React.useMemo(
348
+ () => ensureCurrentUserFilterOption(
349
+ ownerFilterOptions,
350
+ currentUserId,
351
+ t('customers.filters.currentUser', 'Current user'),
352
+ ),
353
+ [currentUserId, ownerFilterOptions, t],
354
+ )
355
+ const loadOwnerFilterOptions = React.useCallback(async (query?: string): Promise<AdvancedFilterOption[]> => {
356
+ const items = await fetchAssignableStaffMembers(query ?? '', { pageSize: 100 })
357
+ return mapAssignableStaffToFilterOptions(items)
358
+ }, [])
358
359
 
359
360
  const queryParams = React.useMemo(() => {
360
361
  const params = new URLSearchParams()
@@ -365,72 +366,40 @@ export default function CustomersPeoplePage() {
365
366
  params.set('order', sorting[0].desc ? 'desc' : 'asc')
366
367
  }
367
368
  if (search.trim()) params.set('search', search.trim())
368
- const status = filterValues.status
369
- if (typeof status === 'string' && status.trim()) params.set('status', status)
370
- const source = filterValues.source
371
- if (typeof source === 'string' && source.trim()) params.set('source', source)
372
- const lifecycleStage = filterValues.lifecycleStage
373
- if (typeof lifecycleStage === 'string' && lifecycleStage.trim()) params.set('lifecycleStage', lifecycleStage)
374
- const createdAt = filterValues.createdAt
375
- if (createdAt && typeof createdAt === 'object') {
376
- if (createdAt.from) params.set('createdFrom', createdAt.from)
377
- if (createdAt.to) params.set('createdTo', createdAt.to)
378
- }
379
- const emailContains = filterValues.emailContains
380
- if (typeof emailContains === 'string' && emailContains.trim()) {
381
- params.set('emailContains', emailContains.trim())
382
- }
383
- const booleanFilters: Array<['hasEmail' | 'hasPhone' | 'hasNextInteraction', string]> = [
384
- ['hasEmail', 'hasEmail'],
385
- ['hasPhone', 'hasPhone'],
386
- ['hasNextInteraction', 'hasNextInteraction'],
387
- ]
388
- for (const [key, queryKey] of booleanFilters) {
389
- const value = filterValues[key]
390
- if (value === true) params.set(queryKey, 'true')
391
- if (value === false) params.set(queryKey, 'false')
392
- }
393
- const tagValues = Array.isArray(filterValues.tagIds)
394
- ? filterValues.tagIds
395
- .map((value) => (typeof value === 'string' ? value.trim() : String(value || '').trim()))
396
- .filter((value) => value.length > 0)
397
- : []
398
- if (tagValues.length > 0) {
399
- const normalizedTagIds = tagValues
400
- .map((value) => (typeof tagIdToLabel[value] === 'string' ? value : tagLabelToId[value]))
401
- .filter((id): id is string => typeof id === 'string' && id.length > 0)
402
- if (normalizedTagIds.length === tagValues.length && normalizedTagIds.length > 0) {
403
- params.set('tagIds', normalizedTagIds.join(','))
404
- } else {
405
- params.set('tagIdsEmpty', 'true')
406
- }
407
- }
408
- Object.entries(filterValues).forEach(([key, value]) => {
409
- if (!key.startsWith('cf_') || value == null) return
410
- if (Array.isArray(value)) {
411
- const normalized = value
412
- .map((item) => {
413
- if (item == null) return ''
414
- if (typeof item === 'string') return item.trim()
415
- return String(item).trim()
416
- })
417
- .filter((item) => item.length > 0)
418
- if (normalized.length) params.set(key, normalized.join(','))
419
- } else if (typeof value === 'object') {
420
- return
421
- } else if (value !== '') {
422
- const stringValue = typeof value === 'string' ? value.trim() : String(value)
423
- if (stringValue) params.set(key, stringValue)
424
- }
425
- })
426
- const advancedParams = serializeAdvancedFilter(advancedFilterState)
369
+ const advancedParams = serializeTree(advancedFilterState)
427
370
  for (const [key, val] of Object.entries(advancedParams)) {
428
371
  params.set(key, val)
429
372
  }
430
373
  return params.toString()
431
- }, [advancedFilterState, filterValues, page, pageSize, search, sorting, tagIdToLabel, tagLabelToId])
374
+ }, [advancedFilterState, page, pageSize, search, sorting])
432
375
 
433
376
  const currentParams = React.useMemo(() => Object.fromEntries(new URLSearchParams(queryParams)), [queryParams])
377
+
378
+ // Mirror page state into the URL so a refresh restores the same filter tree,
379
+ // including nested subgroups. Without this effect the People page would
380
+ // discard everything the user typed into the filter panel on refresh
381
+ // (the previous behavior — top-level rules only "appeared" to survive
382
+ // because a stale localStorage perspective snapshot was being re-applied).
383
+ const queryRef = React.useRef(searchParams?.toString() ?? '')
384
+ React.useEffect(() => {
385
+ if (!pathname) return
386
+ const params = new URLSearchParams()
387
+ if (search.trim().length) params.set('search', search.trim())
388
+ if (page > 1) params.set('page', String(page))
389
+ if (sorting.length > 0) {
390
+ params.set('sort', sorting[0].id)
391
+ params.set('order', sorting[0].desc ? 'desc' : 'asc')
392
+ }
393
+ const advancedParams = serializeTree(advancedFilterState)
394
+ for (const [key, val] of Object.entries(advancedParams)) {
395
+ params.set(key, val)
396
+ }
397
+ const next = params.toString()
398
+ if (queryRef.current === next) return
399
+ queryRef.current = next
400
+ router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false })
401
+ }, [pathname, router, page, search, sorting, advancedFilterState])
402
+
434
403
  const exportConfig = React.useMemo(() => ({
435
404
  view: {
436
405
  getUrl: (format: DataTableExportFormat) =>
@@ -592,32 +561,6 @@ export default function CustomersPeoplePage() {
592
561
  return succeeded.length > 0
593
562
  }, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t])
594
563
 
595
- const handleFiltersApply = React.useCallback((values: FilterValues) => {
596
- const next: FilterValues = {}
597
- Object.entries(values).forEach(([key, value]) => {
598
- if (value !== undefined) {
599
- next[key] = value
600
- }
601
- })
602
- const rawTags = Array.isArray(values.tagIds) ? (values.tagIds as string[]) : []
603
- const sanitizedTags = rawTags
604
- .map((tag) => {
605
- const normalized = typeof tag === 'string' ? tag.trim() : ''
606
- if (!normalized) return ''
607
- return tagIdToLabel[normalized] ?? normalized
608
- })
609
- .filter((tag) => tag.length > 0)
610
- if (sanitizedTags.length) next.tagIds = sanitizedTags
611
- else delete next.tagIds
612
- setFilterValues(next)
613
- setPage(1)
614
- }, [setFilterValues, setPage, tagIdToLabel])
615
-
616
- const handleFiltersClear = React.useCallback(() => {
617
- setFilterValues({})
618
- setPage(1)
619
- }, [setFilterValues, setPage])
620
-
621
564
  const columns = React.useMemo<ColumnDef<PersonRow>[]>(() => {
622
565
  const noValue = <span className="text-muted-foreground text-sm">{t('customers.people.list.noValue')}</span>
623
566
  const renderDictionaryCell = (kind: DictionaryKindKey, rawValue: string | null | undefined) => (
@@ -664,7 +607,13 @@ export default function CustomersPeoplePage() {
664
607
  {
665
608
  accessorKey: 'name',
666
609
  header: t('customers.people.list.columns.name'),
667
- meta: { alwaysVisible: true, columnChooserGroup: 'Basic Info', filterKey: 'display_name', maxWidth: '240px' },
610
+ meta: {
611
+ alwaysVisible: true,
612
+ columnChooserGroup: 'Basic Info',
613
+ filterKey: 'display_name',
614
+ filterGroup: 'CRM',
615
+ maxWidth: '240px',
616
+ },
668
617
  cell: ({ row }) => (
669
618
  <Link href={`/backend/customers/people-v2/${row.original.id}`} className="font-medium hover:underline">
670
619
  {row.original.name}
@@ -674,13 +623,24 @@ export default function CustomersPeoplePage() {
674
623
  {
675
624
  accessorKey: 'email',
676
625
  header: t('customers.people.list.columns.email'),
677
- meta: { columnChooserGroup: 'Contact', filterKey: 'primary_email', maxWidth: '220px' },
626
+ meta: {
627
+ columnChooserGroup: 'Contact',
628
+ filterKey: 'primary_email',
629
+ filterGroup: 'Contact',
630
+ filterIconName: 'mail',
631
+ maxWidth: '220px',
632
+ },
678
633
  cell: ({ row }) => row.original.email || <span className="text-muted-foreground text-sm">{t('customers.people.list.noValue')}</span>,
679
634
  },
680
635
  {
681
636
  accessorKey: 'status',
682
637
  header: t('customers.people.list.columns.status'),
683
- meta: { filterType: 'select' as const, filterOptions: dictionaryOptions.statuses, columnChooserGroup: 'Basic Info' },
638
+ meta: {
639
+ filterType: 'select' as const,
640
+ filterOptions: dictionaryOptions.statuses,
641
+ columnChooserGroup: 'Basic Info',
642
+ filterGroup: 'CRM',
643
+ },
684
644
  cell: ({ row }) => renderDictionaryCell('statuses', row.original.status),
685
645
  },
686
646
  {
@@ -691,6 +651,7 @@ export default function CustomersPeoplePage() {
691
651
  filterOptions: dictionaryOptions.lifecycleStages,
692
652
  columnChooserGroup: 'Basic Info',
693
653
  filterKey: 'lifecycle_stage',
654
+ filterGroup: 'CRM',
694
655
  },
695
656
  cell: ({ row }) => renderDictionaryCell('lifecycle-stages', row.original.lifecycleStage),
696
657
  },
@@ -700,6 +661,8 @@ export default function CustomersPeoplePage() {
700
661
  meta: {
701
662
  columnChooserGroup: 'Dates',
702
663
  filterKey: 'next_interaction_at',
664
+ filterGroup: 'Activity',
665
+ filterIconName: 'calendar',
703
666
  tooltipContent: (row: PersonRow) => {
704
667
  if (!row.nextInteractionAt) return undefined
705
668
  const date = formatDate(row.nextInteractionAt, '')
@@ -734,67 +697,137 @@ export default function CustomersPeoplePage() {
734
697
  {
735
698
  accessorKey: 'source',
736
699
  header: t('customers.people.list.columns.source'),
737
- meta: { filterType: 'select' as const, filterOptions: dictionaryOptions.sources, columnChooserGroup: 'Basic Info' },
700
+ meta: {
701
+ filterType: 'select' as const,
702
+ filterOptions: dictionaryOptions.sources,
703
+ columnChooserGroup: 'Basic Info',
704
+ filterGroup: 'CRM',
705
+ },
738
706
  cell: ({ row }) => renderDictionaryCell('sources', row.original.source),
739
707
  },
708
+ {
709
+ accessorKey: 'ownerUserId',
710
+ header: t('customers.people.list.columns.owner', 'Owner'),
711
+ meta: {
712
+ columnChooserGroup: 'CRM',
713
+ filterType: 'select',
714
+ filterOptions: resolvedOwnerFilterOptions,
715
+ filterLoadOptions: loadOwnerFilterOptions,
716
+ filterGroup: 'CRM',
717
+ filterIconName: 'user-round',
718
+ filterKey: 'owner_user_id',
719
+ hidden: true,
720
+ },
721
+ cell: ({ row }) => row.original.ownerUserId ?? null,
722
+ },
740
723
  {
741
724
  accessorKey: 'firstName',
742
725
  header: t('customers.people.form.firstName', 'First name'),
743
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.first_name' },
726
+ meta: {
727
+ columnChooserGroup: 'Profile',
728
+ hidden: true,
729
+ filterKey: 'person_profile.first_name',
730
+ filterGroup: 'Profile',
731
+ },
744
732
  cell: ({ row }) => row.original.firstName || noValue,
745
733
  },
746
734
  {
747
735
  accessorKey: 'lastName',
748
736
  header: t('customers.people.form.lastName', 'Last name'),
749
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.last_name' },
737
+ meta: {
738
+ columnChooserGroup: 'Profile',
739
+ hidden: true,
740
+ filterKey: 'person_profile.last_name',
741
+ filterGroup: 'Profile',
742
+ },
750
743
  cell: ({ row }) => row.original.lastName || noValue,
751
744
  },
752
745
  {
753
746
  accessorKey: 'preferredName',
754
747
  header: t('customers.people.form.preferredName', 'Preferred name'),
755
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.preferred_name' },
748
+ meta: {
749
+ columnChooserGroup: 'Profile',
750
+ hidden: true,
751
+ filterKey: 'person_profile.preferred_name',
752
+ filterGroup: 'Profile',
753
+ },
756
754
  cell: ({ row }) => row.original.preferredName || noValue,
757
755
  },
758
756
  {
759
757
  accessorKey: 'jobTitle',
760
758
  header: t('customers.people.form.jobTitle', 'Job title'),
761
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.job_title' },
759
+ meta: {
760
+ columnChooserGroup: 'Profile',
761
+ hidden: true,
762
+ filterKey: 'person_profile.job_title',
763
+ filterGroup: 'Profile',
764
+ },
762
765
  cell: ({ row }) => row.original.jobTitle || noValue,
763
766
  },
764
767
  {
765
768
  accessorKey: 'department',
766
769
  header: t('customers.people.detail.fields.department', 'Department'),
767
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.department' },
770
+ meta: {
771
+ columnChooserGroup: 'Profile',
772
+ hidden: true,
773
+ filterKey: 'person_profile.department',
774
+ filterGroup: 'Profile',
775
+ },
768
776
  cell: ({ row }) => row.original.department || noValue,
769
777
  },
770
778
  {
771
779
  accessorKey: 'seniority',
772
780
  header: t('customers.people.detail.fields.seniority', 'Seniority'),
773
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.seniority' },
781
+ meta: {
782
+ columnChooserGroup: 'Profile',
783
+ hidden: true,
784
+ filterKey: 'person_profile.seniority',
785
+ filterGroup: 'Profile',
786
+ },
774
787
  cell: ({ row }) => row.original.seniority || noValue,
775
788
  },
776
789
  {
777
790
  accessorKey: 'timezone',
778
791
  header: t('customers.people.detail.fields.timezone', 'Timezone'),
779
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'person_profile.timezone' },
792
+ meta: {
793
+ columnChooserGroup: 'Profile',
794
+ hidden: true,
795
+ filterKey: 'person_profile.timezone',
796
+ filterGroup: 'Profile',
797
+ },
780
798
  cell: ({ row }) => row.original.timezone || noValue,
781
799
  },
782
800
  {
783
801
  accessorKey: 'linkedInUrl',
784
802
  header: t('customers.people.detail.fields.linkedIn', 'LinkedIn'),
785
- meta: { columnChooserGroup: 'Socials', hidden: true, filterKey: 'person_profile.linked_in_url' },
803
+ meta: {
804
+ columnChooserGroup: 'Socials',
805
+ hidden: true,
806
+ filterKey: 'person_profile.linked_in_url',
807
+ filterGroup: 'Socials',
808
+ },
786
809
  cell: ({ row }) => row.original.linkedInUrl || noValue,
787
810
  },
788
811
  {
789
812
  accessorKey: 'twitterUrl',
790
813
  header: t('customers.people.detail.fields.twitter', 'Twitter'),
791
- meta: { columnChooserGroup: 'Socials', hidden: true, filterKey: 'person_profile.twitter_url' },
814
+ meta: {
815
+ columnChooserGroup: 'Socials',
816
+ hidden: true,
817
+ filterKey: 'person_profile.twitter_url',
818
+ filterGroup: 'Socials',
819
+ },
792
820
  cell: ({ row }) => row.original.twitterUrl || noValue,
793
821
  },
794
822
  {
795
823
  accessorKey: 'description',
796
824
  header: t('customers.people.form.description', 'Description'),
797
- meta: { columnChooserGroup: 'Notes', hidden: true, filterKey: 'description' },
825
+ meta: {
826
+ columnChooserGroup: 'Notes',
827
+ hidden: true,
828
+ filterKey: 'description',
829
+ filterGroup: 'Notes',
830
+ },
798
831
  cell: ({ row }) => row.original.description || noValue,
799
832
  },
800
833
  ]
@@ -816,7 +849,31 @@ export default function CustomersPeoplePage() {
816
849
  }))
817
850
 
818
851
  return [...baseColumns, ...customColumns]
819
- }, [customFieldDefs, dictionaryMaps, dictionaryOptions, t])
852
+ }, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, resolvedOwnerFilterOptions, t])
853
+
854
+ const { advancedFilterFields } = useAutoDiscoveredFields({ columns, customFieldDefs })
855
+
856
+ // Sync auto-discovered fields into the `filterPanel` declared at the top of
857
+ // the component. See the comment on the `panelFields` state for why this
858
+ // late-binding is safe. Bail out by content (field-key list) — every render
859
+ // of `useAutoDiscoveredFields` produces fresh `FilterFieldDef` object refs
860
+ // even when the set of fields hasn't actually changed, so a naive reference
861
+ // setState would loop ("Maximum update depth exceeded").
862
+ React.useEffect(() => {
863
+ setPanelFields((prev) => {
864
+ if (prev === advancedFilterFields) return prev
865
+ if (prev.length === advancedFilterFields.length) {
866
+ let same = true
867
+ for (let i = 0; i < prev.length; i++) {
868
+ if (prev[i].key !== advancedFilterFields[i].key) { same = false; break }
869
+ }
870
+ if (same) return prev
871
+ }
872
+ return advancedFilterFields
873
+ })
874
+ }, [advancedFilterFields])
875
+
876
+ const peoplePresets = React.useMemo<FilterPreset[]>(() => makePeoplePresets(), [])
820
877
 
821
878
  return (
822
879
  <Page>
@@ -843,10 +900,6 @@ export default function CustomersPeoplePage() {
843
900
  searchValue={search}
844
901
  onSearchChange={(value) => { setSearch(value); setPage(1) }}
845
902
  searchPlaceholder={t('customers.people.list.searchPlaceholder')}
846
- filters={filters}
847
- filterValues={filterValues}
848
- onFiltersApply={handleFiltersApply}
849
- onFiltersClear={handleFiltersClear}
850
903
  entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
851
904
  perspective={{ tableId: 'customers.people.list' }}
852
905
  onRowClick={(row) => router.push(`/backend/customers/people-v2/${row.id}`)}
@@ -884,16 +937,54 @@ export default function CustomersPeoplePage() {
884
937
  />
885
938
  )}
886
939
  advancedFilter={{
887
- auto: true,
888
- value: advancedFilterState,
889
- onChange: setAdvancedFilterState,
890
- onApply: () => { setPage(1) },
891
- onClear: () => { setAdvancedFilterState({ logic: 'and', conditions: [] }); setPage(1) },
892
- }}
940
+ auto: true,
941
+ value: filterPanel.tree,
942
+ onChange: filterPanel.setTree,
943
+ onApply: () => filterPanel.flush(),
944
+ onClear: handleAdvancedFilterClear,
945
+ triggerRef: filtersTriggerRef,
946
+ externalPopover: true,
947
+ onTriggerClick: () => setFiltersOpen((prev) => !prev),
948
+ onApplyTree: (tree) => {
949
+ filterPanel.replaceTree(tree)
950
+ setPage(1)
951
+ },
952
+ }}
953
+ activeFilterChips={(
954
+ <ActiveFilterChips
955
+ tree={filterPanel.tree}
956
+ fields={advancedFilterFields}
957
+ popoverOpen={filtersOpen}
958
+ onRemoveNode={(id) => filterPanel.dispatch({ type: 'removeNode', nodeId: id })}
959
+ onOpen={() => setFiltersOpen(true)}
960
+ />
961
+ )}
962
+ filterAwareEmptyState={{
963
+ active: advancedFilterState.root.children.length > 0,
964
+ entityNamePlural: t('customers.people.entityPlural', 'people'),
965
+ canRemoveLast: filterPanel.tree.root.children.length > 0,
966
+ onClearAll: handleAdvancedFilterClear,
967
+ onRemoveLast: () => filterPanel.dispatch({ type: 'removeLast' }),
968
+ }}
893
969
  virtualized
894
970
  pagination={{ page, pageSize, total, totalPages, onPageChange: setPage, cacheStatus, pageSizeOptions: [10, 25, 50, 100], onPageSizeChange: handlePageSizeChange }}
895
971
  isLoading={isLoading}
896
972
  />
973
+ <AdvancedFilterPanel
974
+ fields={advancedFilterFields}
975
+ value={filterPanel.tree}
976
+ onChange={filterPanel.setTree}
977
+ onApply={filterPanel.flush}
978
+ onClear={handleAdvancedFilterClear}
979
+ onFlush={filterPanel.flush}
980
+ pendingErrors={filterPanel.pendingErrors}
981
+ userId={currentUserId}
982
+ presets={peoplePresets}
983
+ open={filtersOpen}
984
+ onOpenChange={setFiltersOpen}
985
+ triggerRef={filtersTriggerRef}
986
+ savedFilterStorageKey="customers.people.list"
987
+ />
897
988
  </PageBody>
898
989
  {ConfirmDialogElement}
899
990
  </Page>