@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/domain_mapping/index.js +31 -0
- package/dist/generated/entities/domain_mapping/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +16 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +18 -17
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customer_accounts/acl.js +2 -1
- package/dist/modules/customer_accounts/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-check.js +70 -0
- package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
- package/dist/modules/customer_accounts/api/login.js +14 -3
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +32 -5
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/data/enrichers.js +78 -12
- package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +85 -2
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/guards.js +130 -0
- package/dist/modules/customer_accounts/data/guards.js.map +7 -0
- package/dist/modules/customer_accounts/data/validators.js +19 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +23 -1
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +9 -2
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
- package/dist/modules/customer_accounts/lib/hostname.js +55 -0
- package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.client.js +76 -0
- package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +76 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +48 -1
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
- package/dist/modules/customers/api/companies/route.js +4 -9
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +4 -9
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +4 -9
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +0 -12
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +361 -284
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +378 -473
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +368 -286
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
- package/dist/modules/customers/widgets/injection-table.js +6 -0
- package/dist/modules/customers/widgets/injection-table.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +16 -2
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +31 -8
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/generated/entities/domain_mapping/index.ts +14 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +16 -0
- package/package.json +5 -5
- package/src/helpers/integration/salesUi.ts +27 -23
- package/src/modules/customer_accounts/acl.ts +1 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
- package/src/modules/customer_accounts/api/domain-check.ts +83 -0
- package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
- package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
- package/src/modules/customer_accounts/api/login.ts +14 -3
- package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
- package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
- package/src/modules/customer_accounts/api/signup.ts +36 -5
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
- package/src/modules/customer_accounts/data/enrichers.ts +95 -12
- package/src/modules/customer_accounts/data/entities.ts +80 -1
- package/src/modules/customer_accounts/data/guards.ts +155 -0
- package/src/modules/customer_accounts/data/validators.ts +26 -0
- package/src/modules/customer_accounts/di.ts +35 -1
- package/src/modules/customer_accounts/events.ts +8 -1
- package/src/modules/customer_accounts/i18n/de.json +78 -0
- package/src/modules/customer_accounts/i18n/en.json +78 -0
- package/src/modules/customer_accounts/i18n/es.json +78 -0
- package/src/modules/customer_accounts/i18n/pl.json +78 -0
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
- package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
- package/src/modules/customer_accounts/lib/hostname.ts +76 -0
- package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
- package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
- package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
- package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
- package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
- package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
- package/src/modules/customer_accounts/notifications.client.ts +76 -0
- package/src/modules/customer_accounts/notifications.ts +76 -0
- package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
- package/src/modules/customer_accounts/setup.ts +81 -1
- package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
- package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
- package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
- package/src/modules/customers/api/companies/route.ts +4 -9
- package/src/modules/customers/api/deals/route.ts +4 -9
- package/src/modules/customers/api/people/route.ts +4 -9
- package/src/modules/customers/api/utils.ts +0 -14
- package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
- package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
- package/src/modules/customers/backend/customers/people/page.tsx +331 -240
- package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
- package/src/modules/customers/i18n/de.json +31 -0
- package/src/modules/customers/i18n/en.json +31 -0
- package/src/modules/customers/i18n/es.json +31 -0
- package/src/modules/customers/i18n/pl.json +31 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
- package/src/modules/customers/widgets/injection-table.ts +6 -0
- package/src/modules/perspectives/services/perspectiveService.ts +30 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
- 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
|
|
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 {
|
|
24
|
-
import {
|
|
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
|
|
159
|
-
const
|
|
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):
|
|
293
|
+
const toOptions = (map?: DictionaryMap | null): DictionaryOptionWithTone[] =>
|
|
213
294
|
Object.values(map ?? {})
|
|
214
|
-
.map((entry) =>
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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,
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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>
|