@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
|
@@ -7,9 +7,10 @@ import { useQueryClient } from '@tanstack/react-query'
|
|
|
7
7
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
8
8
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
9
9
|
import { DataTable, type DataTableExportFormat, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
|
|
10
|
-
import type {
|
|
11
|
-
import
|
|
12
|
-
import { deserializeAdvancedFilter,
|
|
10
|
+
import type { AdvancedFilterTree } from '@open-mercato/shared/lib/query/advanced-filter-tree'
|
|
11
|
+
import { createEmptyTree, makeRuleTree, makeMultiRuleTree } from '@open-mercato/shared/lib/query/advanced-filter-tree'
|
|
12
|
+
import { deserializeTree, deserializeAdvancedFilter, flatToTree, mapDictionaryColorToTone, serializeTree, type FilterFieldDef, type FilterOption as AdvancedFilterOption } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
13
|
+
import { useCurrentUserId } from '@open-mercato/ui/backend/utils/useCurrentUserId'
|
|
13
14
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
15
|
import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
15
16
|
import { groupBulkDeleteFailures, runBulkDelete } from '@open-mercato/ui/backend/utils/bulkDelete'
|
|
@@ -40,6 +41,53 @@ import {
|
|
|
40
41
|
supportsCustomFieldColumn,
|
|
41
42
|
} from '@open-mercato/ui/backend/utils/customFieldColumns'
|
|
42
43
|
import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
|
|
44
|
+
import { useAutoDiscoveredFields } from '@open-mercato/ui/backend/utils/useAutoDiscoveredFields'
|
|
45
|
+
import { useAdvancedFilterTree } from '@open-mercato/ui/backend/hooks/useAdvancedFilter'
|
|
46
|
+
import { AdvancedFilterPanel } from '@open-mercato/ui/backend/filters/AdvancedFilterPanel'
|
|
47
|
+
import { ActiveFilterChips } from '@open-mercato/ui/backend/filters/ActiveFilterChips'
|
|
48
|
+
import type { FilterPreset } from '@open-mercato/ui/backend/filters/QuickFilters'
|
|
49
|
+
import {
|
|
50
|
+
ensureCurrentUserFilterOption,
|
|
51
|
+
fetchAssignableStaffMembers,
|
|
52
|
+
mapAssignableStaffToFilterOptions,
|
|
53
|
+
} from '../../../components/detail/assignableStaff'
|
|
54
|
+
|
|
55
|
+
function makeDealsPresets(): FilterPreset[] {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
id: 'my-deals',
|
|
59
|
+
labelKey: 'customers.deals.presets.myDeals',
|
|
60
|
+
requiresUser: true,
|
|
61
|
+
build: ({ userId }) => makeRuleTree({ field: 'owner_user_id', operator: 'is', value: userId }),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'closing-month',
|
|
65
|
+
labelKey: 'customers.deals.presets.closingMonth',
|
|
66
|
+
iconName: 'clock',
|
|
67
|
+
build: ({ now }) => {
|
|
68
|
+
const start = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10)
|
|
69
|
+
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10)
|
|
70
|
+
return makeRuleTree({ field: 'expected_close_at', operator: 'between', value: [start, end] })
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
// The Deal entity has no dedicated "at risk" or health-score field — `customer_deals`
|
|
74
|
+
// exposes only `status` (open/win/loose/closed/in_progress, dictionary-driven) and
|
|
75
|
+
// `closure_outcome`. Rather than fabricate a mapping, the "At risk" preset is omitted
|
|
76
|
+
// until the data model exposes a first-class signal.
|
|
77
|
+
{
|
|
78
|
+
id: 'won-quarter',
|
|
79
|
+
labelKey: 'customers.deals.presets.wonQuarter',
|
|
80
|
+
build: ({ now }) => {
|
|
81
|
+
const quarter = Math.floor(now.getMonth() / 3)
|
|
82
|
+
const start = new Date(now.getFullYear(), quarter * 3, 1).toISOString().slice(0, 10)
|
|
83
|
+
return makeMultiRuleTree([
|
|
84
|
+
{ field: 'status', operator: 'is', value: 'win' },
|
|
85
|
+
{ field: 'expected_close_at', operator: 'is_after', value: start },
|
|
86
|
+
], 'and')
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
}
|
|
43
91
|
|
|
44
92
|
type DealRow = {
|
|
45
93
|
id: string
|
|
@@ -53,6 +101,7 @@ type DealRow = {
|
|
|
53
101
|
probability?: number | null
|
|
54
102
|
expectedCloseAt?: string | null
|
|
55
103
|
updatedAt?: string | null
|
|
104
|
+
ownerUserId?: string | null
|
|
56
105
|
companies: { id: string; label: string }[]
|
|
57
106
|
people: { id: string; label: string }[]
|
|
58
107
|
} & Record<string, unknown>
|
|
@@ -64,57 +113,10 @@ type DealsResponse = {
|
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
type FilterOption = { value: string; label: string }
|
|
116
|
+
type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
|
|
67
117
|
|
|
68
118
|
type DictionaryKey = Extract<CustomerDictionaryKind, 'deal-statuses' | 'pipeline-stages'>
|
|
69
119
|
|
|
70
|
-
type PersonLookupRecord = {
|
|
71
|
-
id: string
|
|
72
|
-
name: string | null
|
|
73
|
-
email: string | null
|
|
74
|
-
phone: string | null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
type CompanyLookupRecord = {
|
|
78
|
-
id: string
|
|
79
|
-
name: string | null
|
|
80
|
-
domain: string | null
|
|
81
|
-
email: string | null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parsePersonLookupRecord(item: unknown): PersonLookupRecord | null {
|
|
85
|
-
if (typeof item !== 'object' || item === null) return null
|
|
86
|
-
const record = item as Record<string, unknown>
|
|
87
|
-
const id = typeof record.id === 'string' ? record.id : null
|
|
88
|
-
if (!id || !isUuid(id)) return null
|
|
89
|
-
const name = typeof record.display_name === 'string' ? record.display_name : null
|
|
90
|
-
const email = typeof record.primary_email === 'string' ? record.primary_email : null
|
|
91
|
-
const phone = typeof record.primary_phone === 'string' ? record.primary_phone : null
|
|
92
|
-
return { id, name, email, phone }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseCompanyLookupRecord(item: unknown): CompanyLookupRecord | null {
|
|
96
|
-
if (typeof item !== 'object' || item === null) return null
|
|
97
|
-
const record = item as Record<string, unknown>
|
|
98
|
-
const id = typeof record.id === 'string' ? record.id : null
|
|
99
|
-
if (!id || !isUuid(id)) return null
|
|
100
|
-
const name = typeof record.display_name === 'string' ? record.display_name : null
|
|
101
|
-
const domain = typeof record.primary_domain === 'string' ? record.primary_domain : null
|
|
102
|
-
const email = typeof record.primary_email === 'string' ? record.primary_email : null
|
|
103
|
-
return { id, name, domain, email }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type OptionsState = {
|
|
107
|
-
options: FilterOption[]
|
|
108
|
-
idToLabel: Record<string, string>
|
|
109
|
-
labelToId: Record<string, string>
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const EMPTY_OPTIONS_STATE: OptionsState = {
|
|
113
|
-
options: [],
|
|
114
|
-
idToLabel: {},
|
|
115
|
-
labelToId: {},
|
|
116
|
-
}
|
|
117
|
-
|
|
118
120
|
const PAGE_SIZE = 20
|
|
119
121
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
120
122
|
|
|
@@ -143,106 +145,6 @@ function extractIdsFromParams(params: URLSearchParams | null | undefined, key: s
|
|
|
143
145
|
return normalizeIdCandidates(values)
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
function ensureUniqueLabel(base: string, occupied: Set<string>): string {
|
|
147
|
-
const trimmed = base.trim() || 'Unnamed'
|
|
148
|
-
if (!occupied.has(trimmed)) {
|
|
149
|
-
occupied.add(trimmed)
|
|
150
|
-
return trimmed
|
|
151
|
-
}
|
|
152
|
-
let counter = 2
|
|
153
|
-
let candidate = `${trimmed} • ${counter}`
|
|
154
|
-
while (occupied.has(candidate)) {
|
|
155
|
-
counter += 1
|
|
156
|
-
candidate = `${trimmed} • ${counter}`
|
|
157
|
-
}
|
|
158
|
-
occupied.add(candidate)
|
|
159
|
-
return candidate
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function fetchPeopleLookup(query?: string): Promise<PersonLookupRecord[]> {
|
|
163
|
-
const search = new URLSearchParams()
|
|
164
|
-
search.set('page', '1')
|
|
165
|
-
search.set('pageSize', '20')
|
|
166
|
-
if (query && query.trim().length) search.set('search', query.trim())
|
|
167
|
-
try {
|
|
168
|
-
const call = await apiCall<{ items?: unknown[] }>(`/api/customers/people?${search.toString()}`)
|
|
169
|
-
if (!call.ok) return []
|
|
170
|
-
const items = Array.isArray(call.result?.items) ? call.result.items : []
|
|
171
|
-
return items
|
|
172
|
-
.map((item) => parsePersonLookupRecord(item))
|
|
173
|
-
.filter((record): record is PersonLookupRecord => record !== null)
|
|
174
|
-
} catch {
|
|
175
|
-
return []
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function fetchPeopleLookupByIds(ids: string[]): Promise<PersonLookupRecord[]> {
|
|
180
|
-
const unique = Array.from(new Set(ids.filter((id) => isUuid(id))))
|
|
181
|
-
if (!unique.length) return []
|
|
182
|
-
const results = await Promise.all(
|
|
183
|
-
unique.map(async (id) => {
|
|
184
|
-
const search = new URLSearchParams()
|
|
185
|
-
search.set('id', id)
|
|
186
|
-
search.set('page', '1')
|
|
187
|
-
search.set('pageSize', '1')
|
|
188
|
-
try {
|
|
189
|
-
const call = await apiCall<{ items?: unknown[] }>(`/api/customers/people?${search.toString()}`)
|
|
190
|
-
if (!call.ok) return null
|
|
191
|
-
const items = Array.isArray(call.result?.items) ? call.result.items : []
|
|
192
|
-
const match = items
|
|
193
|
-
.map((item) => parsePersonLookupRecord(item))
|
|
194
|
-
.find((record) => record?.id === id)
|
|
195
|
-
return match ?? null
|
|
196
|
-
} catch {
|
|
197
|
-
return null
|
|
198
|
-
}
|
|
199
|
-
}),
|
|
200
|
-
)
|
|
201
|
-
return results.filter((record): record is PersonLookupRecord => !!record)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function fetchCompaniesLookup(query?: string): Promise<CompanyLookupRecord[]> {
|
|
205
|
-
const search = new URLSearchParams()
|
|
206
|
-
search.set('page', '1')
|
|
207
|
-
search.set('pageSize', '20')
|
|
208
|
-
if (query && query.trim().length) search.set('search', query.trim())
|
|
209
|
-
try {
|
|
210
|
-
const call = await apiCall<{ items?: unknown[] }>(`/api/customers/companies?${search.toString()}`)
|
|
211
|
-
if (!call.ok) return []
|
|
212
|
-
const items = Array.isArray(call.result?.items) ? call.result.items : []
|
|
213
|
-
return items
|
|
214
|
-
.map((item) => parseCompanyLookupRecord(item))
|
|
215
|
-
.filter((record): record is CompanyLookupRecord => record !== null)
|
|
216
|
-
} catch {
|
|
217
|
-
return []
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function fetchCompaniesLookupByIds(ids: string[]): Promise<CompanyLookupRecord[]> {
|
|
222
|
-
const unique = Array.from(new Set(ids.filter((id) => isUuid(id))))
|
|
223
|
-
if (!unique.length) return []
|
|
224
|
-
const results = await Promise.all(
|
|
225
|
-
unique.map(async (id) => {
|
|
226
|
-
const search = new URLSearchParams()
|
|
227
|
-
search.set('id', id)
|
|
228
|
-
search.set('page', '1')
|
|
229
|
-
search.set('pageSize', '1')
|
|
230
|
-
try {
|
|
231
|
-
const call = await apiCall<{ items?: unknown[] }>(`/api/customers/companies?${search.toString()}`)
|
|
232
|
-
if (!call.ok) return null
|
|
233
|
-
const items = Array.isArray(call.result?.items) ? call.result.items : []
|
|
234
|
-
const match = items
|
|
235
|
-
.map((item) => parseCompanyLookupRecord(item))
|
|
236
|
-
.find((record) => record?.id === id)
|
|
237
|
-
return match ?? null
|
|
238
|
-
} catch {
|
|
239
|
-
return null
|
|
240
|
-
}
|
|
241
|
-
}),
|
|
242
|
-
)
|
|
243
|
-
return results.filter((record): record is CompanyLookupRecord => !!record)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
148
|
function formatCurrency(amount: number | null | undefined, currency: string | null | undefined, fallback: string): string {
|
|
247
149
|
if (typeof amount !== 'number' || Number.isNaN(amount)) return fallback
|
|
248
150
|
try {
|
|
@@ -264,14 +166,6 @@ function formatDateValue(value: string | null | undefined, fallback: string): st
|
|
|
264
166
|
return date.toLocaleDateString()
|
|
265
167
|
}
|
|
266
168
|
|
|
267
|
-
function arraysEqual(a: string[], b: string[]): boolean {
|
|
268
|
-
if (a.length !== b.length) return false
|
|
269
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
270
|
-
if (a[i] !== b[i]) return false
|
|
271
|
-
}
|
|
272
|
-
return true
|
|
273
|
-
}
|
|
274
|
-
|
|
275
169
|
export default function CustomersDealsPage() {
|
|
276
170
|
const t = useT()
|
|
277
171
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
@@ -294,17 +188,43 @@ export default function CustomersDealsPage() {
|
|
|
294
188
|
const [isLoading, setIsLoading] = React.useState(false)
|
|
295
189
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
296
190
|
const [pendingDeleteId, setPendingDeleteId] = React.useState<string | null>(null)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
191
|
+
// One-shot URL hydration used as the hook's initial value. The hook is the
|
|
192
|
+
// single source of truth from this point on — the page MUST NOT keep a
|
|
193
|
+
// parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
|
|
194
|
+
// Compatibility" → state ownership).
|
|
195
|
+
const initialFilterTree = React.useMemo<AdvancedFilterTree>(() => {
|
|
196
|
+
if (!searchParams) return createEmptyTree()
|
|
301
197
|
const record: Record<string, string> = {}
|
|
302
|
-
|
|
198
|
+
searchParams.forEach((value, key) => {
|
|
303
199
|
if (key.startsWith('filter[')) record[key] = value
|
|
304
200
|
})
|
|
305
|
-
const
|
|
306
|
-
|
|
201
|
+
const v2 = deserializeTree(record)
|
|
202
|
+
if (v2) return v2
|
|
203
|
+
const flat = deserializeAdvancedFilter(record)
|
|
204
|
+
if (flat) return flatToTree(flat)
|
|
205
|
+
return createEmptyTree()
|
|
206
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
+
}, [])
|
|
208
|
+
// `filterPanel` lives at the top of the component so derived state below
|
|
209
|
+
// (URL params, data fetch, export config) can read `filterPanel.appliedTree`
|
|
210
|
+
// directly. Real `FilterFieldDef[]` arrives later from `useAutoDiscoveredFields`
|
|
211
|
+
// (it depends on columns) and is synced into the hook via a small effect at
|
|
212
|
+
// the bottom of the component. The hook reads fields through a ref at
|
|
213
|
+
// validation time only — first validation cannot fire before user input, by
|
|
214
|
+
// which point fields have settled, so the empty initial value is safe.
|
|
215
|
+
const [panelFields, setPanelFields] = React.useState<FilterFieldDef[]>([])
|
|
216
|
+
const [filtersOpen, setFiltersOpen] = React.useState(false)
|
|
217
|
+
const filtersTriggerRef = React.useRef<HTMLButtonElement | null>(null)
|
|
218
|
+
const filterPanel = useAdvancedFilterTree({
|
|
219
|
+
initial: initialFilterTree,
|
|
220
|
+
fields: panelFields,
|
|
221
|
+
onApply: () => setPage(1),
|
|
307
222
|
})
|
|
223
|
+
const advancedFilterState = filterPanel.appliedTree
|
|
224
|
+
const handleAdvancedFilterClear = React.useCallback(() => {
|
|
225
|
+
filterPanel.clear()
|
|
226
|
+
setPage(1)
|
|
227
|
+
}, [filterPanel])
|
|
308
228
|
const [cacheStatus, setCacheStatus] = React.useState<'hit' | 'miss' | null>(null)
|
|
309
229
|
|
|
310
230
|
const initialPersonIds = React.useMemo(
|
|
@@ -319,147 +239,6 @@ export default function CustomersDealsPage() {
|
|
|
319
239
|
const [selectedPersonIds, setSelectedPersonIds] = React.useState<string[]>(initialPersonIds)
|
|
320
240
|
const [selectedCompanyIds, setSelectedCompanyIds] = React.useState<string[]>(initialCompanyIds)
|
|
321
241
|
|
|
322
|
-
const [peopleState, setPeopleState] = React.useState<OptionsState>(EMPTY_OPTIONS_STATE)
|
|
323
|
-
const [companiesState, setCompaniesState] = React.useState<OptionsState>(EMPTY_OPTIONS_STATE)
|
|
324
|
-
const peopleCacheRef = React.useRef<Map<string, FilterOption[]>>(new Map())
|
|
325
|
-
const companiesCacheRef = React.useRef<Map<string, FilterOption[]>>(new Map())
|
|
326
|
-
|
|
327
|
-
const buildPersonLabel = React.useCallback((record: PersonLookupRecord): string => {
|
|
328
|
-
const parts: string[] = []
|
|
329
|
-
const name = record.name?.trim()
|
|
330
|
-
if (name) parts.push(name)
|
|
331
|
-
const email = record.email?.trim()
|
|
332
|
-
if (email && !parts.includes(email)) parts.push(email)
|
|
333
|
-
const phone = record.phone?.trim()
|
|
334
|
-
if (!parts.length && phone) parts.push(phone)
|
|
335
|
-
if (!parts.length) parts.push(t('customers.deals.list.unnamedPerson', 'Unnamed person'))
|
|
336
|
-
return parts.join(' • ')
|
|
337
|
-
}, [t])
|
|
338
|
-
|
|
339
|
-
const buildCompanyLabel = React.useCallback((record: CompanyLookupRecord): string => {
|
|
340
|
-
const parts: string[] = []
|
|
341
|
-
const name = record.name?.trim()
|
|
342
|
-
if (name) parts.push(name)
|
|
343
|
-
const domain = record.domain?.trim()
|
|
344
|
-
if (domain && !parts.includes(domain)) parts.push(domain)
|
|
345
|
-
const email = record.email?.trim()
|
|
346
|
-
if (!parts.length && email) parts.push(email)
|
|
347
|
-
if (!parts.length) parts.push(t('customers.deals.list.unnamedCompany', 'Unnamed company'))
|
|
348
|
-
return parts.join(' • ')
|
|
349
|
-
}, [t])
|
|
350
|
-
|
|
351
|
-
const ingestPeopleRecords = React.useCallback((records: PersonLookupRecord[]) => {
|
|
352
|
-
if (!records.length) return [] as FilterOption[]
|
|
353
|
-
const queryMap = new Map<string, FilterOption>()
|
|
354
|
-
setPeopleState((prev) => {
|
|
355
|
-
const idToLabel = { ...prev.idToLabel }
|
|
356
|
-
const labelToId: Record<string, string> = {}
|
|
357
|
-
const merged = new Map(prev.options.map((opt) => [opt.value, opt]))
|
|
358
|
-
const occupied = new Set<string>()
|
|
359
|
-
Object.entries(prev.labelToId).forEach(([label, id]) => {
|
|
360
|
-
occupied.add(label)
|
|
361
|
-
labelToId[label] = id
|
|
362
|
-
})
|
|
363
|
-
records.forEach((record) => {
|
|
364
|
-
if (!isUuid(record.id)) return
|
|
365
|
-
const base = buildPersonLabel(record)
|
|
366
|
-
let previousLabel = idToLabel[record.id]
|
|
367
|
-
if (previousLabel) {
|
|
368
|
-
delete labelToId[previousLabel]
|
|
369
|
-
occupied.delete(previousLabel)
|
|
370
|
-
}
|
|
371
|
-
const label = ensureUniqueLabel(base, occupied)
|
|
372
|
-
idToLabel[record.id] = label
|
|
373
|
-
labelToId[label] = record.id
|
|
374
|
-
const option = { value: record.id, label }
|
|
375
|
-
merged.set(record.id, option)
|
|
376
|
-
queryMap.set(record.id, option)
|
|
377
|
-
})
|
|
378
|
-
const nextOptions = Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
379
|
-
return { options: nextOptions, idToLabel, labelToId }
|
|
380
|
-
})
|
|
381
|
-
return Array.from(queryMap.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
382
|
-
}, [buildPersonLabel])
|
|
383
|
-
|
|
384
|
-
const ingestCompanyRecords = React.useCallback((records: CompanyLookupRecord[]) => {
|
|
385
|
-
if (!records.length) return [] as FilterOption[]
|
|
386
|
-
const queryMap = new Map<string, FilterOption>()
|
|
387
|
-
setCompaniesState((prev) => {
|
|
388
|
-
const idToLabel = { ...prev.idToLabel }
|
|
389
|
-
const labelToId: Record<string, string> = {}
|
|
390
|
-
const merged = new Map(prev.options.map((opt) => [opt.value, opt]))
|
|
391
|
-
const occupied = new Set<string>()
|
|
392
|
-
Object.entries(prev.labelToId).forEach(([label, id]) => {
|
|
393
|
-
occupied.add(label)
|
|
394
|
-
labelToId[label] = id
|
|
395
|
-
})
|
|
396
|
-
records.forEach((record) => {
|
|
397
|
-
if (!isUuid(record.id)) return
|
|
398
|
-
const base = buildCompanyLabel(record)
|
|
399
|
-
let previousLabel = idToLabel[record.id]
|
|
400
|
-
if (previousLabel) {
|
|
401
|
-
delete labelToId[previousLabel]
|
|
402
|
-
occupied.delete(previousLabel)
|
|
403
|
-
}
|
|
404
|
-
const label = ensureUniqueLabel(base, occupied)
|
|
405
|
-
idToLabel[record.id] = label
|
|
406
|
-
labelToId[label] = record.id
|
|
407
|
-
const option = { value: record.id, label }
|
|
408
|
-
merged.set(record.id, option)
|
|
409
|
-
queryMap.set(record.id, option)
|
|
410
|
-
})
|
|
411
|
-
const nextOptions = Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
412
|
-
return { options: nextOptions, idToLabel, labelToId }
|
|
413
|
-
})
|
|
414
|
-
return Array.from(queryMap.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
415
|
-
}, [buildCompanyLabel])
|
|
416
|
-
|
|
417
|
-
const loadPeopleOptions = React.useCallback(async (query?: string) => {
|
|
418
|
-
const normalizedQuery = (query || '').trim().toLowerCase()
|
|
419
|
-
const cacheKey = `${scopeVersion}|${normalizedQuery}`
|
|
420
|
-
const cached = peopleCacheRef.current.get(cacheKey)
|
|
421
|
-
if (cached) return cached
|
|
422
|
-
const records = await fetchPeopleLookup(query)
|
|
423
|
-
const options = ingestPeopleRecords(records)
|
|
424
|
-
peopleCacheRef.current.set(cacheKey, options)
|
|
425
|
-
return options
|
|
426
|
-
}, [scopeVersion, ingestPeopleRecords])
|
|
427
|
-
|
|
428
|
-
const loadCompanyOptions = React.useCallback(async (query?: string) => {
|
|
429
|
-
const normalizedQuery = (query || '').trim().toLowerCase()
|
|
430
|
-
const cacheKey = `${scopeVersion}|${normalizedQuery}`
|
|
431
|
-
const cached = companiesCacheRef.current.get(cacheKey)
|
|
432
|
-
if (cached) return cached
|
|
433
|
-
const records = await fetchCompaniesLookup(query)
|
|
434
|
-
const options = ingestCompanyRecords(records)
|
|
435
|
-
companiesCacheRef.current.set(cacheKey, options)
|
|
436
|
-
return options
|
|
437
|
-
}, [scopeVersion, ingestCompanyRecords])
|
|
438
|
-
|
|
439
|
-
React.useEffect(() => {
|
|
440
|
-
let cancelled = false
|
|
441
|
-
if (!selectedPersonIds.length) return
|
|
442
|
-
const missing = selectedPersonIds.filter((id) => !peopleState.idToLabel[id])
|
|
443
|
-
if (!missing.length) return
|
|
444
|
-
fetchPeopleLookupByIds(missing).then((records) => {
|
|
445
|
-
if (cancelled) return
|
|
446
|
-
ingestPeopleRecords(records)
|
|
447
|
-
})
|
|
448
|
-
return () => { cancelled = true }
|
|
449
|
-
}, [selectedPersonIds, peopleState.idToLabel, ingestPeopleRecords])
|
|
450
|
-
|
|
451
|
-
React.useEffect(() => {
|
|
452
|
-
let cancelled = false
|
|
453
|
-
if (!selectedCompanyIds.length) return
|
|
454
|
-
const missing = selectedCompanyIds.filter((id) => !companiesState.idToLabel[id])
|
|
455
|
-
if (!missing.length) return
|
|
456
|
-
fetchCompaniesLookupByIds(missing).then((records) => {
|
|
457
|
-
if (cancelled) return
|
|
458
|
-
ingestCompanyRecords(records)
|
|
459
|
-
})
|
|
460
|
-
return () => { cancelled = true }
|
|
461
|
-
}, [selectedCompanyIds, companiesState.idToLabel, ingestCompanyRecords])
|
|
462
|
-
|
|
463
242
|
const [dictionaryMaps, setDictionaryMaps] = React.useState<Record<DictionaryKey, CustomerDictionaryMap>>({
|
|
464
243
|
'deal-statuses': {},
|
|
465
244
|
'pipeline-stages': {},
|
|
@@ -488,6 +267,21 @@ export default function CustomersDealsPage() {
|
|
|
488
267
|
loadDictionaries().catch(() => {})
|
|
489
268
|
return () => { cancelled = true }
|
|
490
269
|
}, [fetchDictionaryEntries, reloadToken])
|
|
270
|
+
const dictionaryOptions = React.useMemo(() => {
|
|
271
|
+
const toOptions = (map?: CustomerDictionaryMap | null): DictionaryOptionWithTone[] =>
|
|
272
|
+
Object.values(map ?? {})
|
|
273
|
+
.map((entry) => {
|
|
274
|
+
const tone = mapDictionaryColorToTone(entry.color)
|
|
275
|
+
const option: DictionaryOptionWithTone = { value: entry.value, label: entry.label }
|
|
276
|
+
if (tone) option.tone = tone
|
|
277
|
+
return option
|
|
278
|
+
})
|
|
279
|
+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
|
|
280
|
+
return {
|
|
281
|
+
dealStatuses: toOptions(dictionaryMaps['deal-statuses']),
|
|
282
|
+
pipelineStages: toOptions(dictionaryMaps['pipeline-stages']),
|
|
283
|
+
}
|
|
284
|
+
}, [dictionaryMaps])
|
|
491
285
|
|
|
492
286
|
React.useEffect(() => {
|
|
493
287
|
let cancelled = false
|
|
@@ -509,87 +303,11 @@ export default function CustomersDealsPage() {
|
|
|
509
303
|
return () => { cancelled = true }
|
|
510
304
|
}, [reloadToken, scopeVersion])
|
|
511
305
|
|
|
512
|
-
React.useEffect(() => {
|
|
513
|
-
peopleCacheRef.current.clear()
|
|
514
|
-
companiesCacheRef.current.clear()
|
|
515
|
-
setPeopleState((prev) => {
|
|
516
|
-
if (!prev.options.length && !Object.keys(prev.idToLabel).length) return prev
|
|
517
|
-
return { ...EMPTY_OPTIONS_STATE }
|
|
518
|
-
})
|
|
519
|
-
setCompaniesState((prev) => {
|
|
520
|
-
if (!prev.options.length && !Object.keys(prev.idToLabel).length) return prev
|
|
521
|
-
return { ...EMPTY_OPTIONS_STATE }
|
|
522
|
-
})
|
|
523
|
-
}, [scopeVersion, reloadToken])
|
|
524
|
-
|
|
525
|
-
const syncFilterIds = React.useCallback((
|
|
526
|
-
key: 'people' | 'companies',
|
|
527
|
-
ids: string[],
|
|
528
|
-
) => {
|
|
529
|
-
setFilterValues((prev) => {
|
|
530
|
-
const current = Array.isArray(prev[key]) ? (prev[key] as string[]) : []
|
|
531
|
-
if (!ids.length) {
|
|
532
|
-
if (!current.length) return prev
|
|
533
|
-
const next = { ...prev }
|
|
534
|
-
delete next[key]
|
|
535
|
-
return next
|
|
536
|
-
}
|
|
537
|
-
if (arraysEqual(current, ids)) return prev
|
|
538
|
-
return { ...prev, [key]: [...ids] }
|
|
539
|
-
})
|
|
540
|
-
}, [])
|
|
541
|
-
|
|
542
|
-
React.useEffect(() => {
|
|
543
|
-
syncFilterIds('people', selectedPersonIds)
|
|
544
|
-
}, [selectedPersonIds, syncFilterIds])
|
|
545
|
-
|
|
546
|
-
React.useEffect(() => {
|
|
547
|
-
syncFilterIds('companies', selectedCompanyIds)
|
|
548
|
-
}, [selectedCompanyIds, syncFilterIds])
|
|
549
|
-
|
|
550
306
|
const handleSearchChange = React.useCallback((value: string) => {
|
|
551
307
|
setSearch(value.trim())
|
|
552
308
|
setPage(1)
|
|
553
309
|
}, [])
|
|
554
310
|
|
|
555
|
-
const handleFiltersApply = React.useCallback((values: FilterValues) => {
|
|
556
|
-
const next: FilterValues = { ...values }
|
|
557
|
-
|
|
558
|
-
const rawPeople = Array.isArray(values.people) ? (values.people as string[]) : []
|
|
559
|
-
const nextPersonIds = Array.from(
|
|
560
|
-
new Set(
|
|
561
|
-
rawPeople
|
|
562
|
-
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
563
|
-
.filter((value) => value.length > 0 && isUuid(value)),
|
|
564
|
-
),
|
|
565
|
-
)
|
|
566
|
-
setSelectedPersonIds(nextPersonIds)
|
|
567
|
-
if (nextPersonIds.length) next.people = nextPersonIds
|
|
568
|
-
else delete next.people
|
|
569
|
-
|
|
570
|
-
const rawCompanies = Array.isArray(values.companies) ? (values.companies as string[]) : []
|
|
571
|
-
const nextCompanyIds = Array.from(
|
|
572
|
-
new Set(
|
|
573
|
-
rawCompanies
|
|
574
|
-
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
575
|
-
.filter((value) => value.length > 0 && isUuid(value)),
|
|
576
|
-
),
|
|
577
|
-
)
|
|
578
|
-
setSelectedCompanyIds(nextCompanyIds)
|
|
579
|
-
if (nextCompanyIds.length) next.companies = nextCompanyIds
|
|
580
|
-
else delete next.companies
|
|
581
|
-
|
|
582
|
-
setFilterValues(next)
|
|
583
|
-
setPage(1)
|
|
584
|
-
}, [])
|
|
585
|
-
|
|
586
|
-
const handleFiltersClear = React.useCallback(() => {
|
|
587
|
-
setFilterValues({})
|
|
588
|
-
setSelectedPersonIds([])
|
|
589
|
-
setSelectedCompanyIds([])
|
|
590
|
-
setPage(1)
|
|
591
|
-
}, [])
|
|
592
|
-
|
|
593
311
|
const queryParams = React.useMemo(() => {
|
|
594
312
|
const params = new URLSearchParams()
|
|
595
313
|
params.set('page', String(page))
|
|
@@ -601,35 +319,12 @@ export default function CustomersDealsPage() {
|
|
|
601
319
|
if (search.trim().length) params.set('search', search.trim())
|
|
602
320
|
if (selectedPersonIds.length) params.set('personId', selectedPersonIds.join(','))
|
|
603
321
|
if (selectedCompanyIds.length) params.set('companyId', selectedCompanyIds.join(','))
|
|
604
|
-
|
|
605
|
-
if (key === 'people' || key === 'companies') return
|
|
606
|
-
if (value == null) return
|
|
607
|
-
if (Array.isArray(value)) {
|
|
608
|
-
const normalized = value
|
|
609
|
-
.map((item) => {
|
|
610
|
-
if (item == null) return ''
|
|
611
|
-
if (typeof item === 'string') return item.trim()
|
|
612
|
-
return String(item).trim()
|
|
613
|
-
})
|
|
614
|
-
.filter((item) => item.length > 0)
|
|
615
|
-
if (normalized.length) params.set(key, normalized.join(','))
|
|
616
|
-
} else if (typeof value === 'object') {
|
|
617
|
-
const obj = value as Record<string, unknown>
|
|
618
|
-
const from = typeof obj.from === 'string' ? obj.from.trim() : ''
|
|
619
|
-
const to = typeof obj.to === 'string' ? obj.to.trim() : ''
|
|
620
|
-
if (from) params.set(`${key}[from]`, from)
|
|
621
|
-
if (to) params.set(`${key}[to]`, to)
|
|
622
|
-
} else {
|
|
623
|
-
const stringValue = typeof value === 'string' ? value.trim() : String(value)
|
|
624
|
-
if (stringValue) params.set(key, stringValue)
|
|
625
|
-
}
|
|
626
|
-
})
|
|
627
|
-
const advancedParams = serializeAdvancedFilter(advancedFilterState)
|
|
322
|
+
const advancedParams = serializeTree(advancedFilterState)
|
|
628
323
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
629
324
|
params.set(key, val)
|
|
630
325
|
}
|
|
631
326
|
return params.toString()
|
|
632
|
-
}, [advancedFilterState,
|
|
327
|
+
}, [advancedFilterState, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting])
|
|
633
328
|
|
|
634
329
|
const currentParams = React.useMemo(
|
|
635
330
|
() => Object.fromEntries(new URLSearchParams(queryParams)),
|
|
@@ -702,7 +397,7 @@ export default function CustomersDealsPage() {
|
|
|
702
397
|
if (selectedPersonIds.length) selectedPersonIds.forEach((id) => params.append('personId', id))
|
|
703
398
|
if (selectedCompanyIds.length) selectedCompanyIds.forEach((id) => params.append('companyId', id))
|
|
704
399
|
if (page > 1) params.set('page', String(page))
|
|
705
|
-
const advancedParams =
|
|
400
|
+
const advancedParams = serializeTree(advancedFilterState)
|
|
706
401
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
707
402
|
params.set(key, val)
|
|
708
403
|
}
|
|
@@ -713,8 +408,6 @@ export default function CustomersDealsPage() {
|
|
|
713
408
|
}, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, advancedFilterState])
|
|
714
409
|
|
|
715
410
|
const handleRefresh = React.useCallback(() => {
|
|
716
|
-
peopleCacheRef.current.clear()
|
|
717
|
-
companiesCacheRef.current.clear()
|
|
718
411
|
void Promise.all([
|
|
719
412
|
invalidateCustomerDictionary(queryClient, 'deal-statuses'),
|
|
720
413
|
invalidateCustomerDictionary(queryClient, 'pipeline-stages'),
|
|
@@ -864,35 +557,38 @@ export default function CustomersDealsPage() {
|
|
|
864
557
|
return succeeded.length > 0
|
|
865
558
|
}, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t])
|
|
866
559
|
|
|
867
|
-
const personOptions = peopleState.options
|
|
868
|
-
const companyOptions = companiesState.options
|
|
869
|
-
|
|
870
|
-
const peopleIdToLabel = peopleState.idToLabel
|
|
871
|
-
const companyIdToLabel = companiesState.idToLabel
|
|
872
|
-
const filters = React.useMemo<FilterDef[]>(() => [
|
|
873
|
-
{
|
|
874
|
-
id: 'people',
|
|
875
|
-
label: t('customers.deals.list.filters.people'),
|
|
876
|
-
type: 'tags',
|
|
877
|
-
options: personOptions,
|
|
878
|
-
loadOptions: loadPeopleOptions,
|
|
879
|
-
placeholder: t('customers.deals.list.filters.peoplePlaceholder'),
|
|
880
|
-
formatValue: (value: string) => peopleIdToLabel[value] ?? value,
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
id: 'companies',
|
|
884
|
-
label: t('customers.deals.list.filters.companies'),
|
|
885
|
-
type: 'tags',
|
|
886
|
-
options: companyOptions,
|
|
887
|
-
loadOptions: loadCompanyOptions,
|
|
888
|
-
placeholder: t('customers.deals.list.filters.companiesPlaceholder'),
|
|
889
|
-
formatValue: (value: string) => companyIdToLabel[value] ?? value,
|
|
890
|
-
},
|
|
891
|
-
], [companyIdToLabel, companyOptions, loadCompanyOptions, loadPeopleOptions, peopleIdToLabel, personOptions, t])
|
|
892
|
-
|
|
893
560
|
const { data: customFieldDefs = [] } = useCustomFieldDefs([E.customers.customer_deal], {
|
|
894
561
|
keyExtras: [scopeVersion, reloadToken],
|
|
895
562
|
})
|
|
563
|
+
const currentUserId = useCurrentUserId()
|
|
564
|
+
const [ownerFilterOptions, setOwnerFilterOptions] = React.useState<AdvancedFilterOption[]>([])
|
|
565
|
+
React.useEffect(() => {
|
|
566
|
+
const controller = new AbortController()
|
|
567
|
+
let cancelled = false
|
|
568
|
+
void fetchAssignableStaffMembers('', { pageSize: 100, signal: controller.signal })
|
|
569
|
+
.then((items) => {
|
|
570
|
+
if (!cancelled) setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items))
|
|
571
|
+
})
|
|
572
|
+
.catch(() => {
|
|
573
|
+
if (!cancelled) setOwnerFilterOptions([])
|
|
574
|
+
})
|
|
575
|
+
return () => {
|
|
576
|
+
cancelled = true
|
|
577
|
+
controller.abort()
|
|
578
|
+
}
|
|
579
|
+
}, [scopeVersion])
|
|
580
|
+
const resolvedOwnerFilterOptions = React.useMemo(
|
|
581
|
+
() => ensureCurrentUserFilterOption(
|
|
582
|
+
ownerFilterOptions,
|
|
583
|
+
currentUserId,
|
|
584
|
+
t('customers.filters.currentUser', 'Current user'),
|
|
585
|
+
),
|
|
586
|
+
[currentUserId, ownerFilterOptions, t],
|
|
587
|
+
)
|
|
588
|
+
const loadOwnerFilterOptions = React.useCallback(async (query?: string): Promise<AdvancedFilterOption[]> => {
|
|
589
|
+
const items = await fetchAssignableStaffMembers(query ?? '', { pageSize: 100 })
|
|
590
|
+
return mapAssignableStaffToFilterOptions(items)
|
|
591
|
+
}, [])
|
|
896
592
|
|
|
897
593
|
const columns = React.useMemo<ColumnDef<DealRow>[]>(() => {
|
|
898
594
|
const noValue = <span className="text-muted-foreground text-sm">{t('customers.deals.list.noValue')}</span>
|
|
@@ -972,6 +668,7 @@ export default function CustomersDealsPage() {
|
|
|
972
668
|
alwaysVisible: true,
|
|
973
669
|
columnChooserGroup: 'Basic Info',
|
|
974
670
|
filterKey: 'title',
|
|
671
|
+
filterGroup: 'Deal',
|
|
975
672
|
maxWidth: '280px',
|
|
976
673
|
},
|
|
977
674
|
cell: ({ row }) => <span className="font-medium text-sm">{row.original.title}</span>,
|
|
@@ -979,19 +676,36 @@ export default function CustomersDealsPage() {
|
|
|
979
676
|
{
|
|
980
677
|
accessorKey: 'status',
|
|
981
678
|
header: t('customers.deals.list.columns.status'),
|
|
982
|
-
meta: {
|
|
679
|
+
meta: {
|
|
680
|
+
filterType: 'select' as const,
|
|
681
|
+
filterOptions: dictionaryOptions.dealStatuses,
|
|
682
|
+
columnChooserGroup: 'Basic Info',
|
|
683
|
+
filterKey: 'status',
|
|
684
|
+
filterGroup: 'Deal',
|
|
685
|
+
},
|
|
983
686
|
cell: ({ row }) => renderDictionaryCell('deal-statuses', row.original.status),
|
|
984
687
|
},
|
|
985
688
|
{
|
|
986
689
|
accessorKey: 'pipelineStage',
|
|
987
690
|
header: t('customers.deals.list.columns.pipelineStage'),
|
|
988
|
-
meta: {
|
|
691
|
+
meta: {
|
|
692
|
+
filterType: 'select' as const,
|
|
693
|
+
filterOptions: dictionaryOptions.pipelineStages,
|
|
694
|
+
columnChooserGroup: 'Pipeline',
|
|
695
|
+
filterKey: 'pipeline_stage',
|
|
696
|
+
filterGroup: 'Deal',
|
|
697
|
+
},
|
|
989
698
|
cell: ({ row }) => renderDictionaryCell('pipeline-stages', row.original.pipelineStage),
|
|
990
699
|
},
|
|
991
700
|
{
|
|
992
701
|
accessorKey: 'pipelineId',
|
|
993
702
|
header: t('customers.deals.list.columns.pipeline', 'Pipeline'),
|
|
994
|
-
meta: {
|
|
703
|
+
meta: {
|
|
704
|
+
columnChooserGroup: 'Pipeline',
|
|
705
|
+
filterKey: 'pipeline_id',
|
|
706
|
+
filterGroup: 'Deal',
|
|
707
|
+
maxWidth: '220px',
|
|
708
|
+
},
|
|
995
709
|
cell: ({ row }) => {
|
|
996
710
|
const name = row.original.pipelineId ? pipelineNames[row.original.pipelineId] : null
|
|
997
711
|
return name ? <span className="text-sm">{name}</span> : noValue
|
|
@@ -1000,7 +714,12 @@ export default function CustomersDealsPage() {
|
|
|
1000
714
|
{
|
|
1001
715
|
accessorKey: 'valueAmount',
|
|
1002
716
|
header: t('customers.deals.list.columns.value'),
|
|
1003
|
-
meta: {
|
|
717
|
+
meta: {
|
|
718
|
+
filterType: 'number' as const,
|
|
719
|
+
columnChooserGroup: 'Financial',
|
|
720
|
+
filterKey: 'value_amount',
|
|
721
|
+
filterGroup: 'Deal',
|
|
722
|
+
},
|
|
1004
723
|
cell: ({ row }) => (
|
|
1005
724
|
<span className="text-sm font-medium">
|
|
1006
725
|
{formatCurrency(row.original.valueAmount ?? null, row.original.valueCurrency ?? null, t('customers.deals.list.noValue'))}
|
|
@@ -1010,7 +729,12 @@ export default function CustomersDealsPage() {
|
|
|
1010
729
|
{
|
|
1011
730
|
accessorKey: 'probability',
|
|
1012
731
|
header: t('customers.deals.list.columns.probability'),
|
|
1013
|
-
meta: {
|
|
732
|
+
meta: {
|
|
733
|
+
filterType: 'number' as const,
|
|
734
|
+
columnChooserGroup: 'Financial',
|
|
735
|
+
filterKey: 'probability',
|
|
736
|
+
filterGroup: 'Deal',
|
|
737
|
+
},
|
|
1014
738
|
cell: ({ row }) => {
|
|
1015
739
|
const value = row.original.probability
|
|
1016
740
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
@@ -1022,19 +746,41 @@ export default function CustomersDealsPage() {
|
|
|
1022
746
|
{
|
|
1023
747
|
accessorKey: 'expectedCloseAt',
|
|
1024
748
|
header: t('customers.deals.list.columns.expectedClose'),
|
|
1025
|
-
meta: {
|
|
749
|
+
meta: {
|
|
750
|
+
columnChooserGroup: 'Dates',
|
|
751
|
+
filterKey: 'expected_close_at',
|
|
752
|
+
filterGroup: 'Activity',
|
|
753
|
+
filterIconName: 'calendar',
|
|
754
|
+
},
|
|
1026
755
|
cell: ({ row }) => (
|
|
1027
756
|
<span className="text-sm">
|
|
1028
757
|
{formatDateValue(row.original.expectedCloseAt ?? null, t('customers.deals.list.noValue'))}
|
|
1029
758
|
</span>
|
|
1030
759
|
),
|
|
1031
760
|
},
|
|
761
|
+
{
|
|
762
|
+
accessorKey: 'ownerUserId',
|
|
763
|
+
header: t('customers.deals.list.columns.owner', 'Owner'),
|
|
764
|
+
meta: {
|
|
765
|
+
columnChooserGroup: 'CRM',
|
|
766
|
+
filterType: 'select',
|
|
767
|
+
filterOptions: resolvedOwnerFilterOptions,
|
|
768
|
+
filterLoadOptions: loadOwnerFilterOptions,
|
|
769
|
+
filterGroup: 'CRM',
|
|
770
|
+
filterIconName: 'user-round',
|
|
771
|
+
filterKey: 'owner_user_id',
|
|
772
|
+
hidden: true,
|
|
773
|
+
},
|
|
774
|
+
cell: ({ row }) => row.original.ownerUserId ?? null,
|
|
775
|
+
},
|
|
1032
776
|
{
|
|
1033
777
|
accessorKey: 'companies',
|
|
1034
778
|
header: t('customers.deals.list.columns.companies'),
|
|
1035
779
|
meta: {
|
|
1036
780
|
columnChooserGroup: 'Associations',
|
|
1037
781
|
filterable: false,
|
|
782
|
+
filterGroup: 'CRM',
|
|
783
|
+
filterIconName: 'building-2',
|
|
1038
784
|
maxWidth: '220px',
|
|
1039
785
|
tooltipContent: (row: DealRow) =>
|
|
1040
786
|
normalizeCollectionLabels(
|
|
@@ -1049,6 +795,8 @@ export default function CustomersDealsPage() {
|
|
|
1049
795
|
meta: {
|
|
1050
796
|
columnChooserGroup: 'Associations',
|
|
1051
797
|
filterable: false,
|
|
798
|
+
filterGroup: 'CRM',
|
|
799
|
+
filterIconName: 'user-round',
|
|
1052
800
|
maxWidth: '220px',
|
|
1053
801
|
tooltipContent: (row: DealRow) =>
|
|
1054
802
|
normalizeCollectionLabels(
|
|
@@ -1060,7 +808,12 @@ export default function CustomersDealsPage() {
|
|
|
1060
808
|
{
|
|
1061
809
|
accessorKey: 'updatedAt',
|
|
1062
810
|
header: t('customers.deals.list.columns.updatedAt'),
|
|
1063
|
-
meta: {
|
|
811
|
+
meta: {
|
|
812
|
+
columnChooserGroup: 'Dates',
|
|
813
|
+
filterKey: 'updated_at',
|
|
814
|
+
filterGroup: 'Activity',
|
|
815
|
+
filterIconName: 'calendar',
|
|
816
|
+
},
|
|
1064
817
|
cell: ({ row }) => (
|
|
1065
818
|
<span className="text-sm">
|
|
1066
819
|
{formatDateValue(row.original.updatedAt ?? null, t('customers.deals.list.noValue'))}
|
|
@@ -1069,7 +822,75 @@ export default function CustomersDealsPage() {
|
|
|
1069
822
|
},
|
|
1070
823
|
...customColumns,
|
|
1071
824
|
]
|
|
1072
|
-
}, [customFieldDefs, dictionaryMaps, pipelineNames, t])
|
|
825
|
+
}, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, pipelineNames, 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 associationFilterFields = React.useMemo<FilterFieldDef[]>(() => {
|
|
850
|
+
const personLabels = new Map<string, string>()
|
|
851
|
+
const companyLabels = new Map<string, string>()
|
|
852
|
+
for (const row of rows) {
|
|
853
|
+
for (const person of row.people) {
|
|
854
|
+
if (selectedPersonIds.includes(person.id) && person.label.trim().length > 0) {
|
|
855
|
+
personLabels.set(person.id, person.label)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const company of row.companies) {
|
|
859
|
+
if (selectedCompanyIds.includes(company.id) && company.label.trim().length > 0) {
|
|
860
|
+
companyLabels.set(company.id, company.label)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return [
|
|
865
|
+
{
|
|
866
|
+
key: 'people',
|
|
867
|
+
label: t('customers.deals.list.columns.people', 'People'),
|
|
868
|
+
type: 'select',
|
|
869
|
+
options: selectedPersonIds.map((id) => ({ value: id, label: personLabels.get(id) ?? id })),
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
key: 'companies',
|
|
873
|
+
label: t('customers.deals.list.columns.companies', 'Companies'),
|
|
874
|
+
type: 'select',
|
|
875
|
+
options: selectedCompanyIds.map((id) => ({ value: id, label: companyLabels.get(id) ?? id })),
|
|
876
|
+
},
|
|
877
|
+
]
|
|
878
|
+
}, [rows, selectedCompanyIds, selectedPersonIds, t])
|
|
879
|
+
const associationFilterTree = React.useMemo<AdvancedFilterTree>(() => {
|
|
880
|
+
const rules: Array<{ field: string; operator: 'is_any_of'; value: string[] }> = []
|
|
881
|
+
if (selectedPersonIds.length) rules.push({ field: 'people', operator: 'is_any_of', value: selectedPersonIds })
|
|
882
|
+
if (selectedCompanyIds.length) rules.push({ field: 'companies', operator: 'is_any_of', value: selectedCompanyIds })
|
|
883
|
+
return rules.length ? makeMultiRuleTree(rules) : createEmptyTree()
|
|
884
|
+
}, [selectedCompanyIds, selectedPersonIds])
|
|
885
|
+
const handleAssociationFilterRemove = React.useCallback((nodeId: string) => {
|
|
886
|
+
const node = associationFilterTree.root.children.find((child) => child.id === nodeId)
|
|
887
|
+
if (!node || node.type !== 'rule') return
|
|
888
|
+
if (node.field === 'people') setSelectedPersonIds([])
|
|
889
|
+
if (node.field === 'companies') setSelectedCompanyIds([])
|
|
890
|
+
setPage(1)
|
|
891
|
+
}, [associationFilterTree.root.children])
|
|
892
|
+
|
|
893
|
+
const dealsPresets = React.useMemo<FilterPreset[]>(() => makeDealsPresets(), [])
|
|
1073
894
|
|
|
1074
895
|
return (
|
|
1075
896
|
<Page>
|
|
@@ -1136,10 +957,6 @@ export default function CustomersDealsPage() {
|
|
|
1136
957
|
searchValue={search}
|
|
1137
958
|
onSearchChange={handleSearchChange}
|
|
1138
959
|
searchPlaceholder={t('customers.deals.list.searchPlaceholder')}
|
|
1139
|
-
filters={filters}
|
|
1140
|
-
filterValues={filterValues}
|
|
1141
|
-
onFiltersApply={handleFiltersApply}
|
|
1142
|
-
onFiltersClear={handleFiltersClear}
|
|
1143
960
|
pagination={{
|
|
1144
961
|
page,
|
|
1145
962
|
pageSize,
|
|
@@ -1159,14 +976,61 @@ export default function CustomersDealsPage() {
|
|
|
1159
976
|
entityId={E.customers.customer_deal}
|
|
1160
977
|
perspective={{ tableId: 'customers.deals.list' }}
|
|
1161
978
|
advancedFilter={{
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
979
|
+
auto: true,
|
|
980
|
+
value: filterPanel.tree,
|
|
981
|
+
onChange: filterPanel.setTree,
|
|
982
|
+
onApply: () => filterPanel.flush(),
|
|
983
|
+
onClear: handleAdvancedFilterClear,
|
|
984
|
+
triggerRef: filtersTriggerRef,
|
|
985
|
+
externalPopover: true,
|
|
986
|
+
onTriggerClick: () => setFiltersOpen((prev) => !prev),
|
|
987
|
+
onApplyTree: (tree) => {
|
|
988
|
+
filterPanel.replaceTree(tree)
|
|
989
|
+
setPage(1)
|
|
990
|
+
},
|
|
991
|
+
}}
|
|
992
|
+
activeFilterChips={(
|
|
993
|
+
<>
|
|
994
|
+
<ActiveFilterChips
|
|
995
|
+
tree={associationFilterTree}
|
|
996
|
+
fields={associationFilterFields}
|
|
997
|
+
popoverOpen={filtersOpen}
|
|
998
|
+
onRemoveNode={handleAssociationFilterRemove}
|
|
999
|
+
onOpen={() => setFiltersOpen(true)}
|
|
1000
|
+
/>
|
|
1001
|
+
<ActiveFilterChips
|
|
1002
|
+
tree={filterPanel.tree}
|
|
1003
|
+
fields={advancedFilterFields}
|
|
1004
|
+
popoverOpen={filtersOpen}
|
|
1005
|
+
onRemoveNode={(id) => filterPanel.dispatch({ type: 'removeNode', nodeId: id })}
|
|
1006
|
+
onOpen={() => setFiltersOpen(true)}
|
|
1007
|
+
/>
|
|
1008
|
+
</>
|
|
1009
|
+
)}
|
|
1010
|
+
filterAwareEmptyState={{
|
|
1011
|
+
active: advancedFilterState.root.children.length > 0,
|
|
1012
|
+
entityNamePlural: t('customers.deals.entityPlural', 'deals'),
|
|
1013
|
+
canRemoveLast: filterPanel.tree.root.children.length > 0,
|
|
1014
|
+
onClearAll: handleAdvancedFilterClear,
|
|
1015
|
+
onRemoveLast: () => filterPanel.dispatch({ type: 'removeLast' }),
|
|
1016
|
+
}}
|
|
1168
1017
|
virtualized
|
|
1169
1018
|
/>
|
|
1019
|
+
<AdvancedFilterPanel
|
|
1020
|
+
fields={advancedFilterFields}
|
|
1021
|
+
value={filterPanel.tree}
|
|
1022
|
+
onChange={filterPanel.setTree}
|
|
1023
|
+
onApply={filterPanel.flush}
|
|
1024
|
+
onClear={handleAdvancedFilterClear}
|
|
1025
|
+
onFlush={filterPanel.flush}
|
|
1026
|
+
pendingErrors={filterPanel.pendingErrors}
|
|
1027
|
+
userId={currentUserId}
|
|
1028
|
+
presets={dealsPresets}
|
|
1029
|
+
open={filtersOpen}
|
|
1030
|
+
onOpenChange={setFiltersOpen}
|
|
1031
|
+
triggerRef={filtersTriggerRef}
|
|
1032
|
+
savedFilterStorageKey="customers.deals.list"
|
|
1033
|
+
/>
|
|
1170
1034
|
</PageBody>
|
|
1171
1035
|
{ConfirmDialogElement}
|
|
1172
1036
|
</Page>
|
|
@@ -1201,6 +1065,7 @@ function mapDeal(item: Record<string, unknown>): DealRow | null {
|
|
|
1201
1065
|
: null
|
|
1202
1066
|
const expectedCloseAt = typeof item.expected_close_at === 'string' ? item.expected_close_at : null
|
|
1203
1067
|
const updatedAt = typeof item.updated_at === 'string' ? item.updated_at : null
|
|
1068
|
+
const ownerUserId = typeof item.owner_user_id === 'string' ? item.owner_user_id : null
|
|
1204
1069
|
const peopleRaw = Array.isArray(item.people) ? item.people : []
|
|
1205
1070
|
const companiesRaw = Array.isArray(item.companies) ? item.companies : []
|
|
1206
1071
|
const people = peopleRaw
|
|
@@ -1239,6 +1104,7 @@ function mapDeal(item: Record<string, unknown>): DealRow | null {
|
|
|
1239
1104
|
probability,
|
|
1240
1105
|
expectedCloseAt,
|
|
1241
1106
|
updatedAt,
|
|
1107
|
+
ownerUserId,
|
|
1242
1108
|
people,
|
|
1243
1109
|
companies,
|
|
1244
1110
|
...customFields,
|