@open-mercato/core 0.6.1-develop.3260.1.dc719cac84 → 0.6.1-develop.3278.1.a4d26475e4

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 (212) 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/salesUi.js +18 -17
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/customer_accounts/acl.js +2 -1
  11. package/dist/modules/customer_accounts/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  19. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  20. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  21. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  22. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  23. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/login.js +14 -3
  25. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  27. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  29. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +32 -5
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  33. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  34. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  35. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  36. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  37. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  38. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  39. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  40. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  41. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  56. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  57. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  58. package/dist/modules/customer_accounts/data/entities.js +85 -2
  59. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  60. package/dist/modules/customer_accounts/data/guards.js +130 -0
  61. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  62. package/dist/modules/customer_accounts/data/validators.js +19 -0
  63. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  64. package/dist/modules/customer_accounts/di.js +23 -1
  65. package/dist/modules/customer_accounts/di.js.map +2 -2
  66. package/dist/modules/customer_accounts/events.js +9 -2
  67. package/dist/modules/customer_accounts/events.js.map +2 -2
  68. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  69. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  70. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  71. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  72. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  73. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  74. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  75. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  76. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  77. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  78. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  79. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  80. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  81. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  82. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  83. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  84. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  85. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  86. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  87. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +76 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  91. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  92. package/dist/modules/customer_accounts/setup.js +48 -1
  93. package/dist/modules/customer_accounts/setup.js.map +2 -2
  94. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  95. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  96. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  97. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  98. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  99. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  100. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  101. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  102. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  103. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  104. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  105. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  106. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  107. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  108. package/dist/modules/customers/api/companies/route.js +4 -9
  109. package/dist/modules/customers/api/companies/route.js.map +2 -2
  110. package/dist/modules/customers/api/deals/route.js +4 -9
  111. package/dist/modules/customers/api/deals/route.js.map +2 -2
  112. package/dist/modules/customers/api/people/route.js +4 -9
  113. package/dist/modules/customers/api/people/route.js.map +2 -2
  114. package/dist/modules/customers/api/utils.js +0 -12
  115. package/dist/modules/customers/api/utils.js.map +2 -2
  116. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  117. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  118. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  119. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  120. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  121. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  122. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  123. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  124. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  125. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  126. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  127. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  128. package/dist/modules/customers/widgets/injection-table.js +6 -0
  129. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  130. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  131. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  132. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
  133. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  134. package/dist/modules/query_index/lib/engine.js +31 -8
  135. package/dist/modules/query_index/lib/engine.js.map +2 -2
  136. package/generated/entities/domain_mapping/index.ts +14 -0
  137. package/generated/entities.ids.generated.ts +2 -1
  138. package/generated/entity-fields-registry.ts +16 -0
  139. package/package.json +5 -5
  140. package/src/helpers/integration/salesUi.ts +27 -23
  141. package/src/modules/customer_accounts/acl.ts +1 -0
  142. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  143. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  144. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  145. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  146. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  147. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  148. package/src/modules/customer_accounts/api/login.ts +14 -3
  149. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  150. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  151. package/src/modules/customer_accounts/api/signup.ts +36 -5
  152. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  153. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  154. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  155. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  156. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  157. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  158. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  159. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  160. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  161. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  162. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  163. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  164. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  165. package/src/modules/customer_accounts/data/entities.ts +80 -1
  166. package/src/modules/customer_accounts/data/guards.ts +155 -0
  167. package/src/modules/customer_accounts/data/validators.ts +26 -0
  168. package/src/modules/customer_accounts/di.ts +35 -1
  169. package/src/modules/customer_accounts/events.ts +8 -1
  170. package/src/modules/customer_accounts/i18n/de.json +78 -0
  171. package/src/modules/customer_accounts/i18n/en.json +78 -0
  172. package/src/modules/customer_accounts/i18n/es.json +78 -0
  173. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  174. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  175. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  176. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  177. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  178. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  179. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  180. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  181. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  182. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  183. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  184. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  185. package/src/modules/customer_accounts/notifications.ts +76 -0
  186. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  187. package/src/modules/customer_accounts/setup.ts +81 -1
  188. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  189. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  190. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  191. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  192. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  193. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  194. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  195. package/src/modules/customers/api/companies/route.ts +4 -9
  196. package/src/modules/customers/api/deals/route.ts +4 -9
  197. package/src/modules/customers/api/people/route.ts +4 -9
  198. package/src/modules/customers/api/utils.ts +0 -14
  199. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  200. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  201. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  202. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  203. package/src/modules/customers/i18n/de.json +31 -0
  204. package/src/modules/customers/i18n/en.json +31 -0
  205. package/src/modules/customers/i18n/es.json +31 -0
  206. package/src/modules/customers/i18n/pl.json +31 -0
  207. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  208. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  209. package/src/modules/customers/widgets/injection-table.ts +6 -0
  210. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  211. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
  212. package/src/modules/query_index/lib/engine.ts +46 -8
@@ -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,51 @@ 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 makeCompaniesPresets(): FilterPreset[] {
60
+ return [
61
+ {
62
+ id: 'my-accounts',
63
+ labelKey: 'customers.companies.presets.myAccounts',
64
+ requiresUser: true,
65
+ build: ({ userId }) => makeRuleTree({ field: 'owner_user_id', operator: 'is', value: userId }),
66
+ },
67
+ {
68
+ id: 'recently-created',
69
+ labelKey: 'customers.companies.presets.recentlyCreated',
70
+ iconName: 'clock',
71
+ build: ({ now }) => {
72
+ const cutoff = new Date(now.getTime() - 7 * 24 * 3600 * 1000).toISOString().slice(0, 10)
73
+ return makeRuleTree({ field: 'created_at', operator: 'is_after', value: cutoff })
74
+ },
75
+ },
76
+ {
77
+ id: 'inactive-60',
78
+ labelKey: 'customers.companies.presets.inactive60',
79
+ build: ({ now }) => {
80
+ const cutoff = new Date(now.getTime() - 60 * 24 * 3600 * 1000).toISOString().slice(0, 10)
81
+ return makeRuleTree({ field: 'next_interaction_at', operator: 'is_before', value: cutoff })
82
+ },
83
+ },
84
+ ]
85
+ }
86
+
87
+
45
88
  type CompanyRow = {
46
89
  id: string
47
90
  name: string
@@ -63,6 +106,7 @@ type CompanyRow = {
63
106
  nextInteractionColor?: string | null
64
107
  organizationId?: string | null
65
108
  source?: string | null
109
+ ownerUserId?: string | null
66
110
  } & Record<string, unknown>
67
111
 
68
112
  type CompaniesResponse = {
@@ -109,6 +153,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
109
153
  const nextInteractionColor = typeof item.next_interaction_color === 'string' ? item.next_interaction_color : null
110
154
  const organizationId = typeof item.organization_id === 'string' ? item.organization_id : null
111
155
  const source = typeof item.source === 'string' ? item.source : null
156
+ const ownerUserId = typeof item.owner_user_id === 'string' ? item.owner_user_id : null
112
157
  const customFields: Record<string, unknown> = {}
113
158
  for (const [key, value] of Object.entries(item)) {
114
159
  if (key.startsWith('cf_')) {
@@ -136,6 +181,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
136
181
  nextInteractionColor,
137
182
  organizationId,
138
183
  source,
184
+ ownerUserId,
139
185
  ...customFields,
140
186
  }, item)
141
187
  }
@@ -149,13 +195,49 @@ export default function CustomersCompaniesPage() {
149
195
  const [total, setTotal] = React.useState(0)
150
196
  const [totalPages, setTotalPages] = React.useState(1)
151
197
  const [search, setSearch] = React.useState('')
152
- const [filterValues, setFilterValues] = React.useState<FilterValues>({})
153
- const [advancedFilterState, setAdvancedFilterState] = React.useState<AdvancedFilterState>({ logic: 'and', conditions: [] })
198
+ const pathname = usePathname()
199
+ const searchParams = useSearchParams()
200
+ // One-shot URL hydration used as the hook's initial value. The hook is the
201
+ // single source of truth from this point on — the page MUST NOT keep a
202
+ // parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
203
+ // Compatibility" → state ownership).
204
+ const initialFilterTree = React.useMemo<AdvancedFilterTree>(() => {
205
+ if (!searchParams) return createEmptyTree()
206
+ const record: Record<string, string> = {}
207
+ searchParams.forEach((value, key) => {
208
+ if (key.startsWith('filter[')) record[key] = value
209
+ })
210
+ const v2 = deserializeTree(record)
211
+ if (v2) return v2
212
+ const flat = deserializeAdvancedFilter(record)
213
+ if (flat) return flatToTree(flat)
214
+ return createEmptyTree()
215
+ // eslint-disable-next-line react-hooks/exhaustive-deps
216
+ }, [])
217
+ // `filterPanel` lives at the top of the component so derived state below
218
+ // (URL params, data fetch, export config) can read `filterPanel.appliedTree`
219
+ // directly. Real `FilterFieldDef[]` arrives later from `useAutoDiscoveredFields`
220
+ // (it depends on columns) and is synced into the hook via a small effect at
221
+ // the bottom of the component. The hook reads fields through a ref at
222
+ // validation time only — first validation cannot fire before user input, by
223
+ // which point fields have settled, so the empty initial value is safe.
224
+ const [panelFields, setPanelFields] = React.useState<FilterFieldDef[]>([])
225
+ const [filtersOpen, setFiltersOpen] = React.useState(false)
226
+ const filtersTriggerRef = React.useRef<HTMLButtonElement | null>(null)
227
+ const filterPanel = useAdvancedFilterTree({
228
+ initial: initialFilterTree,
229
+ fields: panelFields,
230
+ onApply: () => setPage(1),
231
+ })
232
+ const advancedFilterState = filterPanel.appliedTree
233
+ const handleAdvancedFilterClear = React.useCallback(() => {
234
+ filterPanel.clear()
235
+ setPage(1)
236
+ }, [filterPanel])
154
237
  const [isLoading, setIsLoading] = React.useState(true)
155
238
  const [reloadToken, setReloadToken] = React.useState(0)
156
239
  const [cacheStatus, setCacheStatus] = React.useState<'hit' | 'miss' | null>(null)
157
240
  const [dictionaryMaps, setDictionaryMaps] = React.useState<Record<DictionaryKindKey, DictionaryMap>>(createEmptyCustomerDictionaryMaps())
158
- const [tagIdToLabel, setTagIdToLabel] = React.useState<Record<string, string>>({})
159
241
  const scopeVersion = useOrganizationScopeVersion()
160
242
  const queryClient = useQueryClient()
161
243
  const t = useT()
@@ -196,15 +278,15 @@ export default function CustomersCompaniesPage() {
196
278
  return []
197
279
  }
198
280
  }, [queryClient, scopeVersion])
199
- const loadDictionaryOptions = React.useCallback(async (kind: 'statuses' | 'sources' | 'lifecycle-stages') => {
200
- const entries = await fetchDictionaryEntries(kind)
201
- return entries.map((entry) => ({ value: entry.value, label: entry.label }))
202
- }, [fetchDictionaryEntries])
203
-
204
281
  const dictionaryOptions = React.useMemo(() => {
205
- const toOptions = (map?: DictionaryMap | null): FilterOption[] =>
282
+ const toOptions = (map?: DictionaryMap | null): DictionaryOptionWithTone[] =>
206
283
  Object.values(map ?? {})
207
- .map((entry) => ({ value: entry.value, label: entry.label }))
284
+ .map((entry) => {
285
+ const tone = mapDictionaryColorToTone(entry.color)
286
+ const option: DictionaryOptionWithTone = { value: entry.value, label: entry.label }
287
+ if (tone) option.tone = tone
288
+ return option
289
+ })
208
290
  .sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
209
291
  return {
210
292
  statuses: toOptions(dictionaryMaps.statuses),
@@ -213,63 +295,6 @@ export default function CustomersCompaniesPage() {
213
295
  }
214
296
  }, [dictionaryMaps])
215
297
 
216
- const loadTagOptions = React.useCallback(async (query?: string): Promise<FilterOption[]> => {
217
- try {
218
- const params = new URLSearchParams({ pageSize: '100' })
219
- const trimmedQuery = typeof query === 'string' ? query.trim() : ''
220
- if (trimmedQuery) params.set('search', trimmedQuery)
221
- const payload = await readApiResultOrThrow<{ items?: unknown[] }>(
222
- `/api/customers/tags?${params.toString()}`,
223
- undefined,
224
- { errorMessage: t('customers.companies.list.tags.loadError', 'Failed to load tags.') },
225
- )
226
- const items = Array.isArray(payload?.items) ? payload.items : []
227
- const options: FilterOption[] = []
228
- for (const item of items) {
229
- if (!item || typeof item !== 'object') continue
230
- const raw = item as { id?: unknown; tagId?: unknown; label?: unknown; slug?: unknown }
231
- const rawId = typeof raw.id === 'string'
232
- ? raw.id
233
- : typeof raw.tagId === 'string'
234
- ? raw.tagId
235
- : null
236
- if (!rawId) continue
237
- const label = typeof raw.label === 'string' && raw.label.trim().length
238
- ? raw.label.trim()
239
- : typeof raw.slug === 'string' && raw.slug.trim().length
240
- ? raw.slug.trim()
241
- : rawId
242
- options.push({ value: rawId, label })
243
- }
244
- if (options.length) {
245
- setTagIdToLabel((prev) => {
246
- let changed = false
247
- const next = { ...prev }
248
- for (const option of options) {
249
- if (next[option.value] !== option.label) {
250
- next[option.value] = option.label
251
- changed = true
252
- }
253
- }
254
- return changed ? next : prev
255
- })
256
- }
257
- return options
258
- } catch (err) {
259
- console.error('customers.companies.list.loadTagOptions', err)
260
- return []
261
- }
262
- }, [setTagIdToLabel, t])
263
-
264
- const tagLabelToId = React.useMemo(() => {
265
- const map: Record<string, string> = {}
266
- for (const [id, label] of Object.entries(tagIdToLabel)) {
267
- if (!label) continue
268
- map[label] = id
269
- }
270
- return map
271
- }, [tagIdToLabel])
272
-
273
298
  React.useEffect(() => {
274
299
  let cancelled = false
275
300
  async function loadAll() {
@@ -291,63 +316,35 @@ export default function CustomersCompaniesPage() {
291
316
  [E.customers.customer_entity, E.customers.customer_company_profile],
292
317
  { keyExtras: [scopeVersion, reloadToken] },
293
318
  )
294
-
295
- const filters = React.useMemo<FilterDef[]>(() => [
296
- {
297
- id: 'status',
298
- label: t('customers.companies.list.filters.status'),
299
- type: 'select',
300
- options: dictionaryOptions.statuses,
301
- loadOptions: () => loadDictionaryOptions('statuses'),
302
- },
303
- {
304
- id: 'source',
305
- label: t('customers.companies.list.filters.source'),
306
- type: 'select',
307
- options: dictionaryOptions.sources,
308
- loadOptions: () => loadDictionaryOptions('sources'),
309
- },
310
- {
311
- id: 'lifecycleStage',
312
- label: t('customers.companies.list.filters.lifecycleStage'),
313
- type: 'select',
314
- options: dictionaryOptions.lifecycleStages,
315
- loadOptions: () => loadDictionaryOptions('lifecycle-stages'),
316
- },
317
- {
318
- id: 'tagIds',
319
- label: t('customers.companies.list.filters.tags'),
320
- type: 'tags',
321
- loadOptions: loadTagOptions,
322
- formatValue: (value: string) => tagIdToLabel[value] ?? value,
323
- },
324
- {
325
- id: 'createdAt',
326
- label: t('customers.companies.list.filters.createdAt'),
327
- type: 'dateRange',
328
- },
329
- {
330
- id: 'emailContains',
331
- label: t('customers.companies.list.filters.emailContains'),
332
- type: 'text',
333
- placeholder: t('customers.companies.list.filters.emailContainsPlaceholder'),
334
- },
335
- {
336
- id: 'hasEmail',
337
- label: t('customers.companies.list.filters.hasEmail'),
338
- type: 'checkbox',
339
- },
340
- {
341
- id: 'hasPhone',
342
- label: t('customers.companies.list.filters.hasPhone'),
343
- type: 'checkbox',
344
- },
345
- {
346
- id: 'hasNextInteraction',
347
- label: t('customers.companies.list.filters.hasNextInteraction'),
348
- type: 'checkbox',
349
- },
350
- ], [dictionaryOptions.lifecycleStages, dictionaryOptions.sources, dictionaryOptions.statuses, loadDictionaryOptions, loadTagOptions, tagIdToLabel, t])
319
+ const currentUserId = useCurrentUserId()
320
+ const [ownerFilterOptions, setOwnerFilterOptions] = React.useState<AdvancedFilterOption[]>([])
321
+ React.useEffect(() => {
322
+ const controller = new AbortController()
323
+ let cancelled = false
324
+ void fetchAssignableStaffMembers('', { pageSize: 100, signal: controller.signal })
325
+ .then((items) => {
326
+ if (!cancelled) setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items))
327
+ })
328
+ .catch(() => {
329
+ if (!cancelled) setOwnerFilterOptions([])
330
+ })
331
+ return () => {
332
+ cancelled = true
333
+ controller.abort()
334
+ }
335
+ }, [scopeVersion])
336
+ const resolvedOwnerFilterOptions = React.useMemo(
337
+ () => ensureCurrentUserFilterOption(
338
+ ownerFilterOptions,
339
+ currentUserId,
340
+ t('customers.filters.currentUser', 'Current user'),
341
+ ),
342
+ [currentUserId, ownerFilterOptions, t],
343
+ )
344
+ const loadOwnerFilterOptions = React.useCallback(async (query?: string): Promise<AdvancedFilterOption[]> => {
345
+ const items = await fetchAssignableStaffMembers(query ?? '', { pageSize: 100 })
346
+ return mapAssignableStaffToFilterOptions(items)
347
+ }, [])
351
348
 
352
349
  const queryParams = React.useMemo(() => {
353
350
  const params = new URLSearchParams()
@@ -358,72 +355,39 @@ export default function CustomersCompaniesPage() {
358
355
  params.set('order', sorting[0].desc ? 'desc' : 'asc')
359
356
  }
360
357
  if (search.trim()) params.set('search', search.trim())
361
- const status = filterValues.status
362
- if (typeof status === 'string' && status.trim()) params.set('status', status)
363
- const source = filterValues.source
364
- if (typeof source === 'string' && source.trim()) params.set('source', source)
365
- const lifecycleStage = filterValues.lifecycleStage
366
- if (typeof lifecycleStage === 'string' && lifecycleStage.trim()) params.set('lifecycleStage', lifecycleStage)
367
- const createdAt = filterValues.createdAt
368
- if (createdAt && typeof createdAt === 'object') {
369
- if (createdAt.from) params.set('createdFrom', createdAt.from)
370
- if (createdAt.to) params.set('createdTo', createdAt.to)
371
- }
372
- const emailContains = filterValues.emailContains
373
- if (typeof emailContains === 'string' && emailContains.trim()) {
374
- params.set('emailContains', emailContains.trim())
375
- }
376
- const tagValues = Array.isArray(filterValues.tagIds)
377
- ? filterValues.tagIds
378
- .map((value) => (typeof value === 'string' ? value.trim() : String(value || '').trim()))
379
- .filter((value) => value.length > 0)
380
- : []
381
- if (tagValues.length > 0) {
382
- const normalizedTagIds = tagValues
383
- .map((value) => (typeof tagIdToLabel[value] === 'string' ? value : tagLabelToId[value]))
384
- .filter((id): id is string => typeof id === 'string' && id.length > 0)
385
- if (normalizedTagIds.length === tagValues.length && normalizedTagIds.length > 0) {
386
- params.set('tagIds', normalizedTagIds.join(','))
387
- } else {
388
- params.set('tagIdsEmpty', 'true')
389
- }
390
- }
391
- const booleanFilters: Array<['hasEmail' | 'hasPhone' | 'hasNextInteraction', string]> = [
392
- ['hasEmail', 'hasEmail'],
393
- ['hasPhone', 'hasPhone'],
394
- ['hasNextInteraction', 'hasNextInteraction'],
395
- ]
396
- for (const [key, queryKey] of booleanFilters) {
397
- const value = filterValues[key]
398
- if (value === true) params.set(queryKey, 'true')
399
- if (value === false) params.set(queryKey, 'false')
400
- }
401
- Object.entries(filterValues).forEach(([key, value]) => {
402
- if (!key.startsWith('cf_') || value == null) return
403
- if (Array.isArray(value)) {
404
- const normalized = value
405
- .map((item) => {
406
- if (item == null) return ''
407
- if (typeof item === 'string') return item.trim()
408
- return String(item).trim()
409
- })
410
- .filter((item) => item.length > 0)
411
- if (normalized.length) params.set(key, normalized.join(','))
412
- } else if (typeof value === 'object') {
413
- return
414
- } else if (value !== '') {
415
- const stringValue = typeof value === 'string' ? value.trim() : String(value)
416
- if (stringValue) params.set(key, stringValue)
417
- }
418
- })
419
- const advancedParams = serializeAdvancedFilter(advancedFilterState)
358
+ const advancedParams = serializeTree(advancedFilterState)
420
359
  for (const [key, val] of Object.entries(advancedParams)) {
421
360
  params.set(key, val)
422
361
  }
423
362
  return params.toString()
424
- }, [advancedFilterState, filterValues, page, pageSize, search, sorting, tagIdToLabel, tagLabelToId])
363
+ }, [advancedFilterState, page, pageSize, search, sorting])
425
364
 
426
365
  const currentParams = React.useMemo(() => Object.fromEntries(new URLSearchParams(queryParams)), [queryParams])
366
+
367
+ // Mirror page state into the URL so refresh restores the same filter tree,
368
+ // including nested subgroups. Same pattern as the Deals page; without it
369
+ // refreshes silently dropped everything past whatever a stale localStorage
370
+ // perspective snapshot happened to contain.
371
+ const queryRef = React.useRef(searchParams?.toString() ?? '')
372
+ React.useEffect(() => {
373
+ if (!pathname) return
374
+ const params = new URLSearchParams()
375
+ if (search.trim().length) params.set('search', search.trim())
376
+ if (page > 1) params.set('page', String(page))
377
+ if (sorting.length > 0) {
378
+ params.set('sort', sorting[0].id)
379
+ params.set('order', sorting[0].desc ? 'desc' : 'asc')
380
+ }
381
+ const advancedParams = serializeTree(advancedFilterState)
382
+ for (const [key, val] of Object.entries(advancedParams)) {
383
+ params.set(key, val)
384
+ }
385
+ const next = params.toString()
386
+ if (queryRef.current === next) return
387
+ queryRef.current = next
388
+ router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false })
389
+ }, [pathname, router, page, search, sorting, advancedFilterState])
390
+
427
391
  const exportConfig = React.useMemo(() => ({
428
392
  view: {
429
393
  getUrl: (format: DataTableExportFormat) =>
@@ -584,30 +548,6 @@ export default function CustomersCompaniesPage() {
584
548
  return succeeded.length > 0
585
549
  }, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t])
586
550
 
587
- const handleFiltersApply = React.useCallback((values: FilterValues) => {
588
- const next: FilterValues = {}
589
- Object.entries(values).forEach(([key, value]) => {
590
- if (value !== undefined) next[key] = value
591
- })
592
- const rawTags = Array.isArray(values.tagIds) ? (values.tagIds as string[]) : []
593
- const sanitizedTags = rawTags
594
- .map((tag) => {
595
- const normalized = typeof tag === 'string' ? tag.trim() : ''
596
- if (!normalized) return ''
597
- return tagIdToLabel[normalized] ?? normalized
598
- })
599
- .filter((tag) => tag.length > 0)
600
- if (sanitizedTags.length) next.tagIds = sanitizedTags
601
- else delete next.tagIds
602
- setFilterValues(next)
603
- setPage(1)
604
- }, [setFilterValues, setPage, tagIdToLabel])
605
-
606
- const handleFiltersClear = React.useCallback(() => {
607
- setFilterValues({})
608
- setPage(1)
609
- }, [setFilterValues, setPage])
610
-
611
551
  const columns = React.useMemo<ColumnDef<CompanyRow>[]>(() => {
612
552
  const noValue = <span className="text-muted-foreground text-sm">{t('customers.companies.list.noValue')}</span>
613
553
  const renderDictionaryCell = (kind: DictionaryKindKey, rawValue: string | null | undefined) => (
@@ -654,7 +594,13 @@ export default function CustomersCompaniesPage() {
654
594
  {
655
595
  accessorKey: 'name',
656
596
  header: t('customers.companies.list.columns.name'),
657
- meta: { alwaysVisible: true, columnChooserGroup: 'Basic Info', filterKey: 'display_name', maxWidth: '260px' },
597
+ meta: {
598
+ alwaysVisible: true,
599
+ columnChooserGroup: 'Basic Info',
600
+ filterKey: 'display_name',
601
+ filterGroup: 'CRM',
602
+ maxWidth: '260px',
603
+ },
658
604
  cell: ({ row }) => (
659
605
  <Link href={`/backend/customers/companies-v2/${row.original.id}`} className="font-medium hover:underline">
660
606
  {row.original.name}
@@ -664,19 +610,37 @@ export default function CustomersCompaniesPage() {
664
610
  {
665
611
  accessorKey: 'email',
666
612
  header: t('customers.companies.list.columns.email'),
667
- meta: { columnChooserGroup: 'Contact', filterKey: 'primary_email', maxWidth: '220px' },
613
+ meta: {
614
+ columnChooserGroup: 'Contact',
615
+ filterKey: 'primary_email',
616
+ filterGroup: 'Contact',
617
+ filterIconName: 'mail',
618
+ maxWidth: '220px',
619
+ },
668
620
  cell: ({ row }) => row.original.email || noValue,
669
621
  },
670
622
  {
671
623
  accessorKey: 'phone',
672
624
  header: t('customers.companies.detail.highlights.primaryPhone', 'Primary phone'),
673
- meta: { columnChooserGroup: 'Contact', hidden: true, filterKey: 'primary_phone', maxWidth: '180px' },
625
+ meta: {
626
+ columnChooserGroup: 'Contact',
627
+ hidden: true,
628
+ filterKey: 'primary_phone',
629
+ filterGroup: 'Contact',
630
+ filterIconName: 'phone',
631
+ maxWidth: '180px',
632
+ },
674
633
  cell: ({ row }) => row.original.phone || noValue,
675
634
  },
676
635
  {
677
636
  accessorKey: 'status',
678
637
  header: t('customers.companies.list.columns.status'),
679
- 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
+ },
680
644
  cell: ({ row }) => renderDictionaryCell('statuses', row.original.status),
681
645
  },
682
646
  {
@@ -687,13 +651,19 @@ export default function CustomersCompaniesPage() {
687
651
  filterOptions: dictionaryOptions.lifecycleStages,
688
652
  columnChooserGroup: 'Basic Info',
689
653
  filterKey: 'lifecycle_stage',
654
+ filterGroup: 'CRM',
690
655
  },
691
656
  cell: ({ row }) => renderDictionaryCell('lifecycle-stages', row.original.lifecycleStage),
692
657
  },
693
658
  {
694
659
  accessorKey: 'nextInteractionAt',
695
660
  header: t('customers.companies.list.columns.nextInteraction'),
696
- meta: { columnChooserGroup: 'Dates', filterKey: 'next_interaction_at' },
661
+ meta: {
662
+ columnChooserGroup: 'Dates',
663
+ filterKey: 'next_interaction_at',
664
+ filterGroup: 'Activity',
665
+ filterIconName: 'calendar',
666
+ },
697
667
  cell: ({ row }) =>
698
668
  row.original.nextInteractionAt
699
669
  ? (
@@ -721,43 +691,93 @@ export default function CustomersCompaniesPage() {
721
691
  {
722
692
  accessorKey: 'source',
723
693
  header: t('customers.companies.list.columns.source'),
724
- meta: { filterType: 'select' as const, filterOptions: dictionaryOptions.sources, columnChooserGroup: 'Basic Info' },
694
+ meta: {
695
+ filterType: 'select' as const,
696
+ filterOptions: dictionaryOptions.sources,
697
+ columnChooserGroup: 'Basic Info',
698
+ filterGroup: 'CRM',
699
+ },
725
700
  cell: ({ row }) => renderDictionaryCell('sources', row.original.source),
726
701
  },
702
+ {
703
+ accessorKey: 'ownerUserId',
704
+ header: t('customers.companies.list.columns.owner', 'Owner'),
705
+ meta: {
706
+ columnChooserGroup: 'CRM',
707
+ filterType: 'select',
708
+ filterOptions: resolvedOwnerFilterOptions,
709
+ filterLoadOptions: loadOwnerFilterOptions,
710
+ filterGroup: 'CRM',
711
+ filterIconName: 'user-round',
712
+ filterKey: 'owner_user_id',
713
+ hidden: true,
714
+ },
715
+ cell: ({ row }) => row.original.ownerUserId ?? null,
716
+ },
727
717
  {
728
718
  accessorKey: 'legalName',
729
719
  header: t('customers.companies.detail.fields.legalName', 'Legal name'),
730
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.legal_name' },
720
+ meta: {
721
+ columnChooserGroup: 'Profile',
722
+ hidden: true,
723
+ filterKey: 'company_profile.legal_name',
724
+ filterGroup: 'Profile',
725
+ },
731
726
  cell: ({ row }) => row.original.legalName || noValue,
732
727
  },
733
728
  {
734
729
  accessorKey: 'brandName',
735
730
  header: t('customers.companies.detail.fields.brandName', 'Brand name'),
736
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.brand_name' },
731
+ meta: {
732
+ columnChooserGroup: 'Profile',
733
+ hidden: true,
734
+ filterKey: 'company_profile.brand_name',
735
+ filterGroup: 'Profile',
736
+ },
737
737
  cell: ({ row }) => row.original.brandName || noValue,
738
738
  },
739
739
  {
740
740
  accessorKey: 'domain',
741
741
  header: t('customers.companies.detail.fields.domain', 'Domain'),
742
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.domain' },
742
+ meta: {
743
+ columnChooserGroup: 'Profile',
744
+ hidden: true,
745
+ filterKey: 'company_profile.domain',
746
+ filterGroup: 'Profile',
747
+ },
743
748
  cell: ({ row }) => row.original.domain || noValue,
744
749
  },
745
750
  {
746
751
  accessorKey: 'websiteUrl',
747
752
  header: t('customers.companies.detail.fields.website', 'Website'),
748
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.website_url' },
753
+ meta: {
754
+ columnChooserGroup: 'Profile',
755
+ hidden: true,
756
+ filterKey: 'company_profile.website_url',
757
+ filterGroup: 'Profile',
758
+ },
749
759
  cell: ({ row }) => row.original.websiteUrl || noValue,
750
760
  },
751
761
  {
752
762
  accessorKey: 'industry',
753
763
  header: t('customers.companies.detail.fields.industry', 'Industry'),
754
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.industry' },
764
+ meta: {
765
+ columnChooserGroup: 'Profile',
766
+ hidden: true,
767
+ filterKey: 'company_profile.industry',
768
+ filterGroup: 'Profile',
769
+ },
755
770
  cell: ({ row }) => row.original.industry || noValue,
756
771
  },
757
772
  {
758
773
  accessorKey: 'sizeBucket',
759
774
  header: t('customers.companies.detail.fields.sizeBucket', 'Company size'),
760
- meta: { columnChooserGroup: 'Profile', hidden: true, filterKey: 'company_profile.size_bucket' },
775
+ meta: {
776
+ columnChooserGroup: 'Profile',
777
+ hidden: true,
778
+ filterKey: 'company_profile.size_bucket',
779
+ filterGroup: 'Profile',
780
+ },
761
781
  cell: ({ row }) => row.original.sizeBucket || noValue,
762
782
  },
763
783
  {
@@ -768,13 +788,19 @@ export default function CustomersCompaniesPage() {
768
788
  hidden: true,
769
789
  filterKey: 'company_profile.annual_revenue',
770
790
  filterType: 'number' as const,
791
+ filterGroup: 'Profile',
771
792
  },
772
793
  cell: ({ row }) => row.original.annualRevenue || noValue,
773
794
  },
774
795
  {
775
796
  accessorKey: 'description',
776
797
  header: t('customers.companies.detail.fields.description', 'Description'),
777
- meta: { columnChooserGroup: 'Notes', hidden: true, filterKey: 'description' },
798
+ meta: {
799
+ columnChooserGroup: 'Notes',
800
+ hidden: true,
801
+ filterKey: 'description',
802
+ filterGroup: 'Notes',
803
+ },
778
804
  cell: ({ row }) => row.original.description || noValue,
779
805
  },
780
806
  ]
@@ -796,7 +822,31 @@ export default function CustomersCompaniesPage() {
796
822
  }))
797
823
 
798
824
  return [...baseColumns, ...customColumns]
799
- }, [customFieldDefs, dictionaryMaps, t])
825
+ }, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, resolvedOwnerFilterOptions, t])
826
+
827
+ const { advancedFilterFields } = useAutoDiscoveredFields({ columns, customFieldDefs })
828
+
829
+ // Sync auto-discovered fields into the `filterPanel` declared at the top of
830
+ // the component. See the comment on the `panelFields` state for why this
831
+ // late-binding is safe. Bail out by content (field-key list) — every render
832
+ // of `useAutoDiscoveredFields` produces fresh `FilterFieldDef` object refs
833
+ // even when the set of fields hasn't actually changed, so a naive reference
834
+ // setState would loop ("Maximum update depth exceeded").
835
+ React.useEffect(() => {
836
+ setPanelFields((prev) => {
837
+ if (prev === advancedFilterFields) return prev
838
+ if (prev.length === advancedFilterFields.length) {
839
+ let same = true
840
+ for (let i = 0; i < prev.length; i++) {
841
+ if (prev[i].key !== advancedFilterFields[i].key) { same = false; break }
842
+ }
843
+ if (same) return prev
844
+ }
845
+ return advancedFilterFields
846
+ })
847
+ }, [advancedFilterFields])
848
+
849
+ const companiesPresets = React.useMemo<FilterPreset[]>(() => makeCompaniesPresets(), [])
800
850
 
801
851
  return (
802
852
  <Page>
@@ -823,10 +873,6 @@ export default function CustomersCompaniesPage() {
823
873
  searchValue={search}
824
874
  onSearchChange={(value) => { setSearch(value); setPage(1) }}
825
875
  searchPlaceholder={t('customers.companies.list.searchPlaceholder')}
826
- filters={filters}
827
- filterValues={filterValues}
828
- onFiltersApply={handleFiltersApply}
829
- onFiltersClear={handleFiltersClear}
830
876
  entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
831
877
  onRowClick={(row) => router.push(`/backend/customers/companies-v2/${row.id}`)}
832
878
  perspective={{ tableId: 'customers.companies.list' }}
@@ -864,16 +910,54 @@ export default function CustomersCompaniesPage() {
864
910
  />
865
911
  )}
866
912
  advancedFilter={{
867
- auto: true,
868
- value: advancedFilterState,
869
- onChange: setAdvancedFilterState,
870
- onApply: () => { setPage(1) },
871
- onClear: () => { setAdvancedFilterState({ logic: 'and', conditions: [] }); setPage(1) },
872
- }}
913
+ auto: true,
914
+ value: filterPanel.tree,
915
+ onChange: filterPanel.setTree,
916
+ onApply: () => filterPanel.flush(),
917
+ onClear: handleAdvancedFilterClear,
918
+ triggerRef: filtersTriggerRef,
919
+ externalPopover: true,
920
+ onTriggerClick: () => setFiltersOpen((prev) => !prev),
921
+ onApplyTree: (tree) => {
922
+ filterPanel.replaceTree(tree)
923
+ setPage(1)
924
+ },
925
+ }}
926
+ activeFilterChips={(
927
+ <ActiveFilterChips
928
+ tree={filterPanel.tree}
929
+ fields={advancedFilterFields}
930
+ popoverOpen={filtersOpen}
931
+ onRemoveNode={(id) => filterPanel.dispatch({ type: 'removeNode', nodeId: id })}
932
+ onOpen={() => setFiltersOpen(true)}
933
+ />
934
+ )}
935
+ filterAwareEmptyState={{
936
+ active: advancedFilterState.root.children.length > 0,
937
+ entityNamePlural: t('customers.companies.entityPlural', 'companies'),
938
+ canRemoveLast: filterPanel.tree.root.children.length > 0,
939
+ onClearAll: handleAdvancedFilterClear,
940
+ onRemoveLast: () => filterPanel.dispatch({ type: 'removeLast' }),
941
+ }}
873
942
  virtualized
874
943
  pagination={{ page, pageSize, total, totalPages, onPageChange: setPage, pageSizeOptions: [10, 25, 50, 100], onPageSizeChange: handlePageSizeChange, cacheStatus }}
875
944
  isLoading={isLoading}
876
945
  />
946
+ <AdvancedFilterPanel
947
+ fields={advancedFilterFields}
948
+ value={filterPanel.tree}
949
+ onChange={filterPanel.setTree}
950
+ onApply={filterPanel.flush}
951
+ onClear={handleAdvancedFilterClear}
952
+ onFlush={filterPanel.flush}
953
+ pendingErrors={filterPanel.pendingErrors}
954
+ userId={currentUserId}
955
+ presets={companiesPresets}
956
+ open={filtersOpen}
957
+ onOpenChange={setFiltersOpen}
958
+ triggerRef={filtersTriggerRef}
959
+ savedFilterStorageKey="customers.companies.list"
960
+ />
877
961
  </PageBody>
878
962
  {ConfirmDialogElement}
879
963
  </Page>