@aglyn/plugins-crm 1.0.0-beta.246 → 1.0.0-beta.249
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/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/contact-custom-columns.d.ts +15 -6
- package/src/lib/components/contact-custom-columns.js +27 -7
- package/src/lib/components/contact-custom-columns.js.map +1 -1
- package/src/lib/components/contact-field-drawer.d.ts +2 -0
- package/src/lib/components/contact-field-drawer.js +25 -1
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-custom-field-control.d.ts +4 -2
- package/src/lib/components/crm-custom-field-control.js +53 -1
- package/src/lib/components/crm-custom-field-control.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -2
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +2 -0
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +7 -0
- package/src/lib/constants/contact-filters.js +8 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +2 -0
- package/src/lib/declarations.console-server.js +5 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/crm-picklist-setup-content.d.ts +121 -0
- package/src/lib/model/crm-picklist-setup-content.js +262 -0
- package/src/lib/model/crm-picklist-setup-content.js.map +1 -0
- package/src/lib/model/crm-record-routes.js +6 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-setup-content.d.ts +4 -1
- package/src/lib/model/crm-setup-content.js +15 -4
- package/src/lib/model/crm-setup-content.js.map +1 -1
- package/src/lib/server/crm-setup-ai-capabilities.d.ts +13 -0
- package/src/lib/server/crm-setup-ai-capabilities.js +94 -2
- package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -1
- package/src/lib/server/crm-setup-writers.server.d.ts +28 -0
- package/src/lib/server/crm-setup-writers.server.js +181 -4
- package/src/lib/server/crm-setup-writers.server.js.map +1 -1
- package/src/lib/server/picklist-regroup.d.ts +30 -0
- package/src/lib/server/picklist-regroup.js +61 -0
- package/src/lib/server/picklist-regroup.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +2 -14
- package/src/lib/server/picklist-values.js +3 -40
- package/src/lib/server/picklist-values.js.map +1 -1
- package/src/lib/transfer/common.d.ts +4 -2
- package/src/lib/transfer/common.js +14 -3
- package/src/lib/transfer/common.js.map +1 -1
- package/src/lib/transfer/fields.js +10 -1
- package/src/lib/transfer/fields.js.map +1 -1
- package/src/lib/transfer/pipelines.js +4 -2
- package/src/lib/transfer/pipelines.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n CRM_FACET_KEY_ANY_GROUP,\n type CrmFacetKeyField,\n crmContactCustomColumn,\n crmFacetKey,\n crmFacetKeyValue,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { EMAIL_STATE_STATUSES } from '@aglyn/aglyn/app-utils/email-state'\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n} from '../model/crm-list-query'\n\n/*==========================================\n * CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE\n * PANEL OFFERS, AND WHAT THE QUERY ASKS (AGL-3321).\n *\n * Every clause and the search word are on the list's one Firestore query,\n * newest change first, paged by it. A contact is one row shared by every\n * holder, and most of what a holder filters by — the owner, the stage, the\n * sources, the company, the tags, a custom value, whether the person has\n * bought — lives on that holder's FACET, a path per holder no index can\n * reach. So every writer stamps `facetKeys` (`crmContactFacetKeys` in\n * `@aglyn/aglyn`): `{group}:{field}={value}` for each holder's value,\n * `{group}:{field}` for \"has one\", and the same under `*` for any holder.\n * Under a site a facet clause asks the viewing holder's key; at the\n * organization level, any holder's.\n *\n * What one query holds decides the rest:\n *\n * - ONE array clause. The scope clause (`visibleTo array-contains-any`),\n * the search (`searchTokens`), a name word (`nameTokens`), a form\n * (`formIds`) and a facet clause (`facetKeys`) all are one. The search\n * folds into the scope clause for a reader who may drop it; a facet or\n * form clause stands in the scope clause's place for that reader, since\n * only a record the viewing holder keeps carries its key. Two of them\n * at once is refused by name.\n * - ONE range. `updatedAt`, the field the list is ordered by, composes\n * with anything; Created and a name or address prefix order the list by\n * their own field and so stand alone beside the scope clause\n * (`contactSoloClause`), served by the `(visibleTo, field)` composites.\n * - equalities (the address, the site, the verdict, \"No next activity\")\n * compose freely, one `(field, updatedAt DESC)` composite each.\n *\n * The per-holder FIGURES (orders, lifetime value) live on each holder's\n * facet, where no range reaches — a top-level `ordersCount` was never\n * written — so they offer \"is not empty\" alone: \"has bought\".\n *\n * A collaborator's search is the start of the name (`CONTACT_PREFIX_SEARCH`).\n *=========================================*/\n\n/**\n * A word of the canonical name (`nameTokens`), the whole of it, or its start\n * or end — `nameLower`, which every contact carries, and `nameReversed`.\n */\nconst NAME_FIELD: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n operators: ['contains', 'equals', 'startsWith', 'endsWith'],\n}\n\n/**\n * The address. `normalizeContactEmail` lower-cases before every write, so the\n * stored value IS its own normalized key.\n */\nconst EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals', 'startsWith'],\n}\n\n/** When the person was first captured — a range the list is then ordered by. */\nconst CREATED_FIELD: ListFilterField = {\n column: CRM_CONTACT_VIEW_FIELDS.createdAt,\n kind: 'date',\n path: 'createdAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/**\n * The forms a person came in through — the top-level `formIds` mirror of the\n * interactions' `formId` (AGL-2612), matched whole and as typed because a\n * form id is minted with mixed case. The form's own page links here with its\n * id; nobody types one.\n */\nconst FORM_FIELD: ListFilterField = {\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n operators: ['contains'],\n}\n\n/** The column a contact's lead source filters as (AGL-3511). */\nexport const CONTACT_LEAD_SOURCE_COLUMN = 'leadSource'\n\n/** What the Filters panel offers on a contact, in the grid's grammar. */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n {\n // A holder's tags, matched whole and case-insensitively.\n column: CRM_CONTACT_VIEW_FIELDS.tags,\n kind: 'exact',\n path: 'tags',\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n FORM_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n // Per-holder figures: \"has bought\", never a range across holders.\n {\n column: CRM_CONTACT_VIEW_FIELDS.orders,\n kind: 'number',\n path: 'ordersCount',\n operators: ['isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.ltv,\n kind: 'number',\n path: 'ltvCents',\n operators: ['isNotEmpty'],\n },\n CREATED_FIELD,\n {\n column: CRM_CONTACT_VIEW_FIELDS.updatedAt,\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n /*\n * \"No next activity\" (AGL-2661): `nextTaskAtMs == null`, which every\n * contact carries from its creation until a task is scheduled against it.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n /*\n * THE FACET FIELDS (AGL-2617). The values are picked, not typed — a uid\n * from the roster, a stage from the fixed list, a source from its labels,\n * a company from the picker — so `equals` and `isAnyOf` are what they\n * offer; the section supplies the choices. The column names are the\n * contract `CRM_CONTACT_VIEW_FIELDS` states, because the dynamic-list\n * translator reads a view by them. The paths are the flattened ROW'S.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * A holder's lead source (AGL-3511), picked from the org's Lead source\n * values and asked through its `leadSource` facet key — the label as the\n * picklist compares it.\n */\n {\n column: CONTACT_LEAD_SOURCE_COLUMN,\n kind: 'exact',\n path: 'leadSource',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245), asked through `emailStatus` —\n * `none` for a person nothing has been said about, which is what \"is\n * empty\" asks.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617). A custom\n * value lives under the holder's `custom` map, so it is asked through the\n * holder's facet key: equality on a picked, typed or ticked value, and\n * \"is not empty\". Its column is the one the table shows it under —\n * `crmContactCustomColumn(key)` — so a clause and a column agree on the name.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<\n ContactFieldDefinition,\n 'key' | 'type' | 'retiredAt'\n >[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition): ListFilterField => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n return {\n column,\n kind: 'number',\n path,\n operators: ['=', 'isNotEmpty'],\n }\n case 'date':\n return { column, kind: 'date', path, operators: ['isNotEmpty'] }\n case 'checkbox':\n return { column, kind: 'boolean', path }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n lowerPath: path,\n operators: ['equals', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'Lead source',\n emailState: 'Email',\n}\n\n/*------------------------------------------\n * THE QUERY THE CONTACTS LIST RUNS.\n *-----------------------------------------*/\n\n/**\n * The list's orders: newest change first, then each header the query can\n * order by (AGL-3680) — the person's name or address (`nameSortKey`), the\n * Last activity column's `updatedAt` the other way, the next activity and\n * the address verdict, every one stored on every contact. Each is `alone`:\n * served with no filter or search on, beside the scope clause by one\n * `(visibleTo, field)` composite per direction. The other columns — a\n * holder's owner, stage, sources, tags, last engagement and custom values,\n * which live on a facet, and the sites that know the person — sort the page.\n */\nexport const CONTACT_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt', label: 'Last activity' },\n { path: 'updatedAt', direction: 'asc', column: 'updatedAt', label: 'Last activity', alone: true },\n { path: 'nameSortKey', direction: 'asc', column: 'name', label: 'Contact', alone: true },\n { path: 'nameSortKey', direction: 'desc', column: 'name', label: 'Contact', alone: true },\n { path: 'nextTaskAtMs', direction: 'asc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n { path: 'nextTaskAtMs', direction: 'desc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n { path: 'emailStatus', direction: 'asc', column: 'emailState', label: 'Email', alone: true },\n { path: 'emailStatus', direction: 'desc', column: 'emailState', label: 'Email', alone: true },\n]\n\n/** The stored array a facet clause is asked of. */\nexport const CONTACT_FACET_KEYS_PATH = 'facetKeys'\n\n/** The fields the Contacts query asks — each a stored field, as named. */\nexport const CONTACT_QUERY_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n FORM_FIELD,\n CREATED_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n {\n column: 'emailStatus',\n kind: 'exact',\n path: 'emailStatus',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CONTACT_FACET_KEYS_PATH,\n kind: 'exact',\n path: CONTACT_FACET_KEYS_PATH,\n tokensPath: CONTACT_FACET_KEYS_PATH,\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const CONTACT_LIST_DECLARATION: ListQueryDeclaration = {\n fields: CONTACT_QUERY_FIELDS,\n sorts: CONTACT_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The grid fields a stored clause passes to the query unchanged. */\nconst AS_STORED = new Set([\n 'name',\n 'email',\n 'formIds',\n 'hostId',\n 'createdAt',\n 'updatedAt',\n 'nextTaskAtMs',\n])\n\n/**\n * The ranges that stand ALONE beside the scope clause: Created and a name\n * or address prefix each order the list by their own field, which the\n * `(visibleTo, field)` composites serve and nothing beside them. Updated is\n * not one: the list is ordered by it already, so it composes freely.\n */\nexport function contactSoloClause(clause: ListFilterRequest): boolean {\n if (clause.field === 'createdAt') return true\n if (clause.field === 'name') return clause.op === 'startsWith' || clause.op === 'endsWith'\n return clause.field === 'email' && clause.op === 'startsWith'\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the contact's name, beside the scope clause.\n */\nexport const CONTACT_PREFIX_SEARCH = {\n field: NAME_FIELD,\n notice: 'Search matches the start of a contact’s name for access limited to specific sites.',\n}\n\n/** The facet field each grid column is keyed under. */\nconst FACET_FIELDS: Readonly<Record<string, CrmFacetKeyField>> = {\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'company',\n [CRM_CONTACT_VIEW_FIELDS.tags]: 'tag',\n [CRM_CONTACT_VIEW_FIELDS.orders]: 'orders',\n [CRM_CONTACT_VIEW_FIELDS.ltv]: 'ltv',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'leadSource',\n}\n\n/** Who is asking: the viewing holder, and whether their query may drop the scope clause. */\nexport interface ContactQueryReader {\n /** The viewing holder's group id under a site; `null` at the organization level. */\n groupId: string | null\n /**\n * Whether the reader's query may stand a facet or form clause in the\n * scope clause's place (`useCrmFoldsScope`) — always at the organization\n * level, where there is no scope clause.\n */\n foldsScope: boolean\n}\n\n/** Why a reader whose access is some sites cannot filter by a holder's field. */\nconst LIMITED_ACCESS =\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'\n\n/** The facet field a grid column names, or null for one that is not a facet field. */\nfunction facetFieldOf(column: string): CrmFacetKeyField | null {\n if (FACET_FIELDS[column]) return FACET_FIELDS[column]\n if (column.startsWith(CRM_CONTACT_VIEW_FIELDS.custom)) {\n return `custom.${column.slice(CRM_CONTACT_VIEW_FIELDS.custom.length)}`\n }\n return null\n}\n\n/**\n * One grid clause as the clause the Contacts query asks — see the block at\n * the top. A clause a query cannot serve for this reader is refused by\n * name, never matched over the rows a page loaded.\n */\nexport function contactQueryClause(\n clause: ListFilterRequest,\n reader: ContactQueryReader,\n): CrmClauseAsked {\n if (AS_STORED.has(clause.field)) {\n if (clause.field === 'formIds' && !reader.foldsScope)\n return { refused: LIMITED_ACCESS }\n return { field: clause.field, op: clause.op, value: clause.value }\n }\n if (clause.field === 'emailState') {\n if (clause.op === 'isEmpty')\n return { field: 'emailStatus', op: 'equals', value: 'none' }\n if (clause.op === 'isNotEmpty')\n return crmAnyOf('emailStatus', EMAIL_STATE_STATUSES)\n const values = crmClauseValues(clause)\n if (\n values.some(\n (value) => !(EMAIL_STATE_STATUSES as readonly string[]).includes(value),\n )\n ) {\n return { refused: 'pick one of the choices' }\n }\n return crmAnyOf('emailStatus', values)\n }\n const facet = facetFieldOf(clause.field)\n if (!facet) return { refused: 'this list does not filter by that' }\n if (!reader.foldsScope) return { refused: LIMITED_ACCESS }\n const group = reader.groupId ?? CRM_FACET_KEY_ANY_GROUP\n if (clause.op === 'isNotEmpty') {\n return {\n field: CONTACT_FACET_KEYS_PATH,\n op: 'contains',\n value: crmFacetKey(group, facet),\n }\n }\n const keys: string[] = []\n for (const value of crmClauseValues(clause)) {\n const key = crmFacetKeyValue(clause.op === '=' ? Number(value) : value)\n if (key === null) return { refused: 'no value yet' }\n keys.push(crmFacetKey(group, facet, key))\n }\n const asked = crmAnyOf(CONTACT_FACET_KEYS_PATH, keys)\n // One key is an `array-contains`; several, an `array-contains-any`.\n return 'refused' in asked || asked.op === 'isAnyOf'\n ? asked\n : { ...asked, op: 'contains' }\n}\n\n/**\n * Whether an asked clause stands in for the scope clause: a facet key of\n * the viewing holder, which only a record that holder keeps carries, or a\n * form, which only its site's captures carry.\n */\nexport function contactClauseImpliesScope(groupId: string | null) {\n return (clause: ListFilterRequest): boolean => {\n if (clause.field === 'formIds') return true\n if (clause.field !== CONTACT_FACET_KEYS_PATH || !groupId) return false\n return crmClauseValues(clause).every((key) => key.startsWith(`${groupId}:`))\n }\n}\n\n/**\n * The query shapes the Contacts list can send, as declarations the index\n * pin enumerates (AGL-3321): the list's own, without its solo ranges, whose\n * every equality rides `(field, updatedAt DESC)`; and each solo range beside\n * the scope clause alone.\n */\nexport function contactIndexShapes(): ListQueryDeclaration[] {\n const soloOps = new Set(['startsWith', 'endsWith'])\n const main: ListQueryDeclaration = {\n ...CONTACT_LIST_DECLARATION,\n fields: CONTACT_QUERY_FIELDS.filter((field) => field !== CREATED_FIELD).map((field) =>\n field.operators\n ? { ...field, operators: field.operators.filter((op) => !soloOps.has(op)) }\n : field,\n ),\n }\n const solo: ListQueryDeclaration = {\n fields: [\n { ...NAME_FIELD, operators: ['startsWith', 'endsWith'] },\n { ...EMAIL_FIELD, operators: ['startsWith'] },\n CREATED_FIELD,\n ],\n sorts: [],\n }\n return [main, solo]\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","CRM_FACET_KEY_ANY_GROUP","crmContactCustomColumn","crmFacetKey","crmFacetKeyValue","EMAIL_STATE_STATUSES","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","NAME_FIELD","column","kind","path","lowerPath","tokensPath","reversedPath","operators","EMAIL_FIELD","presence","CREATED_FIELD","createdAt","FORM_FIELD","verbatimTokens","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_FILTER_FIELDS","tags","orders","ltv","updatedAt","storedAs","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","formIds","hostId","ordersCount","ltvCents","nextTaskAtMs","emailState","CONTACT_LIST_SORTS","direction","alone","CONTACT_FACET_KEYS_PATH","CONTACT_QUERY_FIELDS","CONTACT_LIST_DECLARATION","fields","sorts","search","AS_STORED","Set","contactSoloClause","clause","field","op","CONTACT_PREFIX_SEARCH","notice","FACET_FIELDS","LIMITED_ACCESS","facetFieldOf","startsWith","custom","slice","length","contactQueryClause","reader","has","foldsScope","refused","value","values","some","includes","facet","group","groupId","keys","Number","push","asked","contactClauseImpliesScope","every","contactIndexShapes","soloOps","main","solo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n CRM_FACET_KEY_ANY_GROUP,\n type CrmFacetKeyField,\n crmContactCustomColumn,\n crmFacetKey,\n crmFacetKeyValue,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { EMAIL_STATE_STATUSES } from '@aglyn/aglyn/app-utils/email-state'\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n} from '../model/crm-list-query'\n\n/*==========================================\n * CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE\n * PANEL OFFERS, AND WHAT THE QUERY ASKS (AGL-3321).\n *\n * Every clause and the search word are on the list's one Firestore query,\n * newest change first, paged by it. A contact is one row shared by every\n * holder, and most of what a holder filters by — the owner, the stage, the\n * sources, the company, the tags, a custom value, whether the person has\n * bought — lives on that holder's FACET, a path per holder no index can\n * reach. So every writer stamps `facetKeys` (`crmContactFacetKeys` in\n * `@aglyn/aglyn`): `{group}:{field}={value}` for each holder's value,\n * `{group}:{field}` for \"has one\", and the same under `*` for any holder.\n * Under a site a facet clause asks the viewing holder's key; at the\n * organization level, any holder's.\n *\n * What one query holds decides the rest:\n *\n * - ONE array clause. The scope clause (`visibleTo array-contains-any`),\n * the search (`searchTokens`), a name word (`nameTokens`), a form\n * (`formIds`) and a facet clause (`facetKeys`) all are one. The search\n * folds into the scope clause for a reader who may drop it; a facet or\n * form clause stands in the scope clause's place for that reader, since\n * only a record the viewing holder keeps carries its key. Two of them\n * at once is refused by name.\n * - ONE range. `updatedAt`, the field the list is ordered by, composes\n * with anything; Created and a name or address prefix order the list by\n * their own field and so stand alone beside the scope clause\n * (`contactSoloClause`), served by the `(visibleTo, field)` composites.\n * - equalities (the address, the site, the verdict, \"No next activity\")\n * compose freely, one `(field, updatedAt DESC)` composite each.\n *\n * The per-holder FIGURES (orders, lifetime value) live on each holder's\n * facet, where no range reaches — a top-level `ordersCount` was never\n * written — so they offer \"is not empty\" alone: \"has bought\".\n *\n * A collaborator's search is the start of the name (`CONTACT_PREFIX_SEARCH`).\n *=========================================*/\n\n/**\n * A word of the canonical name (`nameTokens`), the whole of it, or its start\n * or end — `nameLower`, which every contact carries, and `nameReversed`.\n */\nconst NAME_FIELD: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n operators: ['contains', 'equals', 'startsWith', 'endsWith'],\n}\n\n/**\n * The address. `normalizeContactEmail` lower-cases before every write, so the\n * stored value IS its own normalized key.\n */\nconst EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals', 'startsWith'],\n}\n\n/** When the person was first captured — a range the list is then ordered by. */\nconst CREATED_FIELD: ListFilterField = {\n column: CRM_CONTACT_VIEW_FIELDS.createdAt,\n kind: 'date',\n path: 'createdAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/**\n * The forms a person came in through — the top-level `formIds` mirror of the\n * interactions' `formId` (AGL-2612), matched whole and as typed because a\n * form id is minted with mixed case. The form's own page links here with its\n * id; nobody types one.\n */\nconst FORM_FIELD: ListFilterField = {\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n operators: ['contains'],\n}\n\n/** The column a contact's lead source filters as (AGL-3511). */\nexport const CONTACT_LEAD_SOURCE_COLUMN = 'leadSource'\n\n/** What the Filters panel offers on a contact, in the grid's grammar. */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n {\n // A holder's tags, matched whole and case-insensitively.\n column: CRM_CONTACT_VIEW_FIELDS.tags,\n kind: 'exact',\n path: 'tags',\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n FORM_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n // Per-holder figures: \"has bought\", never a range across holders.\n {\n column: CRM_CONTACT_VIEW_FIELDS.orders,\n kind: 'number',\n path: 'ordersCount',\n operators: ['isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.ltv,\n kind: 'number',\n path: 'ltvCents',\n operators: ['isNotEmpty'],\n },\n CREATED_FIELD,\n {\n column: CRM_CONTACT_VIEW_FIELDS.updatedAt,\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n /*\n * \"No next activity\" (AGL-2661): `nextTaskAtMs == null`, which every\n * contact carries from its creation until a task is scheduled against it.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n /*\n * THE FACET FIELDS (AGL-2617). The values are picked, not typed — a uid\n * from the roster, a stage from the fixed list, a source from its labels,\n * a company from the picker — so `equals` and `isAnyOf` are what they\n * offer; the section supplies the choices. The column names are the\n * contract `CRM_CONTACT_VIEW_FIELDS` states, because the dynamic-list\n * translator reads a view by them. The paths are the flattened ROW'S.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * A holder's lead source (AGL-3511), picked from the org's Lead source\n * values and asked through its `leadSource` facet key — the label as the\n * picklist compares it.\n */\n {\n column: CONTACT_LEAD_SOURCE_COLUMN,\n kind: 'exact',\n path: 'leadSource',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245), asked through `emailStatus` —\n * `none` for a person nothing has been said about, which is what \"is\n * empty\" asks.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617). A custom\n * value lives under the holder's `custom` map, so it is asked through the\n * holder's facet key: equality on a picked, typed or ticked value, and\n * \"is not empty\". Its column is the one the table shows it under —\n * `crmContactCustomColumn(key)` — so a clause and a column agree on the name.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<\n ContactFieldDefinition,\n 'key' | 'type' | 'retiredAt'\n >[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition): ListFilterField => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n // An amount is typed in major units and asked as the minor units it\n // is stored in — `ContactQueryReader.currencies` (AGL-3756).\n case 'currency':\n return {\n column,\n kind: 'number',\n path,\n operators: ['=', 'isNotEmpty'],\n }\n case 'date':\n return { column, kind: 'date', path, operators: ['isNotEmpty'] }\n case 'checkbox':\n return { column, kind: 'boolean', path }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n lowerPath: path,\n operators: ['equals', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'Lead source',\n emailState: 'Email',\n}\n\n/*------------------------------------------\n * THE QUERY THE CONTACTS LIST RUNS.\n *-----------------------------------------*/\n\n/**\n * The list's orders: newest change first, then each header the query can\n * order by (AGL-3680) — the person's name or address (`nameSortKey`), the\n * Last activity column's `updatedAt` the other way, the next activity and\n * the address verdict, every one stored on every contact. Each is `alone`:\n * served with no filter or search on, beside the scope clause by one\n * `(visibleTo, field)` composite per direction. The other columns — a\n * holder's owner, stage, sources, tags, last engagement and custom values,\n * which live on a facet, and the sites that know the person — sort the page.\n */\nexport const CONTACT_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt', label: 'Last activity' },\n { path: 'updatedAt', direction: 'asc', column: 'updatedAt', label: 'Last activity', alone: true },\n { path: 'nameSortKey', direction: 'asc', column: 'name', label: 'Contact', alone: true },\n { path: 'nameSortKey', direction: 'desc', column: 'name', label: 'Contact', alone: true },\n { path: 'nextTaskAtMs', direction: 'asc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n { path: 'nextTaskAtMs', direction: 'desc', column: 'nextTaskAtMs', label: 'Next activity', alone: true },\n { path: 'emailStatus', direction: 'asc', column: 'emailState', label: 'Email', alone: true },\n { path: 'emailStatus', direction: 'desc', column: 'emailState', label: 'Email', alone: true },\n]\n\n/** The stored array a facet clause is asked of. */\nexport const CONTACT_FACET_KEYS_PATH = 'facetKeys'\n\n/** The fields the Contacts query asks — each a stored field, as named. */\nexport const CONTACT_QUERY_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n FORM_FIELD,\n CREATED_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n {\n column: 'emailStatus',\n kind: 'exact',\n path: 'emailStatus',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CONTACT_FACET_KEYS_PATH,\n kind: 'exact',\n path: CONTACT_FACET_KEYS_PATH,\n tokensPath: CONTACT_FACET_KEYS_PATH,\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const CONTACT_LIST_DECLARATION: ListQueryDeclaration = {\n fields: CONTACT_QUERY_FIELDS,\n sorts: CONTACT_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The grid fields a stored clause passes to the query unchanged. */\nconst AS_STORED = new Set([\n 'name',\n 'email',\n 'formIds',\n 'hostId',\n 'createdAt',\n 'updatedAt',\n 'nextTaskAtMs',\n])\n\n/**\n * The ranges that stand ALONE beside the scope clause: Created and a name\n * or address prefix each order the list by their own field, which the\n * `(visibleTo, field)` composites serve and nothing beside them. Updated is\n * not one: the list is ordered by it already, so it composes freely.\n */\nexport function contactSoloClause(clause: ListFilterRequest): boolean {\n if (clause.field === 'createdAt') return true\n if (clause.field === 'name') return clause.op === 'startsWith' || clause.op === 'endsWith'\n return clause.field === 'email' && clause.op === 'startsWith'\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the contact's name, beside the scope clause.\n */\nexport const CONTACT_PREFIX_SEARCH = {\n field: NAME_FIELD,\n notice: 'Search matches the start of a contact’s name for access limited to specific sites.',\n}\n\n/** The facet field each grid column is keyed under. */\nconst FACET_FIELDS: Readonly<Record<string, CrmFacetKeyField>> = {\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'company',\n [CRM_CONTACT_VIEW_FIELDS.tags]: 'tag',\n [CRM_CONTACT_VIEW_FIELDS.orders]: 'orders',\n [CRM_CONTACT_VIEW_FIELDS.ltv]: 'ltv',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'leadSource',\n}\n\n/** Who is asking: the viewing holder, and whether their query may drop the scope clause. */\nexport interface ContactQueryReader {\n /** The viewing holder's group id under a site; `null` at the organization level. */\n groupId: string | null\n /**\n * Whether the reader's query may stand a facet or form clause in the\n * scope clause's place (`useCrmFoldsScope`) — always at the organization\n * level, where there is no scope clause.\n */\n foldsScope: boolean\n /**\n * How each custom CURRENCY column reads a typed amount, by column\n * (AGL-3756): its `=` is typed in major units and asked as the minor units\n * the facet key holds, or `null` when it is not one amount. The web side\n * builds it (`contactCustomAmountReaders`), so this module stays pure.\n */\n amounts?: Readonly<Record<string, (typed: string) => number | null>>\n}\n\n/** Why a reader whose access is some sites cannot filter by a holder's field. */\nconst LIMITED_ACCESS =\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'\n\n/** The facet field a grid column names, or null for one that is not a facet field. */\nfunction facetFieldOf(column: string): CrmFacetKeyField | null {\n if (FACET_FIELDS[column]) return FACET_FIELDS[column]\n if (column.startsWith(CRM_CONTACT_VIEW_FIELDS.custom)) {\n return `custom.${column.slice(CRM_CONTACT_VIEW_FIELDS.custom.length)}`\n }\n return null\n}\n\n/**\n * One grid clause as the clause the Contacts query asks — see the block at\n * the top. A clause a query cannot serve for this reader is refused by\n * name, never matched over the rows a page loaded.\n */\nexport function contactQueryClause(\n clause: ListFilterRequest,\n reader: ContactQueryReader,\n): CrmClauseAsked {\n if (AS_STORED.has(clause.field)) {\n if (clause.field === 'formIds' && !reader.foldsScope)\n return { refused: LIMITED_ACCESS }\n return { field: clause.field, op: clause.op, value: clause.value }\n }\n if (clause.field === 'emailState') {\n if (clause.op === 'isEmpty')\n return { field: 'emailStatus', op: 'equals', value: 'none' }\n if (clause.op === 'isNotEmpty')\n return crmAnyOf('emailStatus', EMAIL_STATE_STATUSES)\n const values = crmClauseValues(clause)\n if (\n values.some(\n (value) => !(EMAIL_STATE_STATUSES as readonly string[]).includes(value),\n )\n ) {\n return { refused: 'pick one of the choices' }\n }\n return crmAnyOf('emailStatus', values)\n }\n const facet = facetFieldOf(clause.field)\n if (!facet) return { refused: 'this list does not filter by that' }\n if (!reader.foldsScope) return { refused: LIMITED_ACCESS }\n const group = reader.groupId ?? CRM_FACET_KEY_ANY_GROUP\n if (clause.op === 'isNotEmpty') {\n return {\n field: CONTACT_FACET_KEYS_PATH,\n op: 'contains',\n value: crmFacetKey(group, facet),\n }\n }\n const keys: string[] = []\n const amount = reader.amounts?.[clause.field]\n for (const value of crmClauseValues(clause)) {\n const asked =\n clause.op !== '='\n ? value\n : amount\n ? (amount(value) ?? Number.NaN)\n : Number(value)\n const key = crmFacetKeyValue(asked)\n if (key === null) return { refused: 'no value yet' }\n keys.push(crmFacetKey(group, facet, key))\n }\n const asked = crmAnyOf(CONTACT_FACET_KEYS_PATH, keys)\n // One key is an `array-contains`; several, an `array-contains-any`.\n return 'refused' in asked || asked.op === 'isAnyOf'\n ? asked\n : { ...asked, op: 'contains' }\n}\n\n/**\n * Whether an asked clause stands in for the scope clause: a facet key of\n * the viewing holder, which only a record that holder keeps carries, or a\n * form, which only its site's captures carry.\n */\nexport function contactClauseImpliesScope(groupId: string | null) {\n return (clause: ListFilterRequest): boolean => {\n if (clause.field === 'formIds') return true\n if (clause.field !== CONTACT_FACET_KEYS_PATH || !groupId) return false\n return crmClauseValues(clause).every((key) => key.startsWith(`${groupId}:`))\n }\n}\n\n/**\n * The query shapes the Contacts list can send, as declarations the index\n * pin enumerates (AGL-3321): the list's own, without its solo ranges, whose\n * every equality rides `(field, updatedAt DESC)`; and each solo range beside\n * the scope clause alone.\n */\nexport function contactIndexShapes(): ListQueryDeclaration[] {\n const soloOps = new Set(['startsWith', 'endsWith'])\n const main: ListQueryDeclaration = {\n ...CONTACT_LIST_DECLARATION,\n fields: CONTACT_QUERY_FIELDS.filter((field) => field !== CREATED_FIELD).map((field) =>\n field.operators\n ? { ...field, operators: field.operators.filter((op) => !soloOps.has(op)) }\n : field,\n ),\n }\n const solo: ListQueryDeclaration = {\n fields: [\n { ...NAME_FIELD, operators: ['startsWith', 'endsWith'] },\n { ...EMAIL_FIELD, operators: ['startsWith'] },\n CREATED_FIELD,\n ],\n sorts: [],\n }\n return [main, solo]\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","CRM_FACET_KEY_ANY_GROUP","crmContactCustomColumn","crmFacetKey","crmFacetKeyValue","EMAIL_STATE_STATUSES","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","NAME_FIELD","column","kind","path","lowerPath","tokensPath","reversedPath","operators","EMAIL_FIELD","presence","CREATED_FIELD","createdAt","FORM_FIELD","verbatimTokens","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_FILTER_FIELDS","tags","orders","ltv","updatedAt","storedAs","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","formIds","hostId","ordersCount","ltvCents","nextTaskAtMs","emailState","CONTACT_LIST_SORTS","direction","alone","CONTACT_FACET_KEYS_PATH","CONTACT_QUERY_FIELDS","CONTACT_LIST_DECLARATION","fields","sorts","search","AS_STORED","Set","contactSoloClause","clause","field","op","CONTACT_PREFIX_SEARCH","notice","FACET_FIELDS","LIMITED_ACCESS","facetFieldOf","startsWith","custom","slice","length","contactQueryClause","reader","has","foldsScope","refused","value","values","some","includes","facet","group","groupId","keys","amount","amounts","asked","Number","NaN","push","contactClauseImpliesScope","every","contactIndexShapes","soloOps","main","solo"],"mappings":"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"}
|
|
@@ -19,6 +19,8 @@ import { type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plug
|
|
|
19
19
|
export declare const lazyCrmPipelineSetupWriter: PluginResourceDraftWriter;
|
|
20
20
|
/** The `crm-field` writer as boot registers it. */
|
|
21
21
|
export declare const lazyCrmFieldSetupWriter: PluginResourceDraftWriter;
|
|
22
|
+
/** The `crm-picklist-values` writer as boot registers it (AGL-3756). */
|
|
23
|
+
export declare const lazyCrmPicklistValuesSetupWriter: PluginResourceDraftWriter;
|
|
22
24
|
/**
|
|
23
25
|
* The CRM's CONSOLE-ONLY server declarations, named under
|
|
24
26
|
* `consoleServerDeclarations` in `plugins.config.json` and run at the
|
|
@@ -22,6 +22,7 @@ import { registerPluginTransferResource } from "@aglyn/aglyn/plugin-manager/plug
|
|
|
22
22
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
23
23
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
24
24
|
import { checkCrmFieldSetup, checkCrmPipelineSetup, CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE } from "./model/crm-setup-content.js";
|
|
25
|
+
import { checkCrmPicklistValuesSetup, CRM_PICKLIST_VALUES_SETUP_RESOURCE } from "./model/crm-picklist-setup-content.js";
|
|
25
26
|
import { registerCrmSetupAiCapabilities } from "./server/crm-setup-ai-capabilities.js";
|
|
26
27
|
import { crmTransferPlanGate, registerCrmTransferResources } from "./transfer/register.js";
|
|
27
28
|
import { CRM_EMAIL_TEMPLATE_PACKAGE_RULES, CRM_EMAIL_TEMPLATES_TRANSFER_KEY, crmEmailTemplateDependencies, remapCrmEmailTemplateIds } from "./transfer/email-templates-package.js";
|
|
@@ -46,6 +47,7 @@ let setupLoading = null;
|
|
|
46
47
|
}
|
|
47
48
|
/** The `crm-pipeline` writer as boot registers it. */ export const lazyCrmPipelineSetupWriter = lazySetupWriter(checkCrmPipelineSetup, (module)=>module.crmPipelineSetupWriter);
|
|
48
49
|
/** The `crm-field` writer as boot registers it. */ export const lazyCrmFieldSetupWriter = lazySetupWriter(checkCrmFieldSetup, (module)=>module.crmFieldSetupWriter);
|
|
50
|
+
/** The `crm-picklist-values` writer as boot registers it (AGL-3756). */ export const lazyCrmPicklistValuesSetupWriter = lazySetupWriter(checkCrmPicklistValuesSetup, (module)=>module.crmPicklistValuesSetupWriter);
|
|
49
51
|
/** The email templates package's server half (AGL-3535), loaded when an import or export first asks. */ const templatesPackage = async ()=>(await import("./transfer/email-templates-package.server.js")).createCrmEmailTemplatesPackage();
|
|
50
52
|
/**
|
|
51
53
|
* The plan feature every CRM resource of `/v1` needs, and the sentence its
|
|
@@ -168,6 +170,9 @@ let setupLoading = null;
|
|
|
168
170
|
registerPluginResourceDraftWriter(CRM_FIELD_SETUP_RESOURCE, lazyCrmFieldSetupWriter, {
|
|
169
171
|
pluginId: BUNDLE_ID
|
|
170
172
|
});
|
|
173
|
+
registerPluginResourceDraftWriter(CRM_PICKLIST_VALUES_SETUP_RESOURCE, lazyCrmPicklistValuesSetupWriter, {
|
|
174
|
+
pluginId: BUNDLE_ID
|
|
175
|
+
});
|
|
171
176
|
registerCrmSetupAiCapabilities();
|
|
172
177
|
// What the CRM imports and exports (AGL-3527): each resource's module
|
|
173
178
|
// loads with the first transfer that asks for it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the data layer's barrel: boot needs one\n// registry, not the whole server surface.\nimport {\n type ApiV1ResourceHandler,\n registerApiV1Resource,\n registerApiV1UsageFigures,\n} from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport {\n listPluginMembershipDetachers,\n registerPluginMembershipDetacher,\n} from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { registerPluginPersonRecordsEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n registerPluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n checkCrmFieldSetup,\n checkCrmPipelineSetup,\n CRM_FIELD_SETUP_RESOURCE,\n CRM_PIPELINE_SETUP_RESOURCE,\n} from './model/crm-setup-content'\nimport { registerCrmSetupAiCapabilities } from './server/crm-setup-ai-capabilities'\nimport { crmTransferPlanGate, registerCrmTransferResources } from './transfer/register'\nimport {\n CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n crmEmailTemplateDependencies,\n remapCrmEmailTemplateIds,\n type CrmEmailTemplatePackageContent,\n} from './transfer/email-templates-package'\n\ntype CrmSetupWriters = typeof import('./server/crm-setup-writers.server')\nlet setupLoading: Promise<CrmSetupWriters> | null = null\n\n/** The CRM's setup writers over the Admin SDK (AGL-3746), loaded on their first read or write. */\nfunction setupWriters(): Promise<CrmSetupWriters> {\n setupLoading ??= import('./server/crm-setup-writers.server').catch((error: unknown) => {\n setupLoading = null\n throw error\n })\n return setupLoading\n}\n\n/**\n * A setup writer as boot registers it: the check is pure and answers at once;\n * the hooks that read or write load the Admin SDK half on first use.\n */\nfunction lazySetupWriter(\n check: (content: Readonly<Record<string, unknown>>) => ReturnType<PluginResourceDraftWriter['check']>,\n writer: (module: CrmSetupWriters) => PluginResourceDraftWriter,\n): PluginResourceDraftWriter {\n return {\n check: (content) => check(content),\n refusal: async (context) => writer(await setupWriters()).refusal(context),\n read: async (context) => writer(await setupWriters()).read(context),\n write: async (request) => writer(await setupWriters()).write(request),\n }\n}\n\n/** The `crm-pipeline` writer as boot registers it. */\nexport const lazyCrmPipelineSetupWriter = lazySetupWriter(\n checkCrmPipelineSetup,\n (module) => module.crmPipelineSetupWriter,\n)\n\n/** The `crm-field` writer as boot registers it. */\nexport const lazyCrmFieldSetupWriter = lazySetupWriter(\n checkCrmFieldSetup,\n (module) => module.crmFieldSetupWriter,\n)\n\n/** The email templates package's server half (AGL-3535), loaded when an import or export first asks. */\nconst templatesPackage = async () =>\n (await import('./transfer/email-templates-package.server')).createCrmEmailTemplatesPackage()\n\n/**\n * The plan feature every CRM resource of `/v1` needs, and the sentence its\n * refusal says (AGL-2611, AGL-2851): the whole CRM is included from\n * Starter, so a Free workspace reads and changes none of it over the API. A\n * capture still records people on every plan; working them is the CRM.\n */\nconst CRM_SUITE = {\n feature: 'crm',\n message:\n 'The CRM — contacts, leads, companies, deals, tasks, pipelines, ' +\n 'activities and email templates — is not included in this organization’s plan',\n} as const\n\n/**\n * The CRM's resources on the customer REST API, each handler loaded with the\n * first request for it. In the order the API root lists them: the people\n * first, then the records a team keeps about them, then a site's leads\n * (`/v1/leads?siteId=`, a top-level path with the site as a parameter), then\n * the letters sent from a record (AGL-2658).\n */\nconst CRM_API_V1_RESOURCES: ReadonlyArray<readonly [string, ApiV1ResourceHandler]> = [\n ['contacts', async (...args) => (await import('./server/api-v1/contacts')).handleContacts(...args)],\n ['companies', async (...args) => (await import('./server/api-v1/crm-companies')).handleCompanies(...args)],\n ['pipelines', async (...args) => (await import('./server/api-v1/crm-pipelines')).handlePipelines(...args)],\n ['deals', async (...args) => (await import('./server/api-v1/crm-deals')).handleDeals(...args)],\n ['tasks', async (...args) => (await import('./server/api-v1/crm-tasks')).handleTasks(...args)],\n ['activities', async (...args) => (await import('./server/api-v1/crm-activities')).handleActivities(...args)],\n ['leads', async (...args) => (await import('./server/api-v1/crm-leads')).handleLeads(...args)],\n [\n 'email-templates',\n async (...args) => (await import('./server/api-v1/crm-email-templates')).handleEmailTemplates(...args),\n ],\n]\n\n/**\n * The CRM's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * It serves the CRM's resources of `/v1` (AGL-2606): the console's router\n * owns the pipeline in front of them — the key, the plan's API access, the\n * quota, the rate limit, the error envelope — and hands every request under\n * `/v1/contacts`, `/v1/deals` and the rest to the CRM once the key is\n * authenticated and the organization's plan carries the CRM. Each resource\n * brings its description for the API's OpenAPI document, and the CRM's\n * records band and collection sizes join `GET /v1/usage`. The API is the\n * console's alone, so the tenant runtime does not register them.\n *\n * It registers the server half of the CRM's import and export resources\n * (`transfer/register.ts`).\n *\n * It also clears a removed container off the CRM's records: a campaign is\n * deleted from the console, and its owner asks every plugin's membership\n * detacher first (`server/container-detach.ts`).\n *\n * Light at boot: each resource's module is imported with its first request,\n * the descriptions when the document is first built, and the usage reader\n * with the first usage call.\n * Registering again replaces this plugin's own entries, so a second call (a\n * hot reload, a spec) is harmless.\n */\nexport function registerCrmConsoleServerDeclarations(): void {\n // Email templates in a workspace package (AGL-3535).\n registerPluginTransferResource(\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n {\n items: async (ctx) => (await templatesPackage()).items(ctx),\n dependencies: (item) => crmEmailTemplateDependencies(item as CrmEmailTemplatePackageContent),\n remapIds: (item, idMap) => remapCrmEmailTemplateIds(item as CrmEmailTemplatePackageContent, idMap),\n readItems: async (ctx, ids) => (await templatesPackage()).readItems(ctx, ids),\n writeItems: async (ctx, items, writer) => (await templatesPackage()).writeItems(ctx, items as never, writer),\n revertItems: async (ctx, steps) => (await templatesPackage()).revertItems(ctx, steps as never),\n problems: async (ctx, write) => (await templatesPackage()).problems(ctx, write as never),\n rules: CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n planGate: crmTransferPlanGate('email templates'),\n },\n { pluginId: BUNDLE_ID },\n )\n // The record system's share of a person erasure (AGL-2623, AGL-3080): it\n // names the person's contacts before anybody erases, and erases them, their\n // satellites and their lead after everybody else. Required, and loaded with\n // the first erasure.\n registerPluginPersonRecordsEraser(\n {\n locate: async (target) => (await import('./server/person-eraser')).crmPersonEraser().locate(target),\n erase: async (request) => (await import('./server/person-eraser')).crmPersonEraser().erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n for (const [resource, handle] of CRM_API_V1_RESOURCES) {\n registerApiV1Resource(\n resource,\n {\n handle,\n entitlement: CRM_SUITE,\n describe: async () =>\n (await import('./server/api-v1/openapi')).CRM_API_V1_DESCRIPTIONS[resource],\n },\n { pluginId: BUNDLE_ID },\n )\n }\n registerApiV1UsageFigures(\n async (ctx) => (await import('./server/api-v1/usage')).crmUsageFigures(ctx),\n { pluginId: BUNDLE_ID },\n )\n // The CRM's setup an AI build asks for (AGL-3746): a pipeline and a custom\n // field, saved by these writers under the CRM's rules, and the operations\n // the build plans for them. The console runs AI jobs (AGL-3026).\n registerPluginResourceDraftWriter(CRM_PIPELINE_SETUP_RESOURCE, lazyCrmPipelineSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_FIELD_SETUP_RESOURCE, lazyCrmFieldSetupWriter, { pluginId: BUNDLE_ID })\n registerCrmSetupAiCapabilities()\n // What the CRM imports and exports (AGL-3527): each resource's module\n // loads with the first transfer that asks for it.\n registerCrmTransferResources(BUNDLE_ID)\n // A removed container — a deleted campaign — comes off every lead and every\n // contact facet naming it, before its owner removes it (AGL-3080).\n if (!listPluginMembershipDetachers().includes(BUNDLE_ID)) {\n registerPluginMembershipDetacher(\n async (request) => (await import('./server/container-detach')).crmContainerDetacher(request),\n { pluginId: BUNDLE_ID },\n )\n }\n}\n"],"names":["registerApiV1Resource","registerApiV1UsageFigures","listPluginMembershipDetachers","registerPluginMembershipDetacher","registerPluginPersonRecordsEraser","registerPluginTransferResource","registerPluginResourceDraftWriter","BUNDLE_ID","checkCrmFieldSetup","checkCrmPipelineSetup","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","registerCrmSetupAiCapabilities","crmTransferPlanGate","registerCrmTransferResources","CRM_EMAIL_TEMPLATE_PACKAGE_RULES","CRM_EMAIL_TEMPLATES_TRANSFER_KEY","crmEmailTemplateDependencies","remapCrmEmailTemplateIds","setupLoading","setupWriters","catch","error","lazySetupWriter","check","writer","content","refusal","context","read","write","request","lazyCrmPipelineSetupWriter","module","crmPipelineSetupWriter","lazyCrmFieldSetupWriter","crmFieldSetupWriter","templatesPackage","createCrmEmailTemplatesPackage","CRM_SUITE","feature","message","CRM_API_V1_RESOURCES","args","handleContacts","handleCompanies","handlePipelines","handleDeals","handleTasks","handleActivities","handleLeads","handleEmailTemplates","registerCrmConsoleServerDeclarations","items","ctx","dependencies","item","remapIds","idMap","readItems","ids","writeItems","revertItems","steps","problems","rules","planGate","pluginId","locate","target","crmPersonEraser","erase","resource","handle","entitlement","describe","CRM_API_V1_DESCRIPTIONS","crmUsageFigures","includes","crmContainerDetacher"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the data layer's barrel: boot needs one\n// registry, not the whole server surface.\nimport {\n type ApiV1ResourceHandler,\n registerApiV1Resource,\n registerApiV1UsageFigures,\n} from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport {\n listPluginMembershipDetachers,\n registerPluginMembershipDetacher,\n} from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { registerPluginPersonRecordsEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n registerPluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n checkCrmFieldSetup,\n checkCrmPipelineSetup,\n CRM_FIELD_SETUP_RESOURCE,\n CRM_PIPELINE_SETUP_RESOURCE,\n} from './model/crm-setup-content'\nimport { checkCrmPicklistValuesSetup, CRM_PICKLIST_VALUES_SETUP_RESOURCE } from './model/crm-picklist-setup-content'\nimport { registerCrmSetupAiCapabilities } from './server/crm-setup-ai-capabilities'\nimport { crmTransferPlanGate, registerCrmTransferResources } from './transfer/register'\nimport {\n CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n crmEmailTemplateDependencies,\n remapCrmEmailTemplateIds,\n type CrmEmailTemplatePackageContent,\n} from './transfer/email-templates-package'\n\ntype CrmSetupWriters = typeof import('./server/crm-setup-writers.server')\nlet setupLoading: Promise<CrmSetupWriters> | null = null\n\n/** The CRM's setup writers over the Admin SDK (AGL-3746), loaded on their first read or write. */\nfunction setupWriters(): Promise<CrmSetupWriters> {\n setupLoading ??= import('./server/crm-setup-writers.server').catch((error: unknown) => {\n setupLoading = null\n throw error\n })\n return setupLoading\n}\n\n/**\n * A setup writer as boot registers it: the check is pure and answers at once;\n * the hooks that read or write load the Admin SDK half on first use.\n */\nfunction lazySetupWriter(\n check: (content: Readonly<Record<string, unknown>>) => ReturnType<PluginResourceDraftWriter['check']>,\n writer: (module: CrmSetupWriters) => PluginResourceDraftWriter,\n): PluginResourceDraftWriter {\n return {\n check: (content) => check(content),\n refusal: async (context) => writer(await setupWriters()).refusal(context),\n read: async (context) => writer(await setupWriters()).read(context),\n write: async (request) => writer(await setupWriters()).write(request),\n }\n}\n\n/** The `crm-pipeline` writer as boot registers it. */\nexport const lazyCrmPipelineSetupWriter = lazySetupWriter(\n checkCrmPipelineSetup,\n (module) => module.crmPipelineSetupWriter,\n)\n\n/** The `crm-field` writer as boot registers it. */\nexport const lazyCrmFieldSetupWriter = lazySetupWriter(\n checkCrmFieldSetup,\n (module) => module.crmFieldSetupWriter,\n)\n\n/** The `crm-picklist-values` writer as boot registers it (AGL-3756). */\nexport const lazyCrmPicklistValuesSetupWriter = lazySetupWriter(\n checkCrmPicklistValuesSetup,\n (module) => module.crmPicklistValuesSetupWriter,\n)\n\n/** The email templates package's server half (AGL-3535), loaded when an import or export first asks. */\nconst templatesPackage = async () =>\n (await import('./transfer/email-templates-package.server')).createCrmEmailTemplatesPackage()\n\n/**\n * The plan feature every CRM resource of `/v1` needs, and the sentence its\n * refusal says (AGL-2611, AGL-2851): the whole CRM is included from\n * Starter, so a Free workspace reads and changes none of it over the API. A\n * capture still records people on every plan; working them is the CRM.\n */\nconst CRM_SUITE = {\n feature: 'crm',\n message:\n 'The CRM — contacts, leads, companies, deals, tasks, pipelines, ' +\n 'activities and email templates — is not included in this organization’s plan',\n} as const\n\n/**\n * The CRM's resources on the customer REST API, each handler loaded with the\n * first request for it. In the order the API root lists them: the people\n * first, then the records a team keeps about them, then a site's leads\n * (`/v1/leads?siteId=`, a top-level path with the site as a parameter), then\n * the letters sent from a record (AGL-2658).\n */\nconst CRM_API_V1_RESOURCES: ReadonlyArray<readonly [string, ApiV1ResourceHandler]> = [\n ['contacts', async (...args) => (await import('./server/api-v1/contacts')).handleContacts(...args)],\n ['companies', async (...args) => (await import('./server/api-v1/crm-companies')).handleCompanies(...args)],\n ['pipelines', async (...args) => (await import('./server/api-v1/crm-pipelines')).handlePipelines(...args)],\n ['deals', async (...args) => (await import('./server/api-v1/crm-deals')).handleDeals(...args)],\n ['tasks', async (...args) => (await import('./server/api-v1/crm-tasks')).handleTasks(...args)],\n ['activities', async (...args) => (await import('./server/api-v1/crm-activities')).handleActivities(...args)],\n ['leads', async (...args) => (await import('./server/api-v1/crm-leads')).handleLeads(...args)],\n [\n 'email-templates',\n async (...args) => (await import('./server/api-v1/crm-email-templates')).handleEmailTemplates(...args),\n ],\n]\n\n/**\n * The CRM's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * It serves the CRM's resources of `/v1` (AGL-2606): the console's router\n * owns the pipeline in front of them — the key, the plan's API access, the\n * quota, the rate limit, the error envelope — and hands every request under\n * `/v1/contacts`, `/v1/deals` and the rest to the CRM once the key is\n * authenticated and the organization's plan carries the CRM. Each resource\n * brings its description for the API's OpenAPI document, and the CRM's\n * records band and collection sizes join `GET /v1/usage`. The API is the\n * console's alone, so the tenant runtime does not register them.\n *\n * It registers the server half of the CRM's import and export resources\n * (`transfer/register.ts`).\n *\n * It also clears a removed container off the CRM's records: a campaign is\n * deleted from the console, and its owner asks every plugin's membership\n * detacher first (`server/container-detach.ts`).\n *\n * Light at boot: each resource's module is imported with its first request,\n * the descriptions when the document is first built, and the usage reader\n * with the first usage call.\n * Registering again replaces this plugin's own entries, so a second call (a\n * hot reload, a spec) is harmless.\n */\nexport function registerCrmConsoleServerDeclarations(): void {\n // Email templates in a workspace package (AGL-3535).\n registerPluginTransferResource(\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n {\n items: async (ctx) => (await templatesPackage()).items(ctx),\n dependencies: (item) => crmEmailTemplateDependencies(item as CrmEmailTemplatePackageContent),\n remapIds: (item, idMap) => remapCrmEmailTemplateIds(item as CrmEmailTemplatePackageContent, idMap),\n readItems: async (ctx, ids) => (await templatesPackage()).readItems(ctx, ids),\n writeItems: async (ctx, items, writer) => (await templatesPackage()).writeItems(ctx, items as never, writer),\n revertItems: async (ctx, steps) => (await templatesPackage()).revertItems(ctx, steps as never),\n problems: async (ctx, write) => (await templatesPackage()).problems(ctx, write as never),\n rules: CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n planGate: crmTransferPlanGate('email templates'),\n },\n { pluginId: BUNDLE_ID },\n )\n // The record system's share of a person erasure (AGL-2623, AGL-3080): it\n // names the person's contacts before anybody erases, and erases them, their\n // satellites and their lead after everybody else. Required, and loaded with\n // the first erasure.\n registerPluginPersonRecordsEraser(\n {\n locate: async (target) => (await import('./server/person-eraser')).crmPersonEraser().locate(target),\n erase: async (request) => (await import('./server/person-eraser')).crmPersonEraser().erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n for (const [resource, handle] of CRM_API_V1_RESOURCES) {\n registerApiV1Resource(\n resource,\n {\n handle,\n entitlement: CRM_SUITE,\n describe: async () =>\n (await import('./server/api-v1/openapi')).CRM_API_V1_DESCRIPTIONS[resource],\n },\n { pluginId: BUNDLE_ID },\n )\n }\n registerApiV1UsageFigures(\n async (ctx) => (await import('./server/api-v1/usage')).crmUsageFigures(ctx),\n { pluginId: BUNDLE_ID },\n )\n // The CRM's setup an AI build asks for (AGL-3746): a pipeline and a custom\n // field, saved by these writers under the CRM's rules, and the operations\n // the build plans for them. The console runs AI jobs (AGL-3026).\n registerPluginResourceDraftWriter(CRM_PIPELINE_SETUP_RESOURCE, lazyCrmPipelineSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_FIELD_SETUP_RESOURCE, lazyCrmFieldSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_PICKLIST_VALUES_SETUP_RESOURCE, lazyCrmPicklistValuesSetupWriter, {\n pluginId: BUNDLE_ID,\n })\n registerCrmSetupAiCapabilities()\n // What the CRM imports and exports (AGL-3527): each resource's module\n // loads with the first transfer that asks for it.\n registerCrmTransferResources(BUNDLE_ID)\n // A removed container — a deleted campaign — comes off every lead and every\n // contact facet naming it, before its owner removes it (AGL-3080).\n if (!listPluginMembershipDetachers().includes(BUNDLE_ID)) {\n registerPluginMembershipDetacher(\n async (request) => (await import('./server/container-detach')).crmContainerDetacher(request),\n { pluginId: BUNDLE_ID },\n )\n }\n}\n"],"names":["registerApiV1Resource","registerApiV1UsageFigures","listPluginMembershipDetachers","registerPluginMembershipDetacher","registerPluginPersonRecordsEraser","registerPluginTransferResource","registerPluginResourceDraftWriter","BUNDLE_ID","checkCrmFieldSetup","checkCrmPipelineSetup","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","checkCrmPicklistValuesSetup","CRM_PICKLIST_VALUES_SETUP_RESOURCE","registerCrmSetupAiCapabilities","crmTransferPlanGate","registerCrmTransferResources","CRM_EMAIL_TEMPLATE_PACKAGE_RULES","CRM_EMAIL_TEMPLATES_TRANSFER_KEY","crmEmailTemplateDependencies","remapCrmEmailTemplateIds","setupLoading","setupWriters","catch","error","lazySetupWriter","check","writer","content","refusal","context","read","write","request","lazyCrmPipelineSetupWriter","module","crmPipelineSetupWriter","lazyCrmFieldSetupWriter","crmFieldSetupWriter","lazyCrmPicklistValuesSetupWriter","crmPicklistValuesSetupWriter","templatesPackage","createCrmEmailTemplatesPackage","CRM_SUITE","feature","message","CRM_API_V1_RESOURCES","args","handleContacts","handleCompanies","handlePipelines","handleDeals","handleTasks","handleActivities","handleLeads","handleEmailTemplates","registerCrmConsoleServerDeclarations","items","ctx","dependencies","item","remapIds","idMap","readItems","ids","writeItems","revertItems","steps","problems","rules","planGate","pluginId","locate","target","crmPersonEraser","erase","resource","handle","entitlement","describe","CRM_API_V1_DESCRIPTIONS","crmUsageFigures","includes","crmContainerDetacher"],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmFieldObject, type CrmPicklist, type CrmPicklistDefinition, type CrmPicklistId } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
19
|
+
import { type CrmFieldDefinitionDoc } from './crm-setup-content';
|
|
20
|
+
/**
|
|
21
|
+
* VALUES ADDED TO A CRM PICKLIST (AGL-3756): what a person adds in CRM →
|
|
22
|
+
* Fields with a picklist's **Add value**, or by typing another line into a
|
|
23
|
+
* Choice field's choices — as pure rules, for Aglyn AI's build.
|
|
24
|
+
*
|
|
25
|
+
* Two kinds of list take values, and the console extends both:
|
|
26
|
+
*
|
|
27
|
+
* - every STANDARD picklist in `CRM_PICKLIST_DEFINITIONS` — lead status,
|
|
28
|
+
* lead source, a deal's type and contact role, a company's type,
|
|
29
|
+
* industry, rating and ownership, the salutation, and a task's status,
|
|
30
|
+
* priority, type and subject. An added value is `addPicklistValue`'s, the
|
|
31
|
+
* console's own move: a lead status or task status names its MEANING
|
|
32
|
+
* (never one the platform reserves, such as Qualified), a lead source may
|
|
33
|
+
* name its GROUP (Inbound or Outbound);
|
|
34
|
+
* - a custom CHOICE field's options, on the object it describes.
|
|
35
|
+
*
|
|
36
|
+
* ONLY ADDS. Nothing is removed, renamed, reordered, deactivated or
|
|
37
|
+
* reactivated: a value already in the list, in any capitals, is left as it
|
|
38
|
+
* is — an inactive one included, which the console would have the person
|
|
39
|
+
* activate, so it is reported rather than touched. Asked twice, nothing is
|
|
40
|
+
* added twice.
|
|
41
|
+
*/
|
|
42
|
+
/** The resource the CRM's picklist values writer is registered under. */
|
|
43
|
+
export declare const CRM_PICKLIST_VALUES_SETUP_RESOURCE = "crm-picklist-values";
|
|
44
|
+
/** The `list` that names a custom Choice field rather than a standard picklist. */
|
|
45
|
+
export declare const CRM_CUSTOM_CHOICE_LIST = "custom";
|
|
46
|
+
/** The most values one ask adds. */
|
|
47
|
+
export declare const CRM_PICKLIST_VALUES_SETUP_MAX = 50;
|
|
48
|
+
/** Every `list` an ask may name: the standard picklists, then a custom Choice field. */
|
|
49
|
+
export declare const CRM_PICKLIST_VALUES_LISTS: readonly string[];
|
|
50
|
+
/** Which list an ask adds to. */
|
|
51
|
+
export type CrmPicklistValuesTarget = {
|
|
52
|
+
kind: 'picklist';
|
|
53
|
+
id: CrmPicklistId;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'custom';
|
|
56
|
+
object: CrmFieldObject;
|
|
57
|
+
field: string;
|
|
58
|
+
};
|
|
59
|
+
/** One ask, read. */
|
|
60
|
+
export interface CrmPicklistValuesSetup {
|
|
61
|
+
target: CrmPicklistValuesTarget;
|
|
62
|
+
/** The labels, normalized, without duplicates in any capitals, in the asked order. */
|
|
63
|
+
values: string[];
|
|
64
|
+
/** What each value means, on a picklist with meanings; `null` otherwise. */
|
|
65
|
+
meaning: string | null;
|
|
66
|
+
/** The group each value is filed under, on a picklist with groups; `null` for none. */
|
|
67
|
+
group: string | null;
|
|
68
|
+
}
|
|
69
|
+
/** An ask's content, read and judged. Pure. */
|
|
70
|
+
export declare function readCrmPicklistValuesSetup(content: Readonly<Record<string, unknown>>): {
|
|
71
|
+
ok: true;
|
|
72
|
+
value: CrmPicklistValuesSetup;
|
|
73
|
+
} | {
|
|
74
|
+
ok: false;
|
|
75
|
+
problems: string[];
|
|
76
|
+
};
|
|
77
|
+
/** The writer's check: whether the content is an ask, with the list and values it adds. Pure. */
|
|
78
|
+
export declare function checkCrmPicklistValuesSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
|
|
79
|
+
/** What adding to a list comes to: the values added, those it already held, and those it holds inactive. */
|
|
80
|
+
export interface CrmPicklistValuesOutcome {
|
|
81
|
+
added: string[];
|
|
82
|
+
existing: string[];
|
|
83
|
+
inactive: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The values a standard picklist gains, as the console's Add value adds them
|
|
87
|
+
* one by one at the end of the list. Pure. A value the list holds, in any
|
|
88
|
+
* capitals, is left as it is; the list's ceiling refuses the whole ask
|
|
89
|
+
* rather than adding part of it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function planCrmPicklistValues(definition: CrmPicklistDefinition, picklist: CrmPicklist, setup: Pick<CrmPicklistValuesSetup, 'values' | 'meaning' | 'group'>): ({
|
|
92
|
+
ok: true;
|
|
93
|
+
picklist: CrmPicklist;
|
|
94
|
+
} & CrmPicklistValuesOutcome) | {
|
|
95
|
+
ok: false;
|
|
96
|
+
error: string;
|
|
97
|
+
};
|
|
98
|
+
/** What adding choices to a custom Choice field comes to. */
|
|
99
|
+
export type CrmCustomChoiceValuesPlan = ({
|
|
100
|
+
ok: true;
|
|
101
|
+
id: string;
|
|
102
|
+
label: string;
|
|
103
|
+
options: string[];
|
|
104
|
+
} & CrmPicklistValuesOutcome) | {
|
|
105
|
+
ok: false;
|
|
106
|
+
status: 404 | 409;
|
|
107
|
+
error: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* The choices a custom Choice field gains, appended as the drawer appends a
|
|
111
|
+
* typed line. Pure. The field is the active one on the object with that
|
|
112
|
+
* label or key, in any capitals; a choice it holds in any capitals is left
|
|
113
|
+
* as it is, and the drawer's 50-choice ceiling refuses the whole ask.
|
|
114
|
+
*/
|
|
115
|
+
export declare function planCrmCustomChoiceValues(setup: Pick<CrmPicklistValuesSetup, 'values'> & {
|
|
116
|
+
target: Extract<CrmPicklistValuesTarget, {
|
|
117
|
+
kind: 'custom';
|
|
118
|
+
}>;
|
|
119
|
+
}, definitions: readonly CrmFieldDefinitionDoc[]): CrmCustomChoiceValuesPlan;
|
|
120
|
+
/** Where a picklist's values are kept, for a refusal and a record's name: "Lead status (Leads)". */
|
|
121
|
+
export declare function crmPicklistValuesName(definition: CrmPicklistDefinition): string;
|