@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/domain_mapping/index.js +31 -0
  3. package/dist/generated/entities/domain_mapping/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +16 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +18 -17
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/customer_accounts/acl.js +2 -1
  11. package/dist/modules/customer_accounts/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  19. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  20. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  21. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  22. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  23. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/login.js +14 -3
  25. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  27. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  29. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +32 -5
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  33. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  34. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  35. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  36. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  37. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  38. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  39. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  40. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  41. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  56. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  57. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  58. package/dist/modules/customer_accounts/data/entities.js +85 -2
  59. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  60. package/dist/modules/customer_accounts/data/guards.js +130 -0
  61. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  62. package/dist/modules/customer_accounts/data/validators.js +19 -0
  63. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  64. package/dist/modules/customer_accounts/di.js +23 -1
  65. package/dist/modules/customer_accounts/di.js.map +2 -2
  66. package/dist/modules/customer_accounts/events.js +9 -2
  67. package/dist/modules/customer_accounts/events.js.map +2 -2
  68. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  69. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  70. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  71. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  72. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  73. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  74. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  75. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  76. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  77. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  78. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  79. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  80. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  81. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  82. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  83. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  84. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  85. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  86. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  87. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +76 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  91. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  92. package/dist/modules/customer_accounts/setup.js +48 -1
  93. package/dist/modules/customer_accounts/setup.js.map +2 -2
  94. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  95. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  96. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  97. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  98. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  99. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  100. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  101. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  102. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  103. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  104. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  105. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  106. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  107. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  108. package/dist/modules/customers/api/companies/route.js +4 -9
  109. package/dist/modules/customers/api/companies/route.js.map +2 -2
  110. package/dist/modules/customers/api/deals/route.js +4 -9
  111. package/dist/modules/customers/api/deals/route.js.map +2 -2
  112. package/dist/modules/customers/api/people/route.js +4 -9
  113. package/dist/modules/customers/api/people/route.js.map +2 -2
  114. package/dist/modules/customers/api/utils.js +0 -12
  115. package/dist/modules/customers/api/utils.js.map +2 -2
  116. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  117. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  118. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  119. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  120. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  121. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  122. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  123. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  124. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  125. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  126. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  127. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  128. package/dist/modules/customers/widgets/injection-table.js +6 -0
  129. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  130. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  131. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  132. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
  133. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  134. package/dist/modules/query_index/lib/engine.js +31 -8
  135. package/dist/modules/query_index/lib/engine.js.map +2 -2
  136. package/generated/entities/domain_mapping/index.ts +14 -0
  137. package/generated/entities.ids.generated.ts +2 -1
  138. package/generated/entity-fields-registry.ts +16 -0
  139. package/package.json +5 -5
  140. package/src/helpers/integration/salesUi.ts +27 -23
  141. package/src/modules/customer_accounts/acl.ts +1 -0
  142. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  143. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  144. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  145. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  146. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  147. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  148. package/src/modules/customer_accounts/api/login.ts +14 -3
  149. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  150. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  151. package/src/modules/customer_accounts/api/signup.ts +36 -5
  152. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  153. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  154. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  155. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  156. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  157. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  158. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  159. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  160. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  161. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  162. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  163. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  164. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  165. package/src/modules/customer_accounts/data/entities.ts +80 -1
  166. package/src/modules/customer_accounts/data/guards.ts +155 -0
  167. package/src/modules/customer_accounts/data/validators.ts +26 -0
  168. package/src/modules/customer_accounts/di.ts +35 -1
  169. package/src/modules/customer_accounts/events.ts +8 -1
  170. package/src/modules/customer_accounts/i18n/de.json +78 -0
  171. package/src/modules/customer_accounts/i18n/en.json +78 -0
  172. package/src/modules/customer_accounts/i18n/es.json +78 -0
  173. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  174. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  175. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  176. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  177. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  178. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  179. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  180. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  181. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  182. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  183. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  184. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  185. package/src/modules/customer_accounts/notifications.ts +76 -0
  186. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  187. package/src/modules/customer_accounts/setup.ts +81 -1
  188. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  189. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  190. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  191. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  192. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  193. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  194. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  195. package/src/modules/customers/api/companies/route.ts +4 -9
  196. package/src/modules/customers/api/deals/route.ts +4 -9
  197. package/src/modules/customers/api/people/route.ts +4 -9
  198. package/src/modules/customers/api/utils.ts +0 -14
  199. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  200. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  201. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  202. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  203. package/src/modules/customers/i18n/de.json +31 -0
  204. package/src/modules/customers/i18n/en.json +31 -0
  205. package/src/modules/customers/i18n/es.json +31 -0
  206. package/src/modules/customers/i18n/pl.json +31 -0
  207. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  208. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  209. package/src/modules/customers/widgets/injection-table.ts +6 -0
  210. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  211. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
  212. package/src/modules/query_index/lib/engine.ts +46 -8
@@ -1,12 +1,14 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import Link from "next/link";
5
5
  import { usePathname, useRouter, useSearchParams } from "next/navigation";
6
6
  import { useQueryClient } from "@tanstack/react-query";
7
7
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
8
8
  import { DataTable, withDataTableNamespaces } from "@open-mercato/ui/backend/DataTable";
9
- import { deserializeAdvancedFilter, serializeAdvancedFilter } from "@open-mercato/shared/lib/query/advanced-filter";
9
+ import { createEmptyTree, makeRuleTree, makeMultiRuleTree } from "@open-mercato/shared/lib/query/advanced-filter-tree";
10
+ import { deserializeTree, deserializeAdvancedFilter, flatToTree, mapDictionaryColorToTone, serializeTree } from "@open-mercato/shared/lib/query/advanced-filter";
11
+ import { useCurrentUserId } from "@open-mercato/ui/backend/utils/useCurrentUserId";
10
12
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
11
13
  import { buildCrudExportUrl, deleteCrud } from "@open-mercato/ui/backend/utils/crud";
12
14
  import { groupBulkDeleteFailures, runBulkDelete } from "@open-mercato/ui/backend/utils/bulkDelete";
@@ -35,31 +37,51 @@ import {
35
37
  supportsCustomFieldColumn
36
38
  } from "@open-mercato/ui/backend/utils/customFieldColumns";
37
39
  import { CollectionPreviewCell, normalizeCollectionLabels } from "../../../components/list/CollectionPreviewCell.js";
38
- function parsePersonLookupRecord(item) {
39
- if (typeof item !== "object" || item === null) return null;
40
- const record = item;
41
- const id = typeof record.id === "string" ? record.id : null;
42
- if (!id || !isUuid(id)) return null;
43
- const name = typeof record.display_name === "string" ? record.display_name : null;
44
- const email = typeof record.primary_email === "string" ? record.primary_email : null;
45
- const phone = typeof record.primary_phone === "string" ? record.primary_phone : null;
46
- return { id, name, email, phone };
47
- }
48
- function parseCompanyLookupRecord(item) {
49
- if (typeof item !== "object" || item === null) return null;
50
- const record = item;
51
- const id = typeof record.id === "string" ? record.id : null;
52
- if (!id || !isUuid(id)) return null;
53
- const name = typeof record.display_name === "string" ? record.display_name : null;
54
- const domain = typeof record.primary_domain === "string" ? record.primary_domain : null;
55
- const email = typeof record.primary_email === "string" ? record.primary_email : null;
56
- return { id, name, domain, email };
40
+ import { useAutoDiscoveredFields } from "@open-mercato/ui/backend/utils/useAutoDiscoveredFields";
41
+ import { useAdvancedFilterTree } from "@open-mercato/ui/backend/hooks/useAdvancedFilter";
42
+ import { AdvancedFilterPanel } from "@open-mercato/ui/backend/filters/AdvancedFilterPanel";
43
+ import { ActiveFilterChips } from "@open-mercato/ui/backend/filters/ActiveFilterChips";
44
+ import {
45
+ ensureCurrentUserFilterOption,
46
+ fetchAssignableStaffMembers,
47
+ mapAssignableStaffToFilterOptions
48
+ } from "../../../components/detail/assignableStaff.js";
49
+ function makeDealsPresets() {
50
+ return [
51
+ {
52
+ id: "my-deals",
53
+ labelKey: "customers.deals.presets.myDeals",
54
+ requiresUser: true,
55
+ build: ({ userId }) => makeRuleTree({ field: "owner_user_id", operator: "is", value: userId })
56
+ },
57
+ {
58
+ id: "closing-month",
59
+ labelKey: "customers.deals.presets.closingMonth",
60
+ iconName: "clock",
61
+ build: ({ now }) => {
62
+ const start = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
63
+ const end = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
64
+ return makeRuleTree({ field: "expected_close_at", operator: "between", value: [start, end] });
65
+ }
66
+ },
67
+ // The Deal entity has no dedicated "at risk" or health-score field — `customer_deals`
68
+ // exposes only `status` (open/win/loose/closed/in_progress, dictionary-driven) and
69
+ // `closure_outcome`. Rather than fabricate a mapping, the "At risk" preset is omitted
70
+ // until the data model exposes a first-class signal.
71
+ {
72
+ id: "won-quarter",
73
+ labelKey: "customers.deals.presets.wonQuarter",
74
+ build: ({ now }) => {
75
+ const quarter = Math.floor(now.getMonth() / 3);
76
+ const start = new Date(now.getFullYear(), quarter * 3, 1).toISOString().slice(0, 10);
77
+ return makeMultiRuleTree([
78
+ { field: "status", operator: "is", value: "win" },
79
+ { field: "expected_close_at", operator: "is_after", value: start }
80
+ ], "and");
81
+ }
82
+ }
83
+ ];
57
84
  }
58
- const EMPTY_OPTIONS_STATE = {
59
- options: [],
60
- idToLabel: {},
61
- labelToId: {}
62
- };
63
85
  const PAGE_SIZE = 20;
64
86
  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;
65
87
  function isUuid(value) {
@@ -80,93 +102,6 @@ function extractIdsFromParams(params, key) {
80
102
  const values = params.getAll(key);
81
103
  return normalizeIdCandidates(values);
82
104
  }
83
- function ensureUniqueLabel(base, occupied) {
84
- const trimmed = base.trim() || "Unnamed";
85
- if (!occupied.has(trimmed)) {
86
- occupied.add(trimmed);
87
- return trimmed;
88
- }
89
- let counter = 2;
90
- let candidate = `${trimmed} \u2022 ${counter}`;
91
- while (occupied.has(candidate)) {
92
- counter += 1;
93
- candidate = `${trimmed} \u2022 ${counter}`;
94
- }
95
- occupied.add(candidate);
96
- return candidate;
97
- }
98
- async function fetchPeopleLookup(query) {
99
- const search = new URLSearchParams();
100
- search.set("page", "1");
101
- search.set("pageSize", "20");
102
- if (query && query.trim().length) search.set("search", query.trim());
103
- try {
104
- const call = await apiCall(`/api/customers/people?${search.toString()}`);
105
- if (!call.ok) return [];
106
- const items = Array.isArray(call.result?.items) ? call.result.items : [];
107
- return items.map((item) => parsePersonLookupRecord(item)).filter((record) => record !== null);
108
- } catch {
109
- return [];
110
- }
111
- }
112
- async function fetchPeopleLookupByIds(ids) {
113
- const unique = Array.from(new Set(ids.filter((id) => isUuid(id))));
114
- if (!unique.length) return [];
115
- const results = await Promise.all(
116
- unique.map(async (id) => {
117
- const search = new URLSearchParams();
118
- search.set("id", id);
119
- search.set("page", "1");
120
- search.set("pageSize", "1");
121
- try {
122
- const call = await apiCall(`/api/customers/people?${search.toString()}`);
123
- if (!call.ok) return null;
124
- const items = Array.isArray(call.result?.items) ? call.result.items : [];
125
- const match = items.map((item) => parsePersonLookupRecord(item)).find((record) => record?.id === id);
126
- return match ?? null;
127
- } catch {
128
- return null;
129
- }
130
- })
131
- );
132
- return results.filter((record) => !!record);
133
- }
134
- async function fetchCompaniesLookup(query) {
135
- const search = new URLSearchParams();
136
- search.set("page", "1");
137
- search.set("pageSize", "20");
138
- if (query && query.trim().length) search.set("search", query.trim());
139
- try {
140
- const call = await apiCall(`/api/customers/companies?${search.toString()}`);
141
- if (!call.ok) return [];
142
- const items = Array.isArray(call.result?.items) ? call.result.items : [];
143
- return items.map((item) => parseCompanyLookupRecord(item)).filter((record) => record !== null);
144
- } catch {
145
- return [];
146
- }
147
- }
148
- async function fetchCompaniesLookupByIds(ids) {
149
- const unique = Array.from(new Set(ids.filter((id) => isUuid(id))));
150
- if (!unique.length) return [];
151
- const results = await Promise.all(
152
- unique.map(async (id) => {
153
- const search = new URLSearchParams();
154
- search.set("id", id);
155
- search.set("page", "1");
156
- search.set("pageSize", "1");
157
- try {
158
- const call = await apiCall(`/api/customers/companies?${search.toString()}`);
159
- if (!call.ok) return null;
160
- const items = Array.isArray(call.result?.items) ? call.result.items : [];
161
- const match = items.map((item) => parseCompanyLookupRecord(item)).find((record) => record?.id === id);
162
- return match ?? null;
163
- } catch {
164
- return null;
165
- }
166
- })
167
- );
168
- return results.filter((record) => !!record);
169
- }
170
105
  function formatCurrency(amount, currency, fallback) {
171
106
  if (typeof amount !== "number" || Number.isNaN(amount)) return fallback;
172
107
  try {
@@ -186,13 +121,6 @@ function formatDateValue(value, fallback) {
186
121
  if (Number.isNaN(date.getTime())) return fallback;
187
122
  return date.toLocaleDateString();
188
123
  }
189
- function arraysEqual(a, b) {
190
- if (a.length !== b.length) return false;
191
- for (let i = 0; i < a.length; i += 1) {
192
- if (a[i] !== b[i]) return false;
193
- }
194
- return true;
195
- }
196
124
  function CustomersDealsPage() {
197
125
  const t = useT();
198
126
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
@@ -214,17 +142,31 @@ function CustomersDealsPage() {
214
142
  const [isLoading, setIsLoading] = React.useState(false);
215
143
  const [reloadToken, setReloadToken] = React.useState(0);
216
144
  const [pendingDeleteId, setPendingDeleteId] = React.useState(null);
217
- const [filterValues, setFilterValues] = React.useState({});
218
- const [advancedFilterState, setAdvancedFilterState] = React.useState(() => {
219
- const params = searchParams;
220
- if (!params) return { logic: "and", conditions: [] };
145
+ const initialFilterTree = React.useMemo(() => {
146
+ if (!searchParams) return createEmptyTree();
221
147
  const record = {};
222
- params.forEach((value, key) => {
148
+ searchParams.forEach((value, key) => {
223
149
  if (key.startsWith("filter[")) record[key] = value;
224
150
  });
225
- const hydrated = deserializeAdvancedFilter(record);
226
- return hydrated ?? { logic: "and", conditions: [] };
151
+ const v2 = deserializeTree(record);
152
+ if (v2) return v2;
153
+ const flat = deserializeAdvancedFilter(record);
154
+ if (flat) return flatToTree(flat);
155
+ return createEmptyTree();
156
+ }, []);
157
+ const [panelFields, setPanelFields] = React.useState([]);
158
+ const [filtersOpen, setFiltersOpen] = React.useState(false);
159
+ const filtersTriggerRef = React.useRef(null);
160
+ const filterPanel = useAdvancedFilterTree({
161
+ initial: initialFilterTree,
162
+ fields: panelFields,
163
+ onApply: () => setPage(1)
227
164
  });
165
+ const advancedFilterState = filterPanel.appliedTree;
166
+ const handleAdvancedFilterClear = React.useCallback(() => {
167
+ filterPanel.clear();
168
+ setPage(1);
169
+ }, [filterPanel]);
228
170
  const [cacheStatus, setCacheStatus] = React.useState(null);
229
171
  const initialPersonIds = React.useMemo(
230
172
  () => extractIdsFromParams(searchParams, "personId"),
@@ -236,142 +178,6 @@ function CustomersDealsPage() {
236
178
  );
237
179
  const [selectedPersonIds, setSelectedPersonIds] = React.useState(initialPersonIds);
238
180
  const [selectedCompanyIds, setSelectedCompanyIds] = React.useState(initialCompanyIds);
239
- const [peopleState, setPeopleState] = React.useState(EMPTY_OPTIONS_STATE);
240
- const [companiesState, setCompaniesState] = React.useState(EMPTY_OPTIONS_STATE);
241
- const peopleCacheRef = React.useRef(/* @__PURE__ */ new Map());
242
- const companiesCacheRef = React.useRef(/* @__PURE__ */ new Map());
243
- const buildPersonLabel = React.useCallback((record) => {
244
- const parts = [];
245
- const name = record.name?.trim();
246
- if (name) parts.push(name);
247
- const email = record.email?.trim();
248
- if (email && !parts.includes(email)) parts.push(email);
249
- const phone = record.phone?.trim();
250
- if (!parts.length && phone) parts.push(phone);
251
- if (!parts.length) parts.push(t("customers.deals.list.unnamedPerson", "Unnamed person"));
252
- return parts.join(" \u2022 ");
253
- }, [t]);
254
- const buildCompanyLabel = React.useCallback((record) => {
255
- const parts = [];
256
- const name = record.name?.trim();
257
- if (name) parts.push(name);
258
- const domain = record.domain?.trim();
259
- if (domain && !parts.includes(domain)) parts.push(domain);
260
- const email = record.email?.trim();
261
- if (!parts.length && email) parts.push(email);
262
- if (!parts.length) parts.push(t("customers.deals.list.unnamedCompany", "Unnamed company"));
263
- return parts.join(" \u2022 ");
264
- }, [t]);
265
- const ingestPeopleRecords = React.useCallback((records) => {
266
- if (!records.length) return [];
267
- const queryMap = /* @__PURE__ */ new Map();
268
- setPeopleState((prev) => {
269
- const idToLabel = { ...prev.idToLabel };
270
- const labelToId = {};
271
- const merged = new Map(prev.options.map((opt) => [opt.value, opt]));
272
- const occupied = /* @__PURE__ */ new Set();
273
- Object.entries(prev.labelToId).forEach(([label, id]) => {
274
- occupied.add(label);
275
- labelToId[label] = id;
276
- });
277
- records.forEach((record) => {
278
- if (!isUuid(record.id)) return;
279
- const base = buildPersonLabel(record);
280
- let previousLabel = idToLabel[record.id];
281
- if (previousLabel) {
282
- delete labelToId[previousLabel];
283
- occupied.delete(previousLabel);
284
- }
285
- const label = ensureUniqueLabel(base, occupied);
286
- idToLabel[record.id] = label;
287
- labelToId[label] = record.id;
288
- const option = { value: record.id, label };
289
- merged.set(record.id, option);
290
- queryMap.set(record.id, option);
291
- });
292
- const nextOptions = Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label));
293
- return { options: nextOptions, idToLabel, labelToId };
294
- });
295
- return Array.from(queryMap.values()).sort((a, b) => a.label.localeCompare(b.label));
296
- }, [buildPersonLabel]);
297
- const ingestCompanyRecords = React.useCallback((records) => {
298
- if (!records.length) return [];
299
- const queryMap = /* @__PURE__ */ new Map();
300
- setCompaniesState((prev) => {
301
- const idToLabel = { ...prev.idToLabel };
302
- const labelToId = {};
303
- const merged = new Map(prev.options.map((opt) => [opt.value, opt]));
304
- const occupied = /* @__PURE__ */ new Set();
305
- Object.entries(prev.labelToId).forEach(([label, id]) => {
306
- occupied.add(label);
307
- labelToId[label] = id;
308
- });
309
- records.forEach((record) => {
310
- if (!isUuid(record.id)) return;
311
- const base = buildCompanyLabel(record);
312
- let previousLabel = idToLabel[record.id];
313
- if (previousLabel) {
314
- delete labelToId[previousLabel];
315
- occupied.delete(previousLabel);
316
- }
317
- const label = ensureUniqueLabel(base, occupied);
318
- idToLabel[record.id] = label;
319
- labelToId[label] = record.id;
320
- const option = { value: record.id, label };
321
- merged.set(record.id, option);
322
- queryMap.set(record.id, option);
323
- });
324
- const nextOptions = Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label));
325
- return { options: nextOptions, idToLabel, labelToId };
326
- });
327
- return Array.from(queryMap.values()).sort((a, b) => a.label.localeCompare(b.label));
328
- }, [buildCompanyLabel]);
329
- const loadPeopleOptions = React.useCallback(async (query) => {
330
- const normalizedQuery = (query || "").trim().toLowerCase();
331
- const cacheKey = `${scopeVersion}|${normalizedQuery}`;
332
- const cached = peopleCacheRef.current.get(cacheKey);
333
- if (cached) return cached;
334
- const records = await fetchPeopleLookup(query);
335
- const options = ingestPeopleRecords(records);
336
- peopleCacheRef.current.set(cacheKey, options);
337
- return options;
338
- }, [scopeVersion, ingestPeopleRecords]);
339
- const loadCompanyOptions = React.useCallback(async (query) => {
340
- const normalizedQuery = (query || "").trim().toLowerCase();
341
- const cacheKey = `${scopeVersion}|${normalizedQuery}`;
342
- const cached = companiesCacheRef.current.get(cacheKey);
343
- if (cached) return cached;
344
- const records = await fetchCompaniesLookup(query);
345
- const options = ingestCompanyRecords(records);
346
- companiesCacheRef.current.set(cacheKey, options);
347
- return options;
348
- }, [scopeVersion, ingestCompanyRecords]);
349
- React.useEffect(() => {
350
- let cancelled = false;
351
- if (!selectedPersonIds.length) return;
352
- const missing = selectedPersonIds.filter((id) => !peopleState.idToLabel[id]);
353
- if (!missing.length) return;
354
- fetchPeopleLookupByIds(missing).then((records) => {
355
- if (cancelled) return;
356
- ingestPeopleRecords(records);
357
- });
358
- return () => {
359
- cancelled = true;
360
- };
361
- }, [selectedPersonIds, peopleState.idToLabel, ingestPeopleRecords]);
362
- React.useEffect(() => {
363
- let cancelled = false;
364
- if (!selectedCompanyIds.length) return;
365
- const missing = selectedCompanyIds.filter((id) => !companiesState.idToLabel[id]);
366
- if (!missing.length) return;
367
- fetchCompaniesLookupByIds(missing).then((records) => {
368
- if (cancelled) return;
369
- ingestCompanyRecords(records);
370
- });
371
- return () => {
372
- cancelled = true;
373
- };
374
- }, [selectedCompanyIds, companiesState.idToLabel, ingestCompanyRecords]);
375
181
  const [dictionaryMaps, setDictionaryMaps] = React.useState({
376
182
  "deal-statuses": {},
377
183
  "pipeline-stages": {}
@@ -400,6 +206,18 @@ function CustomersDealsPage() {
400
206
  cancelled = true;
401
207
  };
402
208
  }, [fetchDictionaryEntries, reloadToken]);
209
+ const dictionaryOptions = React.useMemo(() => {
210
+ const toOptions = (map) => Object.values(map ?? {}).map((entry) => {
211
+ const tone = mapDictionaryColorToTone(entry.color);
212
+ const option = { value: entry.value, label: entry.label };
213
+ if (tone) option.tone = tone;
214
+ return option;
215
+ }).sort((a, b) => a.label.localeCompare(b.label, void 0, { sensitivity: "base" }));
216
+ return {
217
+ dealStatuses: toOptions(dictionaryMaps["deal-statuses"]),
218
+ pipelineStages: toOptions(dictionaryMaps["pipeline-stages"])
219
+ };
220
+ }, [dictionaryMaps]);
403
221
  React.useEffect(() => {
404
222
  let cancelled = false;
405
223
  async function loadPipelines() {
@@ -423,70 +241,10 @@ function CustomersDealsPage() {
423
241
  cancelled = true;
424
242
  };
425
243
  }, [reloadToken, scopeVersion]);
426
- React.useEffect(() => {
427
- peopleCacheRef.current.clear();
428
- companiesCacheRef.current.clear();
429
- setPeopleState((prev) => {
430
- if (!prev.options.length && !Object.keys(prev.idToLabel).length) return prev;
431
- return { ...EMPTY_OPTIONS_STATE };
432
- });
433
- setCompaniesState((prev) => {
434
- if (!prev.options.length && !Object.keys(prev.idToLabel).length) return prev;
435
- return { ...EMPTY_OPTIONS_STATE };
436
- });
437
- }, [scopeVersion, reloadToken]);
438
- const syncFilterIds = React.useCallback((key, ids) => {
439
- setFilterValues((prev) => {
440
- const current = Array.isArray(prev[key]) ? prev[key] : [];
441
- if (!ids.length) {
442
- if (!current.length) return prev;
443
- const next = { ...prev };
444
- delete next[key];
445
- return next;
446
- }
447
- if (arraysEqual(current, ids)) return prev;
448
- return { ...prev, [key]: [...ids] };
449
- });
450
- }, []);
451
- React.useEffect(() => {
452
- syncFilterIds("people", selectedPersonIds);
453
- }, [selectedPersonIds, syncFilterIds]);
454
- React.useEffect(() => {
455
- syncFilterIds("companies", selectedCompanyIds);
456
- }, [selectedCompanyIds, syncFilterIds]);
457
244
  const handleSearchChange = React.useCallback((value) => {
458
245
  setSearch(value.trim());
459
246
  setPage(1);
460
247
  }, []);
461
- const handleFiltersApply = React.useCallback((values) => {
462
- const next = { ...values };
463
- const rawPeople = Array.isArray(values.people) ? values.people : [];
464
- const nextPersonIds = Array.from(
465
- new Set(
466
- rawPeople.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0 && isUuid(value))
467
- )
468
- );
469
- setSelectedPersonIds(nextPersonIds);
470
- if (nextPersonIds.length) next.people = nextPersonIds;
471
- else delete next.people;
472
- const rawCompanies = Array.isArray(values.companies) ? values.companies : [];
473
- const nextCompanyIds = Array.from(
474
- new Set(
475
- rawCompanies.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0 && isUuid(value))
476
- )
477
- );
478
- setSelectedCompanyIds(nextCompanyIds);
479
- if (nextCompanyIds.length) next.companies = nextCompanyIds;
480
- else delete next.companies;
481
- setFilterValues(next);
482
- setPage(1);
483
- }, []);
484
- const handleFiltersClear = React.useCallback(() => {
485
- setFilterValues({});
486
- setSelectedPersonIds([]);
487
- setSelectedCompanyIds([]);
488
- setPage(1);
489
- }, []);
490
248
  const queryParams = React.useMemo(() => {
491
249
  const params = new URLSearchParams();
492
250
  params.set("page", String(page));
@@ -498,33 +256,12 @@ function CustomersDealsPage() {
498
256
  if (search.trim().length) params.set("search", search.trim());
499
257
  if (selectedPersonIds.length) params.set("personId", selectedPersonIds.join(","));
500
258
  if (selectedCompanyIds.length) params.set("companyId", selectedCompanyIds.join(","));
501
- Object.entries(filterValues).forEach(([key, value]) => {
502
- if (key === "people" || key === "companies") return;
503
- if (value == null) return;
504
- if (Array.isArray(value)) {
505
- const normalized = value.map((item) => {
506
- if (item == null) return "";
507
- if (typeof item === "string") return item.trim();
508
- return String(item).trim();
509
- }).filter((item) => item.length > 0);
510
- if (normalized.length) params.set(key, normalized.join(","));
511
- } else if (typeof value === "object") {
512
- const obj = value;
513
- const from = typeof obj.from === "string" ? obj.from.trim() : "";
514
- const to = typeof obj.to === "string" ? obj.to.trim() : "";
515
- if (from) params.set(`${key}[from]`, from);
516
- if (to) params.set(`${key}[to]`, to);
517
- } else {
518
- const stringValue = typeof value === "string" ? value.trim() : String(value);
519
- if (stringValue) params.set(key, stringValue);
520
- }
521
- });
522
- const advancedParams = serializeAdvancedFilter(advancedFilterState);
259
+ const advancedParams = serializeTree(advancedFilterState);
523
260
  for (const [key, val] of Object.entries(advancedParams)) {
524
261
  params.set(key, val);
525
262
  }
526
263
  return params.toString();
527
- }, [advancedFilterState, filterValues, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting]);
264
+ }, [advancedFilterState, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting]);
528
265
  const currentParams = React.useMemo(
529
266
  () => Object.fromEntries(new URLSearchParams(queryParams)),
530
267
  [queryParams]
@@ -587,7 +324,7 @@ function CustomersDealsPage() {
587
324
  if (selectedPersonIds.length) selectedPersonIds.forEach((id) => params.append("personId", id));
588
325
  if (selectedCompanyIds.length) selectedCompanyIds.forEach((id) => params.append("companyId", id));
589
326
  if (page > 1) params.set("page", String(page));
590
- const advancedParams = serializeAdvancedFilter(advancedFilterState);
327
+ const advancedParams = serializeTree(advancedFilterState);
591
328
  for (const [key, val] of Object.entries(advancedParams)) {
592
329
  params.set(key, val);
593
330
  }
@@ -597,8 +334,6 @@ function CustomersDealsPage() {
597
334
  router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false });
598
335
  }, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, advancedFilterState]);
599
336
  const handleRefresh = React.useCallback(() => {
600
- peopleCacheRef.current.clear();
601
- companiesCacheRef.current.clear();
602
337
  void Promise.all([
603
338
  invalidateCustomerDictionary(queryClient, "deal-statuses"),
604
339
  invalidateCustomerDictionary(queryClient, "pipeline-stages")
@@ -724,33 +459,36 @@ function CustomersDealsPage() {
724
459
  }
725
460
  return succeeded.length > 0;
726
461
  }, [bulkMutationContextId, confirm, retryBulkMutation, runBulkMutation, t]);
727
- const personOptions = peopleState.options;
728
- const companyOptions = companiesState.options;
729
- const peopleIdToLabel = peopleState.idToLabel;
730
- const companyIdToLabel = companiesState.idToLabel;
731
- const filters = React.useMemo(() => [
732
- {
733
- id: "people",
734
- label: t("customers.deals.list.filters.people"),
735
- type: "tags",
736
- options: personOptions,
737
- loadOptions: loadPeopleOptions,
738
- placeholder: t("customers.deals.list.filters.peoplePlaceholder"),
739
- formatValue: (value) => peopleIdToLabel[value] ?? value
740
- },
741
- {
742
- id: "companies",
743
- label: t("customers.deals.list.filters.companies"),
744
- type: "tags",
745
- options: companyOptions,
746
- loadOptions: loadCompanyOptions,
747
- placeholder: t("customers.deals.list.filters.companiesPlaceholder"),
748
- formatValue: (value) => companyIdToLabel[value] ?? value
749
- }
750
- ], [companyIdToLabel, companyOptions, loadCompanyOptions, loadPeopleOptions, peopleIdToLabel, personOptions, t]);
751
462
  const { data: customFieldDefs = [] } = useCustomFieldDefs([E.customers.customer_deal], {
752
463
  keyExtras: [scopeVersion, reloadToken]
753
464
  });
465
+ const currentUserId = useCurrentUserId();
466
+ const [ownerFilterOptions, setOwnerFilterOptions] = React.useState([]);
467
+ React.useEffect(() => {
468
+ const controller = new AbortController();
469
+ let cancelled = false;
470
+ void fetchAssignableStaffMembers("", { pageSize: 100, signal: controller.signal }).then((items) => {
471
+ if (!cancelled) setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items));
472
+ }).catch(() => {
473
+ if (!cancelled) setOwnerFilterOptions([]);
474
+ });
475
+ return () => {
476
+ cancelled = true;
477
+ controller.abort();
478
+ };
479
+ }, [scopeVersion]);
480
+ const resolvedOwnerFilterOptions = React.useMemo(
481
+ () => ensureCurrentUserFilterOption(
482
+ ownerFilterOptions,
483
+ currentUserId,
484
+ t("customers.filters.currentUser", "Current user")
485
+ ),
486
+ [currentUserId, ownerFilterOptions, t]
487
+ );
488
+ const loadOwnerFilterOptions = React.useCallback(async (query) => {
489
+ const items = await fetchAssignableStaffMembers(query ?? "", { pageSize: 100 });
490
+ return mapAssignableStaffToFilterOptions(items);
491
+ }, []);
754
492
  const columns = React.useMemo(() => {
755
493
  const noValue = /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: t("customers.deals.list.noValue") });
756
494
  const renderDictionaryCell = (kind, value) => /* @__PURE__ */ jsx(
@@ -814,6 +552,7 @@ function CustomersDealsPage() {
814
552
  alwaysVisible: true,
815
553
  columnChooserGroup: "Basic Info",
816
554
  filterKey: "title",
555
+ filterGroup: "Deal",
817
556
  maxWidth: "280px"
818
557
  },
819
558
  cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: row.original.title })
@@ -821,19 +560,36 @@ function CustomersDealsPage() {
821
560
  {
822
561
  accessorKey: "status",
823
562
  header: t("customers.deals.list.columns.status"),
824
- meta: { filterType: "select", columnChooserGroup: "Basic Info", filterKey: "status" },
563
+ meta: {
564
+ filterType: "select",
565
+ filterOptions: dictionaryOptions.dealStatuses,
566
+ columnChooserGroup: "Basic Info",
567
+ filterKey: "status",
568
+ filterGroup: "Deal"
569
+ },
825
570
  cell: ({ row }) => renderDictionaryCell("deal-statuses", row.original.status)
826
571
  },
827
572
  {
828
573
  accessorKey: "pipelineStage",
829
574
  header: t("customers.deals.list.columns.pipelineStage"),
830
- meta: { columnChooserGroup: "Pipeline", filterKey: "pipeline_stage" },
575
+ meta: {
576
+ filterType: "select",
577
+ filterOptions: dictionaryOptions.pipelineStages,
578
+ columnChooserGroup: "Pipeline",
579
+ filterKey: "pipeline_stage",
580
+ filterGroup: "Deal"
581
+ },
831
582
  cell: ({ row }) => renderDictionaryCell("pipeline-stages", row.original.pipelineStage)
832
583
  },
833
584
  {
834
585
  accessorKey: "pipelineId",
835
586
  header: t("customers.deals.list.columns.pipeline", "Pipeline"),
836
- meta: { columnChooserGroup: "Pipeline", filterKey: "pipeline_id", maxWidth: "220px" },
587
+ meta: {
588
+ columnChooserGroup: "Pipeline",
589
+ filterKey: "pipeline_id",
590
+ filterGroup: "Deal",
591
+ maxWidth: "220px"
592
+ },
837
593
  cell: ({ row }) => {
838
594
  const name = row.original.pipelineId ? pipelineNames[row.original.pipelineId] : null;
839
595
  return name ? /* @__PURE__ */ jsx("span", { className: "text-sm", children: name }) : noValue;
@@ -842,13 +598,23 @@ function CustomersDealsPage() {
842
598
  {
843
599
  accessorKey: "valueAmount",
844
600
  header: t("customers.deals.list.columns.value"),
845
- meta: { filterType: "number", columnChooserGroup: "Financial", filterKey: "value_amount" },
601
+ meta: {
602
+ filterType: "number",
603
+ columnChooserGroup: "Financial",
604
+ filterKey: "value_amount",
605
+ filterGroup: "Deal"
606
+ },
846
607
  cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: formatCurrency(row.original.valueAmount ?? null, row.original.valueCurrency ?? null, t("customers.deals.list.noValue")) })
847
608
  },
848
609
  {
849
610
  accessorKey: "probability",
850
611
  header: t("customers.deals.list.columns.probability"),
851
- meta: { filterType: "number", columnChooserGroup: "Financial", filterKey: "probability" },
612
+ meta: {
613
+ filterType: "number",
614
+ columnChooserGroup: "Financial",
615
+ filterKey: "probability",
616
+ filterGroup: "Deal"
617
+ },
852
618
  cell: ({ row }) => {
853
619
  const value = row.original.probability;
854
620
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -860,15 +626,37 @@ function CustomersDealsPage() {
860
626
  {
861
627
  accessorKey: "expectedCloseAt",
862
628
  header: t("customers.deals.list.columns.expectedClose"),
863
- meta: { columnChooserGroup: "Dates", filterKey: "expected_close_at" },
629
+ meta: {
630
+ columnChooserGroup: "Dates",
631
+ filterKey: "expected_close_at",
632
+ filterGroup: "Activity",
633
+ filterIconName: "calendar"
634
+ },
864
635
  cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm", children: formatDateValue(row.original.expectedCloseAt ?? null, t("customers.deals.list.noValue")) })
865
636
  },
637
+ {
638
+ accessorKey: "ownerUserId",
639
+ header: t("customers.deals.list.columns.owner", "Owner"),
640
+ meta: {
641
+ columnChooserGroup: "CRM",
642
+ filterType: "select",
643
+ filterOptions: resolvedOwnerFilterOptions,
644
+ filterLoadOptions: loadOwnerFilterOptions,
645
+ filterGroup: "CRM",
646
+ filterIconName: "user-round",
647
+ filterKey: "owner_user_id",
648
+ hidden: true
649
+ },
650
+ cell: ({ row }) => row.original.ownerUserId ?? null
651
+ },
866
652
  {
867
653
  accessorKey: "companies",
868
654
  header: t("customers.deals.list.columns.companies"),
869
655
  meta: {
870
656
  columnChooserGroup: "Associations",
871
657
  filterable: false,
658
+ filterGroup: "CRM",
659
+ filterIconName: "building-2",
872
660
  maxWidth: "220px",
873
661
  tooltipContent: (row) => normalizeCollectionLabels(
874
662
  row.companies.map((entry) => entry.label && entry.label.trim().length ? entry.label : t("customers.deals.list.unnamedCompany"))
@@ -882,6 +670,8 @@ function CustomersDealsPage() {
882
670
  meta: {
883
671
  columnChooserGroup: "Associations",
884
672
  filterable: false,
673
+ filterGroup: "CRM",
674
+ filterIconName: "user-round",
885
675
  maxWidth: "220px",
886
676
  tooltipContent: (row) => normalizeCollectionLabels(
887
677
  row.people.map((entry) => entry.label && entry.label.trim().length ? entry.label : t("customers.deals.list.unnamedPerson"))
@@ -892,109 +682,222 @@ function CustomersDealsPage() {
892
682
  {
893
683
  accessorKey: "updatedAt",
894
684
  header: t("customers.deals.list.columns.updatedAt"),
895
- meta: { columnChooserGroup: "Dates", filterKey: "updated_at" },
685
+ meta: {
686
+ columnChooserGroup: "Dates",
687
+ filterKey: "updated_at",
688
+ filterGroup: "Activity",
689
+ filterIconName: "calendar"
690
+ },
896
691
  cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm", children: formatDateValue(row.original.updatedAt ?? null, t("customers.deals.list.noValue")) })
897
692
  },
898
693
  ...customColumns
899
694
  ];
900
- }, [customFieldDefs, dictionaryMaps, pipelineNames, t]);
901
- return /* @__PURE__ */ jsxs(Page, { children: [
902
- /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
903
- DataTable,
695
+ }, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, pipelineNames, resolvedOwnerFilterOptions, t]);
696
+ const { advancedFilterFields } = useAutoDiscoveredFields({ columns, customFieldDefs });
697
+ React.useEffect(() => {
698
+ setPanelFields((prev) => {
699
+ if (prev === advancedFilterFields) return prev;
700
+ if (prev.length === advancedFilterFields.length) {
701
+ let same = true;
702
+ for (let i = 0; i < prev.length; i++) {
703
+ if (prev[i].key !== advancedFilterFields[i].key) {
704
+ same = false;
705
+ break;
706
+ }
707
+ }
708
+ if (same) return prev;
709
+ }
710
+ return advancedFilterFields;
711
+ });
712
+ }, [advancedFilterFields]);
713
+ const associationFilterFields = React.useMemo(() => {
714
+ const personLabels = /* @__PURE__ */ new Map();
715
+ const companyLabels = /* @__PURE__ */ new Map();
716
+ for (const row of rows) {
717
+ for (const person of row.people) {
718
+ if (selectedPersonIds.includes(person.id) && person.label.trim().length > 0) {
719
+ personLabels.set(person.id, person.label);
720
+ }
721
+ }
722
+ for (const company of row.companies) {
723
+ if (selectedCompanyIds.includes(company.id) && company.label.trim().length > 0) {
724
+ companyLabels.set(company.id, company.label);
725
+ }
726
+ }
727
+ }
728
+ return [
904
729
  {
905
- stickyFirstColumn: true,
906
- stickyActionsColumn: true,
907
- title: t("customers.deals.list.title"),
908
- actions: /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/backend/customers/deals/create", children: t("customers.deals.list.actions.new", "New deal") }) }),
909
- columns,
910
- columnChooser: { auto: true },
911
- data: rows,
912
- onRowClick: (row) => {
913
- router.push(`/backend/customers/deals/${row.id}`);
914
- },
915
- rowActions: (row) => {
916
- const isDeleting = pendingDeleteId === row.id;
917
- return /* @__PURE__ */ jsx(
918
- RowActions,
919
- {
920
- items: [
921
- {
922
- id: "edit",
923
- label: t("customers.deals.list.actions.edit", "Edit"),
924
- onSelect: () => {
925
- router.push(`/backend/customers/deals/${row.id}`);
926
- }
927
- },
928
- {
929
- id: "open-new-tab",
930
- label: t("customers.deals.list.actions.openInNewTab", "Open in new tab"),
931
- onSelect: () => {
932
- if (typeof window !== "undefined") {
933
- window.open(`/backend/customers/deals/${row.id}`, "_blank", "noopener");
730
+ key: "people",
731
+ label: t("customers.deals.list.columns.people", "People"),
732
+ type: "select",
733
+ options: selectedPersonIds.map((id) => ({ value: id, label: personLabels.get(id) ?? id }))
734
+ },
735
+ {
736
+ key: "companies",
737
+ label: t("customers.deals.list.columns.companies", "Companies"),
738
+ type: "select",
739
+ options: selectedCompanyIds.map((id) => ({ value: id, label: companyLabels.get(id) ?? id }))
740
+ }
741
+ ];
742
+ }, [rows, selectedCompanyIds, selectedPersonIds, t]);
743
+ const associationFilterTree = React.useMemo(() => {
744
+ const rules = [];
745
+ if (selectedPersonIds.length) rules.push({ field: "people", operator: "is_any_of", value: selectedPersonIds });
746
+ if (selectedCompanyIds.length) rules.push({ field: "companies", operator: "is_any_of", value: selectedCompanyIds });
747
+ return rules.length ? makeMultiRuleTree(rules) : createEmptyTree();
748
+ }, [selectedCompanyIds, selectedPersonIds]);
749
+ const handleAssociationFilterRemove = React.useCallback((nodeId) => {
750
+ const node = associationFilterTree.root.children.find((child) => child.id === nodeId);
751
+ if (!node || node.type !== "rule") return;
752
+ if (node.field === "people") setSelectedPersonIds([]);
753
+ if (node.field === "companies") setSelectedCompanyIds([]);
754
+ setPage(1);
755
+ }, [associationFilterTree.root.children]);
756
+ const dealsPresets = React.useMemo(() => makeDealsPresets(), []);
757
+ return /* @__PURE__ */ jsxs(Page, { children: [
758
+ /* @__PURE__ */ jsxs(PageBody, { children: [
759
+ /* @__PURE__ */ jsx(
760
+ DataTable,
761
+ {
762
+ stickyFirstColumn: true,
763
+ stickyActionsColumn: true,
764
+ title: t("customers.deals.list.title"),
765
+ actions: /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/backend/customers/deals/create", children: t("customers.deals.list.actions.new", "New deal") }) }),
766
+ columns,
767
+ columnChooser: { auto: true },
768
+ data: rows,
769
+ onRowClick: (row) => {
770
+ router.push(`/backend/customers/deals/${row.id}`);
771
+ },
772
+ rowActions: (row) => {
773
+ const isDeleting = pendingDeleteId === row.id;
774
+ return /* @__PURE__ */ jsx(
775
+ RowActions,
776
+ {
777
+ items: [
778
+ {
779
+ id: "edit",
780
+ label: t("customers.deals.list.actions.edit", "Edit"),
781
+ onSelect: () => {
782
+ router.push(`/backend/customers/deals/${row.id}`);
783
+ }
784
+ },
785
+ {
786
+ id: "open-new-tab",
787
+ label: t("customers.deals.list.actions.openInNewTab", "Open in new tab"),
788
+ onSelect: () => {
789
+ if (typeof window !== "undefined") {
790
+ window.open(`/backend/customers/deals/${row.id}`, "_blank", "noopener");
791
+ }
934
792
  }
793
+ },
794
+ {
795
+ id: "delete",
796
+ label: isDeleting ? t("customers.deals.list.actions.deleting", "Deleting\u2026") : t("customers.deals.list.actions.delete", "Delete"),
797
+ destructive: true,
798
+ onSelect: () => handleDeleteDeal(row.id)
935
799
  }
936
- },
937
- {
938
- id: "delete",
939
- label: isDeleting ? t("customers.deals.list.actions.deleting", "Deleting\u2026") : t("customers.deals.list.actions.delete", "Delete"),
940
- destructive: true,
941
- onSelect: () => handleDeleteDeal(row.id)
942
- }
943
- ]
800
+ ]
801
+ }
802
+ );
803
+ },
804
+ sortable: true,
805
+ sorting,
806
+ onSortingChange: setSorting,
807
+ bulkActions: [
808
+ {
809
+ id: "delete",
810
+ label: t("customers.deals.list.actions.delete", "Delete"),
811
+ destructive: true,
812
+ onExecute: handleBulkDelete
944
813
  }
945
- );
946
- },
947
- sortable: true,
948
- sorting,
949
- onSortingChange: setSorting,
950
- bulkActions: [
951
- {
952
- id: "delete",
953
- label: t("customers.deals.list.actions.delete", "Delete"),
954
- destructive: true,
955
- onExecute: handleBulkDelete
956
- }
957
- ],
958
- searchValue: search,
959
- onSearchChange: handleSearchChange,
960
- searchPlaceholder: t("customers.deals.list.searchPlaceholder"),
961
- filters,
962
- filterValues,
963
- onFiltersApply: handleFiltersApply,
964
- onFiltersClear: handleFiltersClear,
965
- pagination: {
966
- page,
967
- pageSize,
968
- total,
969
- totalPages,
970
- onPageChange: (nextPage) => setPage(nextPage),
971
- pageSizeOptions: [10, 25, 50, 100],
972
- onPageSizeChange: handlePageSizeChange,
973
- cacheStatus
974
- },
975
- isLoading,
976
- refreshButton: {
977
- label: t("customers.deals.list.refresh"),
978
- onRefresh: handleRefresh
979
- },
980
- exporter: exportConfig,
981
- entityId: E.customers.customer_deal,
982
- perspective: { tableId: "customers.deals.list" },
983
- advancedFilter: {
984
- auto: true,
985
- value: advancedFilterState,
986
- onChange: setAdvancedFilterState,
987
- onApply: () => {
988
- setPage(1);
814
+ ],
815
+ searchValue: search,
816
+ onSearchChange: handleSearchChange,
817
+ searchPlaceholder: t("customers.deals.list.searchPlaceholder"),
818
+ pagination: {
819
+ page,
820
+ pageSize,
821
+ total,
822
+ totalPages,
823
+ onPageChange: (nextPage) => setPage(nextPage),
824
+ pageSizeOptions: [10, 25, 50, 100],
825
+ onPageSizeChange: handlePageSizeChange,
826
+ cacheStatus
989
827
  },
990
- onClear: () => {
991
- setAdvancedFilterState({ logic: "and", conditions: [] });
992
- setPage(1);
993
- }
994
- },
995
- virtualized: true
996
- }
997
- ) }),
828
+ isLoading,
829
+ refreshButton: {
830
+ label: t("customers.deals.list.refresh"),
831
+ onRefresh: handleRefresh
832
+ },
833
+ exporter: exportConfig,
834
+ entityId: E.customers.customer_deal,
835
+ perspective: { tableId: "customers.deals.list" },
836
+ advancedFilter: {
837
+ auto: true,
838
+ value: filterPanel.tree,
839
+ onChange: filterPanel.setTree,
840
+ onApply: () => filterPanel.flush(),
841
+ onClear: handleAdvancedFilterClear,
842
+ triggerRef: filtersTriggerRef,
843
+ externalPopover: true,
844
+ onTriggerClick: () => setFiltersOpen((prev) => !prev),
845
+ onApplyTree: (tree) => {
846
+ filterPanel.replaceTree(tree);
847
+ setPage(1);
848
+ }
849
+ },
850
+ activeFilterChips: /* @__PURE__ */ jsxs(Fragment, { children: [
851
+ /* @__PURE__ */ jsx(
852
+ ActiveFilterChips,
853
+ {
854
+ tree: associationFilterTree,
855
+ fields: associationFilterFields,
856
+ popoverOpen: filtersOpen,
857
+ onRemoveNode: handleAssociationFilterRemove,
858
+ onOpen: () => setFiltersOpen(true)
859
+ }
860
+ ),
861
+ /* @__PURE__ */ jsx(
862
+ ActiveFilterChips,
863
+ {
864
+ tree: filterPanel.tree,
865
+ fields: advancedFilterFields,
866
+ popoverOpen: filtersOpen,
867
+ onRemoveNode: (id) => filterPanel.dispatch({ type: "removeNode", nodeId: id }),
868
+ onOpen: () => setFiltersOpen(true)
869
+ }
870
+ )
871
+ ] }),
872
+ filterAwareEmptyState: {
873
+ active: advancedFilterState.root.children.length > 0,
874
+ entityNamePlural: t("customers.deals.entityPlural", "deals"),
875
+ canRemoveLast: filterPanel.tree.root.children.length > 0,
876
+ onClearAll: handleAdvancedFilterClear,
877
+ onRemoveLast: () => filterPanel.dispatch({ type: "removeLast" })
878
+ },
879
+ virtualized: true
880
+ }
881
+ ),
882
+ /* @__PURE__ */ jsx(
883
+ AdvancedFilterPanel,
884
+ {
885
+ fields: advancedFilterFields,
886
+ value: filterPanel.tree,
887
+ onChange: filterPanel.setTree,
888
+ onApply: filterPanel.flush,
889
+ onClear: handleAdvancedFilterClear,
890
+ onFlush: filterPanel.flush,
891
+ pendingErrors: filterPanel.pendingErrors,
892
+ userId: currentUserId,
893
+ presets: dealsPresets,
894
+ open: filtersOpen,
895
+ onOpenChange: setFiltersOpen,
896
+ triggerRef: filtersTriggerRef,
897
+ savedFilterStorageKey: "customers.deals.list"
898
+ }
899
+ )
900
+ ] }),
998
901
  ConfirmDialogElement
999
902
  ] });
1000
903
  }
@@ -1013,6 +916,7 @@ function mapDeal(item) {
1013
916
  const probability = typeof probabilityRaw === "number" ? probabilityRaw : typeof probabilityRaw === "string" && probabilityRaw.trim().length ? Number(probabilityRaw) : null;
1014
917
  const expectedCloseAt = typeof item.expected_close_at === "string" ? item.expected_close_at : null;
1015
918
  const updatedAt = typeof item.updated_at === "string" ? item.updated_at : null;
919
+ const ownerUserId = typeof item.owner_user_id === "string" ? item.owner_user_id : null;
1016
920
  const peopleRaw = Array.isArray(item.people) ? item.people : [];
1017
921
  const companiesRaw = Array.isArray(item.companies) ? item.companies : [];
1018
922
  const people = peopleRaw.map((entry) => {
@@ -1047,6 +951,7 @@ function mapDeal(item) {
1047
951
  probability,
1048
952
  expectedCloseAt,
1049
953
  updatedAt,
954
+ ownerUserId,
1050
955
  people,
1051
956
  companies,
1052
957
  ...customFields