@aglyn/plugins-crm 1.0.0-beta.230 → 1.0.0-beta.231
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/package.json +14 -13
- package/src/lib/components/crm-next-activity-column.d.ts +1 -13
- package/src/lib/components/crm-next-activity-column.js +1 -19
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +2 -14
- package/src/lib/components/lead-status-options.js +1 -13
- package/src/lib/components/lead-status-options.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
- package/src/lib/components/lead-unqualify-dialog.js +2 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/settings-section.d.ts +18 -0
- package/src/lib/components/settings-section.js +97 -1
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +2 -2
- package/src/lib/constants/company-filters.js +1 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +3 -3
- package/src/lib/constants/contact-filters.js +2 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +3 -3
- package/src/lib/constants/deal-filters.js +1 -1
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +1 -1
- package/src/lib/constants/field-list-query.js +1 -1
- package/src/lib/constants/field-list-query.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contacts-list-seed.d.ts +1 -1
- package/src/lib/model/contacts-list-seed.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -4
- package/src/lib/model/crm-list-query.js +2 -2
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +13 -0
- package/src/lib/model/crm-next-activity.js +19 -0
- package/src/lib/model/crm-next-activity.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +6 -5
- package/src/lib/model/lead-filters.js +3 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-status-choices.d.ts +32 -0
- package/src/lib/model/lead-status-choices.js +31 -0
- package/src/lib/model/lead-status-choices.js.map +1 -0
- package/src/lib/model/task-views.d.ts +2 -2
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/person-records.d.ts +27 -0
- package/src/lib/server/person-records.js +127 -4
- package/src/lib/server/person-records.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/field-list-query.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 type { CrmFieldObject } from '@aglyn/aglyn'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n} from '@aglyn/shared-
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/field-list-query.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 type { CrmFieldObject } from '@aglyn/aglyn'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT THE CRM › FIELDS TABLE ASKS FIRESTORE (AGL-3335).\n *\n * One query over `orgs/{orgId}/contactFields`, narrowed to the tab\n * (`object ==`, the list's base) and read whole — at most\n * `CONTACT_FIELDS_MAX_PER_ORG` — in document-id order. Every clause the\n * Filters panel offers and the search box are predicates on it:\n *\n * type the stored type;\n * required the stored flag, a boolean on every definition;\n * search `searchTokens`: every prefix of the field's name and of its\n * key, whole and split at `_`, `-` and `.`.\n *\n * All three are equalities beside the base, and the order is the document\n * id, so Firestore answers every combination by merging the single-field\n * indexes it keeps on its own: the table costs no composite. The stored\n * `order`, and the grid's sorts by Field, Key and Type, are applied to the\n * answer, which is every match there is — so none of them reorders a\n * window. The three fields are stamped by the Fields section's writes\n * (`crmFieldListFields`) and, for the definitions written before, by\n * `tools/scripts/backfill-crm-list-fields.mjs`.\n */\n\n/** The array the search box reads. */\nexport const CRM_FIELD_SEARCH_TOKENS = 'searchTokens'\n\nexport const CRM_FIELD_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'type',\n kind: 'exact',\n path: 'type',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n { column: 'required', kind: 'boolean', path: 'required', operators: ['equals'] },\n ],\n sorts: [{ path: LIST_QUERY_ID_PATH, direction: 'asc' }],\n search: { tokensPath: CRM_FIELD_SEARCH_TOKENS },\n}\n\n/** The scope every read of the table carries: this tab's definitions. */\nexport function crmFieldListBase(object: CrmFieldObject): ListQueryFilter[] {\n return [{ path: 'object', op: '==', value: object }]\n}\n"],"names":["LIST_QUERY_ID_PATH","CRM_FIELD_SEARCH_TOKENS","CRM_FIELD_LIST_QUERY","fields","column","kind","path","presence","operators","sorts","direction","search","tokensPath","crmFieldListBase","object","op","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,kBAAkB,QAGb,sDAAqD;AAE5D;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,oCAAoC,GACpC,OAAO,MAAMC,0BAA0B,eAAc;AAErD,OAAO,MAAMC,uBAA6C;IACxDC,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNC,MAAM;YACNC,UAAU;YACVC,WAAW;gBAAC;gBAAU;aAAU;QAClC;QACA;YAAEJ,QAAQ;YAAYC,MAAM;YAAWC,MAAM;YAAYE,WAAW;gBAAC;aAAS;QAAC;KAChF;IACDC,OAAO;QAAC;YAAEH,MAAMN;YAAoBU,WAAW;QAAM;KAAE;IACvDC,QAAQ;QAAEC,YAAYX;IAAwB;AAChD,EAAC;AAED,uEAAuE,GACvE,OAAO,SAASY,iBAAiBC,MAAsB;IACrD,OAAO;QAAC;YAAER,MAAM;YAAUS,IAAI;YAAMC,OAAOF;QAAO;KAAE;AACtD"}
|
|
@@ -71,6 +71,15 @@ import { summarizeConsentGroupChange } from "./model/consent-group-summary.js";
|
|
|
71
71
|
async wroteIn (request) {
|
|
72
72
|
const { crmPersonRecords } = await import("./server/person-records.js");
|
|
73
73
|
return crmPersonRecords.wroteIn(request);
|
|
74
|
+
},
|
|
75
|
+
async search (request) {
|
|
76
|
+
const { crmPersonRecords } = await import("./server/person-records.js");
|
|
77
|
+
return crmPersonRecords.search(request);
|
|
78
|
+
},
|
|
79
|
+
// The walk a connector copies a site's people out by (AGL-3639).
|
|
80
|
+
async changedSince (request) {
|
|
81
|
+
const { crmPersonRecords } = await import("./server/person-records.js");
|
|
82
|
+
return crmPersonRecords.changedSince(request);
|
|
74
83
|
}
|
|
75
84
|
};
|
|
76
85
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.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 contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport { registerPluginConsentGroupParticipant } from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { registerPluginLeadConversionListener } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonMatcher } from '@aglyn/aglyn/plugin-manager/plugin-person-matches'\nimport {\n registerPluginPersonRecords,\n type PluginPersonRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { registerPluginRecordEmailStateWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { registerPluginRecordOriginWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordTimelineWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginRecordWrittenListener } from '@aglyn/aglyn/plugin-manager/plugin-record-written'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { BUNDLE_ID, CRM_RECORDS_METER_ID, CRM_STEP_TYPES } from './constants/bundle-common'\nimport { summarizeConsentGroupChange } from './model/consent-group-summary'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The CRM as the plugin other plugins ask for people (AGL-3080), deferred\n * like the capture: `server/person-records.ts` and the Admin SDK it brings\n * load with the first question, not when the process starts.\n */\nexport const crmPersonRecordsService: PluginPersonRecords = {\n async find(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.find(request)\n },\n async read(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.read(request)\n },\n async fileUnder(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.fileUnder(request)\n },\n async recordRefund(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.recordRefund(request)\n },\n async peopleInView(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.peopleInView(request)\n },\n async wroteIn(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.wroteIn(request)\n },\n}\n\n/**\n * The `pipeline` record index, its reads loaded with the first one — see\n * `server/pipeline-record-index.ts` for what it answers.\n */\nexport const crmPipelineRecordIndex: PluginRecordIndex = {\n async list(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.list(request)\n },\n async get(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.get(request)\n },\n}\n\n/** One of `server/crm-record-indexes.ts`'s indexes, its reads loaded with the first one. */\nfunction lazyCrmRecordIndex(kind: 'company' | 'messageTemplate'): PluginRecordIndex {\n return {\n async list(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].list(request)\n },\n async get(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].get(request)\n },\n }\n}\n\n/** The companies a person works for, as another plugin reads them (AGL-3080). */\nexport const crmCompanyRecordIndex = lazyCrmRecordIndex('company')\n\n/** The email templates and snippets reps write, as another plugin reads them (AGL-3080). */\nexport const crmMessageTemplateRecordIndex = lazyCrmRecordIndex('messageTemplate')\n\n/**\n * The CRM's writer on the record-timeline seam (AGL-2981), deferred: the\n * writer and the Admin SDK it brings load with the first entry filed.\n */\nexport const crmRecordTimelineWriter: PluginRecordTimelineWriter = {\n async logActivity(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).logActivity(request)\n },\n async createTask(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).createTask(request)\n },\n async recordEmailDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordEmailDelivery!(request)\n },\n // A delivery event for a message the CRM tagged (AGL-2615, AGL-3080).\n async recordTaggedDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordTaggedDelivery!(request)\n },\n // An automation's email to the person its event is about, filed on that\n // person's timeline with its delivery state (AGL-2615, AGL-3080).\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./server/automation-steps')\n return prepareCrmRecordEmail(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n // The other half of keeping people (AGL-3080): another plugin that holds\n // an address, or a record the CRM handed it, asks here for the person —\n // a flow email's consent read, a refund, an automation filing somebody\n // under a campaign — and never opens the CRM's collections itself.\n registerPluginPersonRecords(crmPersonRecordsService, { pluginId: BUNDLE_ID })\n // The CRM's own share of a lead conversion (AGL-3254): the lead's\n // campaigns go onto the contact's facet. Through the seam every door\n // that converts a lead reaches, and deferred like the capture: the\n // module that writes is loaded when the first conversion arrives, and\n // it brings the Admin SDK with it — this file defers the plugin's OWN\n // module only. A library imported statically across the plugin cannot\n // also be lazy-loaded here: `enforce-module-boundaries` refuses every\n // static import of a library the project lazy-loads anywhere.\n registerPluginLeadConversionListener(\n async (request) => {\n const { carryLeadCampaignsOnConversion } =\n await import('./server/lead-campaign-carry')\n return carryLeadCampaignsOnConversion(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // \"Did this workspace already know this person?\" (AGL-3289), asked by the\n // staff console's acquisition card. Deferred like the rest: the reads load\n // when a staff member first opens a card, not when the process starts.\n registerPluginPersonMatcher(\n async (request) => {\n const { matchCrmPeople } = await import('./server/person-matches')\n return matchCrmPeople(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // The CRM's share of a consent group change (AGL-3320): a contact's\n // per-site refusal carried across a separation, and the records keyed by a\n // group re-homed after the flip. Registered whether or not a workspace has\n // the CRM switched on, because the contacts — and the refusals on them —\n // outlive the switch. Deferred like the rest; the summary is plain words\n // and answers synchronously.\n // The record email-state writer (AGL-3245), here as well as in the console\n // API so the tenant's doors reach it too: a form submission's capture is\n // checked for deliverability after its response (AGL-3328), and the \"Would\n // bounce\" that check finds is written through this seam. Deferred like the\n // rest; the console's own registration replaces this one with the same\n // writer, loaded eagerly.\n registerPluginRecordEmailStateWriter(\n {\n async stamp(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).stamp(request)\n },\n // A send's first opens and clicks, stamped on the contacts they name\n // (AGL-2616) — the campaign webhook's courtesy, deferred like the rest.\n async engaged(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).engaged!(request)\n },\n // A delivered campaign's leads, moved to Nurturing (AGL-3446).\n async reached(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).reached!(request)\n },\n // A lead that wrote back, moved to Working (AGL-3080).\n async replied(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).replied!(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // The record timeline writer (AGL-2981), here as well as in the API\n // surfaces so a door in either app reaches it: a booking confirmed on the\n // tenant, or by the payment webhook, files its meeting and follow-up\n // through it (AGL-2660). Deferred like the rest; an API registration\n // replaces this one with the same writer, loaded eagerly.\n registerPluginRecordTimelineWriter(crmRecordTimelineWriter, { pluginId: BUNDLE_ID })\n // Where a person came from (AGL-3519): a door the CRM does not run — the\n // platform's account sign-up, a sequence enrolling someone — names its\n // origin, and the CRM stamps the built-in Lead source on a record holding\n // none. Deferred like the rest.\n registerPluginRecordOriginWriter(\n {\n async stamp(request) {\n const { crmRecordOriginWriter } = await import('./server/record-origin')\n return crmRecordOriginWriter.stamp(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // Sharing rules (AGL-3336), re-evaluated after every server write of a\n // lead, a contact, a company or a deal: the core tells its record-written\n // listeners from the list-field restamp every such writer ends with, in\n // both apps — a form's capture on the tenant, an edit in the console.\n // Deferred like the rest: an org with no rules pays one cached read.\n registerPluginRecordWrittenListener(\n async (event) => {\n const { crmSharingRecordWritten } = await import('./server/crm-sharing')\n await crmSharingRecordWritten(event)\n },\n { pluginId: BUNDLE_ID, key: 'sharing' },\n )\n // The organization's pipelines and their stages, as another plugin reads\n // them (AGL-3080): an automation the AI drafts names the stage a deal\n // moves to. Deferred like the rest: the reads load with the first one.\n registerPluginRecordIndex('pipeline', crmPipelineRecordIndex, { pluginId: BUNDLE_ID })\n // The company a person works for, and the templates reps write, as a sales\n // sequence reads them (AGL-3080). Deferred like the rest.\n registerPluginRecordIndex('company', crmCompanyRecordIndex, { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('messageTemplate', crmMessageTemplateRecordIndex, { pluginId: BUNDLE_ID })\n // The automation steps that write the CRM (AGL-2605, AGL-3080), run for the\n // workflows engine through the server-step seam and declared under\n // `serverSteps`. Deferred like the rest: the writes load with the first step.\n registerServerStepExecutor(\n CRM_STEP_TYPES,\n async (request) => (await import('./server/automation-steps')).runCrmAutomationStep(request),\n { pluginId: BUNDLE_ID },\n )\n registerPluginConsentGroupParticipant(\n {\n async preview(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.preview(request)\n },\n async run(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.run(request)\n },\n summarize: summarizeConsentGroupChange,\n },\n { pluginId: BUNDLE_ID },\n )\n // The records band in the monthly usage sweep, a core cron that never\n // loads a CRM door; declared in `usageAxes` so the sweep refuses to bill a\n // month without it. Deferred like the rest: the counts load with the\n // first sweep.\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: CRM_RECORDS_METER_ID,\n measure: async (context) => {\n const { measureCrmRecords } = await import('./server/crm-records-meter')\n return measureCrmRecords(context)\n },\n })\n}\n\n"],"names":["registerPluginConsentGroupParticipant","registerPluginContactCaptureWriter","registerPluginLeadConversionListener","registerPluginPersonMatcher","registerPluginPersonRecords","registerPluginRecordIndex","registerPluginRecordEmailStateWriter","registerPluginRecordOriginWriter","registerPluginRecordTimelineWriter","registerPluginRecordWrittenListener","registerServerStepExecutor","registerPluginUsageMeter","BUNDLE_ID","CRM_RECORDS_METER_ID","CRM_STEP_TYPES","summarizeConsentGroupChange","crmContactCaptureWriter","capture","request","captureContactForCrm","crmPersonRecordsService","find","crmPersonRecords","read","fileUnder","recordRefund","peopleInView","wroteIn","crmPipelineRecordIndex","list","pipelineRecordIndex","get","lazyCrmRecordIndex","kind","crmRecordIndexes","crmCompanyRecordIndex","crmMessageTemplateRecordIndex","crmRecordTimelineWriter","logActivity","createCrmRecordTimelineWriter","defaultCrmRecordTimelineDeps","createTask","recordEmailDelivery","recordTaggedDelivery","prepareEmail","prepareCrmRecordEmail","registerCrmServerDeclarations","pluginId","carryLeadCampaignsOnConversion","matchCrmPeople","stamp","createCrmRecordEmailStateWriter","defaultCrmRecordEmailStateDeps","engaged","reached","replied","crmRecordOriginWriter","event","crmSharingRecordWritten","key","runCrmAutomationStep","preview","consentGroupParticipant","run","summarize","id","measure","context","measureCrmRecords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.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 contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport { registerPluginConsentGroupParticipant } from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { registerPluginLeadConversionListener } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonMatcher } from '@aglyn/aglyn/plugin-manager/plugin-person-matches'\nimport {\n registerPluginPersonRecords,\n type PluginPersonRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { registerPluginRecordEmailStateWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { registerPluginRecordOriginWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordTimelineWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginRecordWrittenListener } from '@aglyn/aglyn/plugin-manager/plugin-record-written'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { BUNDLE_ID, CRM_RECORDS_METER_ID, CRM_STEP_TYPES } from './constants/bundle-common'\nimport { summarizeConsentGroupChange } from './model/consent-group-summary'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The CRM as the plugin other plugins ask for people (AGL-3080), deferred\n * like the capture: `server/person-records.ts` and the Admin SDK it brings\n * load with the first question, not when the process starts.\n */\nexport const crmPersonRecordsService: PluginPersonRecords = {\n async find(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.find(request)\n },\n async read(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.read(request)\n },\n async fileUnder(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.fileUnder(request)\n },\n async recordRefund(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.recordRefund(request)\n },\n async peopleInView(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.peopleInView(request)\n },\n async wroteIn(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.wroteIn(request)\n },\n async search(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.search(request)\n },\n // The walk a connector copies a site's people out by (AGL-3639).\n async changedSince(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.changedSince(request)\n },\n}\n\n/**\n * The `pipeline` record index, its reads loaded with the first one — see\n * `server/pipeline-record-index.ts` for what it answers.\n */\nexport const crmPipelineRecordIndex: PluginRecordIndex = {\n async list(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.list(request)\n },\n async get(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.get(request)\n },\n}\n\n/** One of `server/crm-record-indexes.ts`'s indexes, its reads loaded with the first one. */\nfunction lazyCrmRecordIndex(kind: 'company' | 'messageTemplate'): PluginRecordIndex {\n return {\n async list(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].list(request)\n },\n async get(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].get(request)\n },\n }\n}\n\n/** The companies a person works for, as another plugin reads them (AGL-3080). */\nexport const crmCompanyRecordIndex = lazyCrmRecordIndex('company')\n\n/** The email templates and snippets reps write, as another plugin reads them (AGL-3080). */\nexport const crmMessageTemplateRecordIndex = lazyCrmRecordIndex('messageTemplate')\n\n/**\n * The CRM's writer on the record-timeline seam (AGL-2981), deferred: the\n * writer and the Admin SDK it brings load with the first entry filed.\n */\nexport const crmRecordTimelineWriter: PluginRecordTimelineWriter = {\n async logActivity(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).logActivity(request)\n },\n async createTask(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).createTask(request)\n },\n async recordEmailDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordEmailDelivery!(request)\n },\n // A delivery event for a message the CRM tagged (AGL-2615, AGL-3080).\n async recordTaggedDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordTaggedDelivery!(request)\n },\n // An automation's email to the person its event is about, filed on that\n // person's timeline with its delivery state (AGL-2615, AGL-3080).\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./server/automation-steps')\n return prepareCrmRecordEmail(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n // The other half of keeping people (AGL-3080): another plugin that holds\n // an address, or a record the CRM handed it, asks here for the person —\n // a flow email's consent read, a refund, an automation filing somebody\n // under a campaign — and never opens the CRM's collections itself.\n registerPluginPersonRecords(crmPersonRecordsService, { pluginId: BUNDLE_ID })\n // The CRM's own share of a lead conversion (AGL-3254): the lead's\n // campaigns go onto the contact's facet. Through the seam every door\n // that converts a lead reaches, and deferred like the capture: the\n // module that writes is loaded when the first conversion arrives, and\n // it brings the Admin SDK with it — this file defers the plugin's OWN\n // module only. A library imported statically across the plugin cannot\n // also be lazy-loaded here: `enforce-module-boundaries` refuses every\n // static import of a library the project lazy-loads anywhere.\n registerPluginLeadConversionListener(\n async (request) => {\n const { carryLeadCampaignsOnConversion } =\n await import('./server/lead-campaign-carry')\n return carryLeadCampaignsOnConversion(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // \"Did this workspace already know this person?\" (AGL-3289), asked by the\n // staff console's acquisition card. Deferred like the rest: the reads load\n // when a staff member first opens a card, not when the process starts.\n registerPluginPersonMatcher(\n async (request) => {\n const { matchCrmPeople } = await import('./server/person-matches')\n return matchCrmPeople(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // The CRM's share of a consent group change (AGL-3320): a contact's\n // per-site refusal carried across a separation, and the records keyed by a\n // group re-homed after the flip. Registered whether or not a workspace has\n // the CRM switched on, because the contacts — and the refusals on them —\n // outlive the switch. Deferred like the rest; the summary is plain words\n // and answers synchronously.\n // The record email-state writer (AGL-3245), here as well as in the console\n // API so the tenant's doors reach it too: a form submission's capture is\n // checked for deliverability after its response (AGL-3328), and the \"Would\n // bounce\" that check finds is written through this seam. Deferred like the\n // rest; the console's own registration replaces this one with the same\n // writer, loaded eagerly.\n registerPluginRecordEmailStateWriter(\n {\n async stamp(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).stamp(request)\n },\n // A send's first opens and clicks, stamped on the contacts they name\n // (AGL-2616) — the campaign webhook's courtesy, deferred like the rest.\n async engaged(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).engaged!(request)\n },\n // A delivered campaign's leads, moved to Nurturing (AGL-3446).\n async reached(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).reached!(request)\n },\n // A lead that wrote back, moved to Working (AGL-3080).\n async replied(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).replied!(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // The record timeline writer (AGL-2981), here as well as in the API\n // surfaces so a door in either app reaches it: a booking confirmed on the\n // tenant, or by the payment webhook, files its meeting and follow-up\n // through it (AGL-2660). Deferred like the rest; an API registration\n // replaces this one with the same writer, loaded eagerly.\n registerPluginRecordTimelineWriter(crmRecordTimelineWriter, { pluginId: BUNDLE_ID })\n // Where a person came from (AGL-3519): a door the CRM does not run — the\n // platform's account sign-up, a sequence enrolling someone — names its\n // origin, and the CRM stamps the built-in Lead source on a record holding\n // none. Deferred like the rest.\n registerPluginRecordOriginWriter(\n {\n async stamp(request) {\n const { crmRecordOriginWriter } = await import('./server/record-origin')\n return crmRecordOriginWriter.stamp(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // Sharing rules (AGL-3336), re-evaluated after every server write of a\n // lead, a contact, a company or a deal: the core tells its record-written\n // listeners from the list-field restamp every such writer ends with, in\n // both apps — a form's capture on the tenant, an edit in the console.\n // Deferred like the rest: an org with no rules pays one cached read.\n registerPluginRecordWrittenListener(\n async (event) => {\n const { crmSharingRecordWritten } = await import('./server/crm-sharing')\n await crmSharingRecordWritten(event)\n },\n { pluginId: BUNDLE_ID, key: 'sharing' },\n )\n // The organization's pipelines and their stages, as another plugin reads\n // them (AGL-3080): an automation the AI drafts names the stage a deal\n // moves to. Deferred like the rest: the reads load with the first one.\n registerPluginRecordIndex('pipeline', crmPipelineRecordIndex, { pluginId: BUNDLE_ID })\n // The company a person works for, and the templates reps write, as a sales\n // sequence reads them (AGL-3080). Deferred like the rest.\n registerPluginRecordIndex('company', crmCompanyRecordIndex, { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('messageTemplate', crmMessageTemplateRecordIndex, { pluginId: BUNDLE_ID })\n // The automation steps that write the CRM (AGL-2605, AGL-3080), run for the\n // workflows engine through the server-step seam and declared under\n // `serverSteps`. Deferred like the rest: the writes load with the first step.\n registerServerStepExecutor(\n CRM_STEP_TYPES,\n async (request) => (await import('./server/automation-steps')).runCrmAutomationStep(request),\n { pluginId: BUNDLE_ID },\n )\n registerPluginConsentGroupParticipant(\n {\n async preview(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.preview(request)\n },\n async run(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.run(request)\n },\n summarize: summarizeConsentGroupChange,\n },\n { pluginId: BUNDLE_ID },\n )\n // The records band in the monthly usage sweep, a core cron that never\n // loads a CRM door; declared in `usageAxes` so the sweep refuses to bill a\n // month without it. Deferred like the rest: the counts load with the\n // first sweep.\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: CRM_RECORDS_METER_ID,\n measure: async (context) => {\n const { measureCrmRecords } = await import('./server/crm-records-meter')\n return measureCrmRecords(context)\n },\n })\n}\n\n"],"names":["registerPluginConsentGroupParticipant","registerPluginContactCaptureWriter","registerPluginLeadConversionListener","registerPluginPersonMatcher","registerPluginPersonRecords","registerPluginRecordIndex","registerPluginRecordEmailStateWriter","registerPluginRecordOriginWriter","registerPluginRecordTimelineWriter","registerPluginRecordWrittenListener","registerServerStepExecutor","registerPluginUsageMeter","BUNDLE_ID","CRM_RECORDS_METER_ID","CRM_STEP_TYPES","summarizeConsentGroupChange","crmContactCaptureWriter","capture","request","captureContactForCrm","crmPersonRecordsService","find","crmPersonRecords","read","fileUnder","recordRefund","peopleInView","wroteIn","search","changedSince","crmPipelineRecordIndex","list","pipelineRecordIndex","get","lazyCrmRecordIndex","kind","crmRecordIndexes","crmCompanyRecordIndex","crmMessageTemplateRecordIndex","crmRecordTimelineWriter","logActivity","createCrmRecordTimelineWriter","defaultCrmRecordTimelineDeps","createTask","recordEmailDelivery","recordTaggedDelivery","prepareEmail","prepareCrmRecordEmail","registerCrmServerDeclarations","pluginId","carryLeadCampaignsOnConversion","matchCrmPeople","stamp","createCrmRecordEmailStateWriter","defaultCrmRecordEmailStateDeps","engaged","reached","replied","crmRecordOriginWriter","event","crmSharingRecordWritten","key","runCrmAutomationStep","preview","consentGroupParticipant","run","summarize","id","measure","context","measureCrmRecords"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type ContactSource } from '@aglyn/aglyn';
|
|
18
|
-
import type { ListFilterRequest } from '@aglyn/shared-
|
|
18
|
+
import type { ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
19
19
|
/** What the Contacts list opens ON, read off its own address. */
|
|
20
20
|
export interface ContactsListSeed {
|
|
21
21
|
/** The grid filter to start with — the query's predicate — or none. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-list-seed.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 CONTACT_SOURCE_LABELS,\n type ContactSource,\n normalizeContactEmail,\n} from '@aglyn/aglyn'\nimport type { ListFilterRequest } from '@aglyn/shared-
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-list-seed.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 CONTACT_SOURCE_LABELS,\n type ContactSource,\n normalizeContactEmail,\n} from '@aglyn/aglyn'\nimport type { ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport {\n CONTACTS_LIST_EMAIL_PARAM,\n CONTACTS_LIST_FORM_PARAM,\n CONTACTS_LIST_SOURCE_PARAM,\n} from './crm-routes'\n\n/** What the Contacts list opens ON, read off its own address. */\nexport interface ContactsListSeed {\n /** The grid filter to start with — the query's predicate — or none. */\n filter: ListFilterRequest | null\n /** The source select's starting value. */\n source: ContactSource | ''\n /** The form the list was opened for, when it was. */\n formId: string | null\n /**\n * The address the list was asked to OPEN, normalized, when it was — the\n * list moves on to the record itself once exactly one row matches.\n */\n openEmail: string | null\n}\n\nconst NO_SEED: ContactsListSeed = {\n filter: null,\n source: '',\n formId: null,\n openEmail: null,\n}\n\n/**\n * The Contacts list's starting state, from the query string another surface\n * sent it with (AGL-2612).\n *\n * Two surfaces address the list rather than a record: a form's own page,\n * asking for the people that form captured, and an Inbox submission row,\n * asking for the person with this address. Each is a filter the list\n * already speaks — `formIds contains` and `email equals` — so the seed is\n * the SAME `ListFilterRequest` the grid's own panel would produce, and the\n * query built from it is the query a typed filter builds. Nothing here is a\n * second predicate.\n *\n * Pure, and strict about what it accepts: an unknown source or an address\n * that is not one seeds nothing rather than a filter that matches nothing,\n * because an empty list under a bad link reads as \"no such people\".\n */\nexport function contactsListSeed(\n params: { get(name: string): string | null } | null | undefined,\n): ContactsListSeed {\n if (!params) return NO_SEED\n const rawSource = params.get(CONTACTS_LIST_SOURCE_PARAM) ?? ''\n const source =\n rawSource in CONTACT_SOURCE_LABELS ? (rawSource as ContactSource) : ''\n const formId = (params.get(CONTACTS_LIST_FORM_PARAM) ?? '').trim() || null\n const openEmail = normalizeContactEmail(\n params.get(CONTACTS_LIST_EMAIL_PARAM),\n )\n const filter: ListFilterRequest | null = formId\n ? { field: 'formIds', op: 'contains', value: formId }\n : openEmail\n ? { field: 'email', op: 'equals', value: openEmail }\n : null\n return { filter, source, formId, openEmail }\n}\n"],"names":["CONTACT_SOURCE_LABELS","normalizeContactEmail","CONTACTS_LIST_EMAIL_PARAM","CONTACTS_LIST_FORM_PARAM","CONTACTS_LIST_SOURCE_PARAM","NO_SEED","filter","source","formId","openEmail","contactsListSeed","params","rawSource","get","trim","field","op","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EAErBC,qBAAqB,QAChB,eAAc;AAErB,SACEC,yBAAyB,EACzBC,wBAAwB,EACxBC,0BAA0B,QACrB,kBAAc;AAiBrB,MAAMC,UAA4B;IAChCC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;IACRC,WAAW;AACb;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,iBACdC,MAA+D;QAG7CA,aAGFA;IAJhB,IAAI,CAACA,QAAQ,OAAON;IACpB,MAAMO,aAAYD,cAAAA,OAAOE,GAAG,CAACT,uCAAXO,cAA0C;IAC5D,MAAMJ,SACJK,aAAaZ,wBAAyBY,YAA8B;IACtE,MAAMJ,SAAS,EAACG,eAAAA,OAAOE,GAAG,CAACV,qCAAXQ,eAAwC,IAAIG,IAAI,MAAM;IACtE,MAAML,YAAYR,sBAChBU,OAAOE,GAAG,CAACX;IAEb,MAAMI,SAAmCE,SACrC;QAAEO,OAAO;QAAWC,IAAI;QAAYC,OAAOT;IAAO,IAClDC,YACE;QAAEM,OAAO;QAASC,IAAI;QAAUC,OAAOR;IAAU,IACjD;IACN,OAAO;QAAEH;QAAQC;QAAQC;QAAQC;IAAU;AAC7C"}
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type CrmListCollection, type CrmListFieldsContext } from '@aglyn/aglyn';
|
|
18
|
-
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-
|
|
19
|
-
import type { ListFilterOption } from '@aglyn/shared-
|
|
20
|
-
import type { ListQueryDeclaration, ListQueryFilter, ListQueryPlan } from '@aglyn/shared-
|
|
17
|
+
import { type CrmListCollection, type CrmListFieldsContext } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
19
|
+
import type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
|
|
20
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQueryPlan } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
21
21
|
/** Every CRM list's quick search: the tokens every writer stamps. */
|
|
22
22
|
export declare const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']>;
|
|
23
23
|
/**
|
|
@@ -14,9 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { CRM_SCOPED_SEARCH_TOKENS_FIELD, CRM_SEARCH_TOKENS_FIELD, crmListFields } from "@aglyn/aglyn";
|
|
17
|
+
*/ import { CRM_SCOPED_SEARCH_TOKENS_FIELD, CRM_SEARCH_TOKENS_FIELD, crmListFields } from "@aglyn/aglyn/app-utils/crm";
|
|
18
18
|
import { scopedSearch } from "@aglyn/aglyn/app-utils/name-search";
|
|
19
|
-
import { listQueryRefusals } from "@aglyn/shared-
|
|
19
|
+
import { listQueryRefusals } from "@aglyn/shared-util-tools/list-query/list-query-refusals";
|
|
20
20
|
/*
|
|
21
21
|
* THE CRM LISTS ON THEIR QUERIES (AGL-3321).
|
|
22
22
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-list-query.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 CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n type CrmListFieldsContext,\n crmListFields,\n} from '@aglyn/aglyn'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { listQueryRefusals } from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may. A lead's direction is read off the org's\n * lead source list, which the caller passes in `context` once it has it.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n context: CrmListFieldsContext = {},\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged, context)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n\n/**\n * The list fields a deal's contact roles are found by (AGL-3521) — what a\n * client write of `contactRoles` or `contactId` rewrites beside them.\n */\nexport const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [\n 'contactRoleContactIds',\n 'scopedContactRoleContactIds',\n 'contactRoleKeys',\n] as const\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS","CRM_DEAL_CONTACT_ROLE_LIST_FIELDS"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,8BAA8B,EAC9BC,uBAAuB,EAGvBC,aAAa,QACR,eAAc;AACrB,SAASC,YAAY,QAAQ,qCAAoC;AAGjE,SAASC,iBAAiB,QAAQ,+DAA8D;AAOhG;;;;;;;;;;;;;;;CAeC,GAED,mEAAmE,GACnE,OAAO,MAAMC,kBAA+D;IAC1EC,YAAYL;IACZM,QAAQJ,aAAaH;AACvB,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASQ,YAAYC,SAAmC;IAC7D,OAAOA,YAAY;QAAC;YAAEC,MAAM;YAAaC,IAAI;YAAsBC,OAAO;mBAAIH;aAAU;QAAC;KAAE,GAAG,EAAE;AAClG;AAEA,mFAAmF,GACnF,OAAO,MAAMI,sBAAsB;IAAC;QAAEH,MAAM;QAAaI,OAAO;IAAK;CAAE,CAAS;AAEhF;;;;;;;;;CASC,GACD,OAAO,SAASC,sBACdC,WAAiC,EACjCC,UAAmB;QAEAD;IAAnB,IAAIC,cAAc,GAACD,sBAAAA,YAAYE,MAAM,qBAAlBF,oBAAoBT,MAAM,GAAE,OAAOS;IACtD,OAAO,aAAKA;QAAaE,QAAQ;YAAEZ,YAAYU,YAAYE,MAAM,CAACZ,UAAU;QAAC;;AAC/E;AA2BA,qEAAqE,GACrE,OAAO,SAASa,cACdC,MAAoC,EACpCC,GAAkD;IAElD,MAAMC,UAA+B,EAAE;IACvC,MAAMC,UAA+B,EAAE;IACvC,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMC,UAAUL,OAAQ;QAC3B,MAAMM,QAAQL,IAAII;QAClB,IAAI,aAAaC,OAAO;YACtBF,QAAQG,IAAI,CAAC;gBAAEF;gBAAQG,QAAQF,MAAMF,OAAO;YAAC;QAC/C,OAAO;YACLF,QAAQK,IAAI,CAACD;YACbH,QAAQI,IAAI,CAACF;QACf;IACF;IACA,OAAO;QAAEH;QAASC;QAASC;IAAQ;AACrC;AAEA,MAAMK,aAAa,CAACC,GAAsBC,IACxCD,EAAEE,KAAK,KAAKD,EAAEC,KAAK,IAAIF,EAAEnB,EAAE,KAAKoB,EAAEpB,EAAE,IAAImB,EAAElB,KAAK,KAAKmB,EAAEnB,KAAK;AAE7D;;;;CAIC,GACD,OAAO,SAASqB,iBACdC,IAAoC,EACpCR,KAAsB,EACtBS,OAIC;IAED,MAAMC,UAAUF,KAAKV,OAAO,CAACa,GAAG,CAAC,CAACC;QAChC,IAAIA,MAAMb,MAAM,KAAK,UAAU,OAAOa;QACtC,MAAMb,SAASa,MAAMb,MAAM;QAC3B,MAAMc,KAAKb,MAAMJ,OAAO,CAACkB,SAAS,CAAC,CAACC,YAAcZ,WAAWY,WAAWhB;QACxE,OAAO;YAAEA,QAAQc,MAAM,IAAIb,MAAMH,OAAO,CAACgB,GAAG,GAAGd;YAAQG,QAAQU,MAAMV,MAAM;QAAC;IAC9E;IACA,OAAOxB,kBAAkB;WAAIsB,MAAMF,OAAO;WAAKY;KAAQ,EAAED;AAC3D;AAEA,mFAAmF,GACnF,OAAO,SAASO,gBAAgBjB,MAA+C;IAC7E,MAAMkB,MAAMlB,OAAOd,EAAE,KAAK,YAAYc,OAAOb,KAAK,CAACgC,KAAK,CAAC,OAAO;QAACnB,OAAOb,KAAK;KAAC;IAC9E,OAAO;WAAI,IAAIiC,IAAIF,IAAIN,GAAG,CAAC,CAACzB,QAAUA,MAAMkC,IAAI,IAAIC,MAAM,CAACC;KAAU;AACvE;AAEA,sFAAsF,GACtF,OAAO,SAASC,SAASjB,KAAa,EAAEkB,MAAyB;IAC/D,MAAMC,SAAS;WAAI,IAAIN,IAAIK;KAAQ;IACnC,IAAI,CAACC,OAAOC,MAAM,EAAE,OAAO;QAAE5B,SAAS;IAAe;IACrD,OAAO2B,OAAOC,MAAM,KAAK,IACrB;QAAEpB;QAAOrB,IAAI;QAAUC,OAAOuC,MAAM,CAAC,EAAE;IAAC,IACxC;QAAEnB;QAAOrB,IAAI;QAAWC,OAAOuC,OAAOE,IAAI,CAAC;IAAK;AACtD;AAEA,8EAA8E,GAC9E,OAAO,SAASC,eAAeC,MAAc,EAAE7C,OAAe6C,MAAM;IAClE,OAAO;QAAEA;QAAQC,MAAM;QAAS9C;QAAM+C,WAAW;YAAC;YAAU;SAAU;IAAC;AACzE;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,oBACdC,UAA6B,EAC7BC,MAAyC,EACzCC,KAAwC,EACxCC,MAAyB,EACzB3B,UAAgC,CAAC,CAAC;IAElC,MAAM4B,SAAkC,aAAKH;IAC7C,KAAK,MAAM,CAACI,KAAKpD,MAAM,IAAIqD,OAAOC,OAAO,CAACL,OAAQ;QAChD,wEAAwE;QACxE,IAAIG,IAAIG,QAAQ,CAAC,MAAM;QACvBJ,MAAM,CAACC,IAAI,GAAGpD,UAAU,QAAQwD,WAAWxD,SAASyD,YAAYzD;IAClE;IACA,MAAM0D,WAAWpE,cAAcyD,YAAYI,QAAQ5B;IACnD,OAAO8B,OAAOM,WAAW,CAACT,OAAOf,MAAM,CAAC,CAACf,QAAUA,SAASsC,UAAUjC,GAAG,CAAC,CAACL,QAAU;YAACA;YAAOsC,QAAQ,CAACtC,MAAM;SAAC;AAC/G;AAEA,2FAA2F,GAC3F,MAAMoC,aAAa,CAACxD,QAClB,OAAOA,UAAU,YACjBA,UAAU,QACV,CAAC4D,MAAMC,OAAO,CAAC7D,UACf,CAAEA,CAAAA,iBAAiB8D,IAAG,KACtB,OAAO,AAAC9D,MAAgC+D,OAAO,KAAK;AAEtD,+FAA+F,GAC/F,OAAO,MAAMC,2BAA2B;IAAC;IAAgB;CAAqB,CAAS;AAGvF;;;CAGC,GACD,OAAO,MAAMC,oCAAoC;IAC/C;IACA;IACA;CACD,CAAS"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-list-query.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 CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n type CrmListFieldsContext,\n crmListFields,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport { listQueryRefusals } from '@aglyn/shared-util-tools/list-query/list-query-refusals'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may. A lead's direction is read off the org's\n * lead source list, which the caller passes in `context` once it has it.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n context: CrmListFieldsContext = {},\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged, context)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n\n/**\n * The list fields a deal's contact roles are found by (AGL-3521) — what a\n * client write of `contactRoles` or `contactId` rewrites beside them.\n */\nexport const CRM_DEAL_CONTACT_ROLE_LIST_FIELDS = [\n 'contactRoleContactIds',\n 'scopedContactRoleContactIds',\n 'contactRoleKeys',\n] as const\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS","CRM_DEAL_CONTACT_ROLE_LIST_FIELDS"],"mappings":"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"}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* route is its door for client-direct task writes.
|
|
28
28
|
*/
|
|
29
29
|
import type { CrmDeal, CrmTask, CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm';
|
|
30
|
+
import type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
30
31
|
/** The field on a contact, a company and a deal — the one name every list reads. */
|
|
31
32
|
export declare const CRM_NEXT_ACTIVITY_FIELD = "nextTaskAtMs";
|
|
32
33
|
/**
|
|
@@ -105,3 +106,15 @@ export declare function stuckDeals<T extends Pick<CrmDeal, 'status' | 'nextTaskA
|
|
|
105
106
|
count: number;
|
|
106
107
|
deals: T[];
|
|
107
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
111
|
+
* Next activity column offers "is empty", which stores the clause
|
|
112
|
+
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
113
|
+
* reopens filtered the same way. Asked of the query as `nextTaskAtMs ==
|
|
114
|
+
* null` (AGL-3321): every contact, company and deal is created carrying
|
|
115
|
+
* `null`, and the task writers put `null` back when nothing is left
|
|
116
|
+
* scheduled.
|
|
117
|
+
*/
|
|
118
|
+
export declare const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField;
|
|
119
|
+
/** What the clause reads as on a chip. */
|
|
120
|
+
export declare const CRM_NEXT_ACTIVITY_FILTER_HEADER = "Next activity";
|
|
@@ -136,5 +136,24 @@ export function isNoNextActivityClause(clause) {
|
|
|
136
136
|
deals: stuck
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
141
|
+
* Next activity column offers "is empty", which stores the clause
|
|
142
|
+
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
143
|
+
* reopens filtered the same way. Asked of the query as `nextTaskAtMs ==
|
|
144
|
+
* null` (AGL-3321): every contact, company and deal is created carrying
|
|
145
|
+
* `null`, and the task writers put `null` back when nothing is left
|
|
146
|
+
* scheduled.
|
|
147
|
+
*/ export const CRM_NEXT_ACTIVITY_FILTER_FIELD = {
|
|
148
|
+
column: CRM_NEXT_ACTIVITY_FIELD,
|
|
149
|
+
kind: 'date',
|
|
150
|
+
path: CRM_NEXT_ACTIVITY_FIELD,
|
|
151
|
+
presence: 'nullable',
|
|
152
|
+
storedAs: 'millis',
|
|
153
|
+
operators: [
|
|
154
|
+
'isEmpty'
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
/** What the clause reads as on a chip. */ export const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity';
|
|
139
158
|
|
|
140
159
|
//# sourceMappingURL=crm-next-activity.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-next-activity.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/**\n * NEXT ACTIVITY (AGL-2661): when the earliest open task against a record\n * is due, denormalized onto the record as `nextTaskAtMs`.\n *\n * The question a sales manager asks of a LIST — \"which deals have nothing\n * scheduled?\" — cannot be answered by a task query per row, so every\n * contact, company and deal carries the answer. This module is the pure\n * half: what the value IS given a record's open tasks, how a stored one is\n * read back, the view clause a list filters on, and the report figure. The\n * admin library's `recomputeCrmNextTaskAt` is the writer; the console's\n * route is its door for client-direct task writes.\n */\n\nimport type { CrmDeal, CrmTask, CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm'\n\n/** The field on a contact, a company and a deal — the one name every list reads. */\nexport const CRM_NEXT_ACTIVITY_FIELD = 'nextTaskAtMs'\n\n/**\n * The records one task can be filed against — the ids a recompute reads\n * off a task, and the three collections it writes to.\n */\nexport interface CrmNextActivityLink {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/** How the collections are keyed on a task, in the order a recompute walks them. */\nexport const CRM_NEXT_ACTIVITY_LINK_FIELDS = ['contactId', 'companyId', 'dealId'] as const\n\nexport type CrmNextActivityLinkField = (typeof CRM_NEXT_ACTIVITY_LINK_FIELDS)[number]\n\n/**\n * When the earliest OPEN task in the list is due, or `null`.\n *\n * Only an open task with a finite due time counts: a done task is history,\n * and an open one with no date is a to-do with no \"next\" about it — the\n * record's next activity is unknown rather than immediate, and `null`\n * says so. A pre-change writer never produced a time outside the finite\n * range, but a stored `dueAtMs` is trusted no further than that.\n */\nexport function nextTaskAtMsOf(\n tasks: readonly Pick<CrmTask, 'status' | 'dueAtMs'>[],\n): number | null {\n let earliest: number | null = null\n for (const task of tasks) {\n if (task.status !== 'open') continue\n const due = task.dueAtMs\n if (typeof due !== 'number' || !Number.isFinite(due)) continue\n if (earliest === null || due < earliest) earliest = due\n }\n return earliest\n}\n\n/**\n * A stored `nextTaskAtMs`, read back: a finite positive number, else\n * `null`. Absent — a record written before the field existed — reads as\n * `null` too, because to every reader \"not yet computed\" and \"nothing\n * scheduled\" draw the same dash; the Fields section's recompute is what\n * closes the gap for a filter that asks the store rather than the row.\n */\nexport function readNextTaskAtMs(\n record: { nextTaskAtMs?: unknown } | null | undefined,\n): number | null {\n const value = record?.nextTaskAtMs\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\n/** Whether nothing is scheduled against the record — the \"stuck\" test. */\nexport function hasNoNextActivity(\n record: { nextTaskAtMs?: unknown } | null | undefined,\n): boolean {\n return readNextTaskAtMs(record) === null\n}\n\n/** The links one task carries, with the blanks left out. */\nexport function nextActivityLinksOfTask(\n task: Partial<Pick<CrmTask, CrmNextActivityLinkField>> | null | undefined,\n): CrmNextActivityLink {\n const link: CrmNextActivityLink = {}\n for (const field of CRM_NEXT_ACTIVITY_LINK_FIELDS) {\n const id = String(task?.[field] ?? '').trim()\n if (id) link[field] = id\n }\n return link\n}\n\n/**\n * Several links as one per record, so a bulk action over forty tasks on\n * one deal recomputes that deal once. Order is first-seen.\n */\nexport function mergeNextActivityLinks(\n links: readonly (CrmNextActivityLink | null | undefined)[],\n): Array<{ field: CrmNextActivityLinkField; id: string }> {\n const seen = new Set<string>()\n const merged: Array<{ field: CrmNextActivityLinkField; id: string }> = []\n for (const link of links) {\n for (const field of CRM_NEXT_ACTIVITY_LINK_FIELDS) {\n const id = String(link?.[field] ?? '').trim()\n if (!id) continue\n const key = `${field}:${id}`\n if (seen.has(key)) continue\n seen.add(key)\n merged.push({ field, id })\n }\n }\n return merged\n}\n\n/**\n * The saved-view clause \"No next activity\" is stored as — one clause,\n * spelled once, so the toggle a list draws and the matcher it runs agree\n * on the name. `isEmpty` is honest here: a recompute writes `null` for a\n * record with nothing scheduled, and a record never written reads blank\n * too (see `readNextTaskAtMs`).\n */\nexport const CRM_NO_NEXT_ACTIVITY_CLAUSE: Readonly<CrmViewFilterClause> = Object.freeze({\n field: CRM_NEXT_ACTIVITY_FIELD,\n op: 'isEmpty',\n value: '',\n})\n\nexport function isNoNextActivityClause(clause: Pick<CrmViewFilterClause, 'field' | 'op'>): boolean {\n return clause.field === CRM_NEXT_ACTIVITY_FIELD && clause.op === 'isEmpty'\n}\n\n/** A view's filters with the clause switched on or off, the rest untouched. */\nexport function withNoNextActivity(\n filters: readonly CrmViewFilterClause[],\n on: boolean,\n): CrmViewFilterClause[] {\n const rest = filters.filter((clause) => !isNoNextActivityClause(clause))\n return on ? [...rest, { ...CRM_NO_NEXT_ACTIVITY_CLAUSE }] : rest\n}\n\n/**\n * The rows a \"No next activity\" clause keeps: every one when the view does\n * not carry it, else those with nothing scheduled. What a list that\n * filters through a query — companies, deals — runs over the page it\n * loaded, the way the contacts list runs its window-only clauses.\n */\nexport function filterByNextActivity<T extends { nextTaskAtMs?: unknown }>(\n rows: readonly T[],\n filters: readonly CrmViewFilterClause[],\n): T[] {\n return filters.some(isNoNextActivityClause) ? rows.filter(hasNoNextActivity) : [...rows]\n}\n\n/**\n * The report figure: how many OPEN deals have nothing scheduled, and which\n * (AGL-2661). A won deal needs no next step and a lost one has none, so\n * only open deals are stuck; the caller hands in the window it already\n * reads for the pipeline card, so this costs no read of its own.\n */\nexport function stuckDeals<T extends Pick<CrmDeal, 'status' | 'nextTaskAtMs'>>(\n deals: readonly T[],\n): { count: number; deals: T[] } {\n const stuck = deals.filter((deal) => deal.status === 'open' && hasNoNextActivity(deal))\n return { count: stuck.length, deals: stuck }\n}\n"],"names":["CRM_NEXT_ACTIVITY_FIELD","CRM_NEXT_ACTIVITY_LINK_FIELDS","nextTaskAtMsOf","tasks","earliest","task","status","due","dueAtMs","Number","isFinite","readNextTaskAtMs","record","value","nextTaskAtMs","hasNoNextActivity","nextActivityLinksOfTask","link","field","id","String","trim","mergeNextActivityLinks","links","seen","Set","merged","key","has","add","push","CRM_NO_NEXT_ACTIVITY_CLAUSE","Object","freeze","op","isNoNextActivityClause","clause","withNoNextActivity","filters","on","rest","filter","filterByNextActivity","rows","some","stuckDeals","deals","stuck","deal","count","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC;AAID,kFAAkF,GAClF,OAAO,MAAMA,0BAA0B,eAAc;AAYrD,kFAAkF,GAClF,OAAO,MAAMC,gCAAgC;IAAC;IAAa;IAAa;CAAS,CAAS;AAI1F;;;;;;;;CAQC,GACD,OAAO,SAASC,eACdC,KAAqD;IAErD,IAAIC,WAA0B;IAC9B,KAAK,MAAMC,QAAQF,MAAO;QACxB,IAAIE,KAAKC,MAAM,KAAK,QAAQ;QAC5B,MAAMC,MAAMF,KAAKG,OAAO;QACxB,IAAI,OAAOD,QAAQ,YAAY,CAACE,OAAOC,QAAQ,CAACH,MAAM;QACtD,IAAIH,aAAa,QAAQG,MAAMH,UAAUA,WAAWG;IACtD;IACA,OAAOH;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASO,iBACdC,MAAqD;IAErD,MAAMC,QAAQD,0BAAAA,OAAQE,YAAY;IAClC,OAAO,OAAOD,UAAU,YAAYJ,OAAOC,QAAQ,CAACG,UAAUA,QAAQ,IAAIA,QAAQ;AACpF;AAEA,wEAAwE,GACxE,OAAO,SAASE,kBACdH,MAAqD;IAErD,OAAOD,iBAAiBC,YAAY;AACtC;AAEA,0DAA0D,GAC1D,OAAO,SAASI,wBACdX,IAAyE;IAEzE,MAAMY,OAA4B,CAAC;IACnC,KAAK,MAAMC,SAASjB,8BAA+B;;QACjD,MAAMkB,KAAKC,eAAOf,wBAAAA,IAAM,CAACa,MAAM,mBAAI,IAAIG,IAAI;QAC3C,IAAIF,IAAIF,IAAI,CAACC,MAAM,GAAGC;IACxB;IACA,OAAOF;AACT;AAEA;;;CAGC,GACD,OAAO,SAASK,uBACdC,KAA0D;IAE1D,MAAMC,OAAO,IAAIC;IACjB,MAAMC,SAAiE,EAAE;IACzE,KAAK,MAAMT,QAAQM,MAAO;QACxB,KAAK,MAAML,SAASjB,8BAA+B;;YACjD,MAAMkB,KAAKC,eAAOH,wBAAAA,IAAM,CAACC,MAAM,mBAAI,IAAIG,IAAI;YAC3C,IAAI,CAACF,IAAI;YACT,MAAMQ,MAAM,GAAGT,MAAM,CAAC,EAAEC,IAAI;YAC5B,IAAIK,KAAKI,GAAG,CAACD,MAAM;YACnBH,KAAKK,GAAG,CAACF;YACTD,OAAOI,IAAI,CAAC;gBAAEZ;gBAAOC;YAAG;QAC1B;IACF;IACA,OAAOO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,MAAMK,8BAA6DC,OAAOC,MAAM,CAAC;IACtFf,OAAOlB;IACPkC,IAAI;IACJrB,OAAO;AACT,GAAE;AAEF,OAAO,SAASsB,uBAAuBC,MAAiD;IACtF,OAAOA,OAAOlB,KAAK,KAAKlB,2BAA2BoC,OAAOF,EAAE,KAAK;AACnE;AAEA,6EAA6E,GAC7E,OAAO,SAASG,mBACdC,OAAuC,EACvCC,EAAW;IAEX,MAAMC,OAAOF,QAAQG,MAAM,CAAC,CAACL,SAAW,CAACD,uBAAuBC;IAChE,OAAOG,KAAK;WAAIC;QAAM,aAAKT;KAA8B,GAAGS;AAC9D;AAEA;;;;;CAKC,GACD,OAAO,SAASE,qBACdC,IAAkB,EAClBL,OAAuC;IAEvC,OAAOA,QAAQM,IAAI,CAACT,0BAA0BQ,KAAKF,MAAM,CAAC1B,qBAAqB;WAAI4B;KAAK;AAC1F;AAEA;;;;;CAKC,GACD,OAAO,SAASE,WACdC,KAAmB;IAEnB,MAAMC,QAAQD,MAAML,MAAM,CAAC,CAACO,OAASA,KAAK1C,MAAM,KAAK,UAAUS,kBAAkBiC;IACjF,OAAO;QAAEC,OAAOF,MAAMG,MAAM;QAAEJ,OAAOC;IAAM;AAC7C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-next-activity.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/**\n * NEXT ACTIVITY (AGL-2661): when the earliest open task against a record\n * is due, denormalized onto the record as `nextTaskAtMs`.\n *\n * The question a sales manager asks of a LIST — \"which deals have nothing\n * scheduled?\" — cannot be answered by a task query per row, so every\n * contact, company and deal carries the answer. This module is the pure\n * half: what the value IS given a record's open tasks, how a stored one is\n * read back, the view clause a list filters on, and the report figure. The\n * admin library's `recomputeCrmNextTaskAt` is the writer; the console's\n * route is its door for client-direct task writes.\n */\n\nimport type { CrmDeal, CrmTask, CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm'\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\n\n/** The field on a contact, a company and a deal — the one name every list reads. */\nexport const CRM_NEXT_ACTIVITY_FIELD = 'nextTaskAtMs'\n\n/**\n * The records one task can be filed against — the ids a recompute reads\n * off a task, and the three collections it writes to.\n */\nexport interface CrmNextActivityLink {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/** How the collections are keyed on a task, in the order a recompute walks them. */\nexport const CRM_NEXT_ACTIVITY_LINK_FIELDS = ['contactId', 'companyId', 'dealId'] as const\n\nexport type CrmNextActivityLinkField = (typeof CRM_NEXT_ACTIVITY_LINK_FIELDS)[number]\n\n/**\n * When the earliest OPEN task in the list is due, or `null`.\n *\n * Only an open task with a finite due time counts: a done task is history,\n * and an open one with no date is a to-do with no \"next\" about it — the\n * record's next activity is unknown rather than immediate, and `null`\n * says so. A pre-change writer never produced a time outside the finite\n * range, but a stored `dueAtMs` is trusted no further than that.\n */\nexport function nextTaskAtMsOf(\n tasks: readonly Pick<CrmTask, 'status' | 'dueAtMs'>[],\n): number | null {\n let earliest: number | null = null\n for (const task of tasks) {\n if (task.status !== 'open') continue\n const due = task.dueAtMs\n if (typeof due !== 'number' || !Number.isFinite(due)) continue\n if (earliest === null || due < earliest) earliest = due\n }\n return earliest\n}\n\n/**\n * A stored `nextTaskAtMs`, read back: a finite positive number, else\n * `null`. Absent — a record written before the field existed — reads as\n * `null` too, because to every reader \"not yet computed\" and \"nothing\n * scheduled\" draw the same dash; the Fields section's recompute is what\n * closes the gap for a filter that asks the store rather than the row.\n */\nexport function readNextTaskAtMs(\n record: { nextTaskAtMs?: unknown } | null | undefined,\n): number | null {\n const value = record?.nextTaskAtMs\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\n/** Whether nothing is scheduled against the record — the \"stuck\" test. */\nexport function hasNoNextActivity(\n record: { nextTaskAtMs?: unknown } | null | undefined,\n): boolean {\n return readNextTaskAtMs(record) === null\n}\n\n/** The links one task carries, with the blanks left out. */\nexport function nextActivityLinksOfTask(\n task: Partial<Pick<CrmTask, CrmNextActivityLinkField>> | null | undefined,\n): CrmNextActivityLink {\n const link: CrmNextActivityLink = {}\n for (const field of CRM_NEXT_ACTIVITY_LINK_FIELDS) {\n const id = String(task?.[field] ?? '').trim()\n if (id) link[field] = id\n }\n return link\n}\n\n/**\n * Several links as one per record, so a bulk action over forty tasks on\n * one deal recomputes that deal once. Order is first-seen.\n */\nexport function mergeNextActivityLinks(\n links: readonly (CrmNextActivityLink | null | undefined)[],\n): Array<{ field: CrmNextActivityLinkField; id: string }> {\n const seen = new Set<string>()\n const merged: Array<{ field: CrmNextActivityLinkField; id: string }> = []\n for (const link of links) {\n for (const field of CRM_NEXT_ACTIVITY_LINK_FIELDS) {\n const id = String(link?.[field] ?? '').trim()\n if (!id) continue\n const key = `${field}:${id}`\n if (seen.has(key)) continue\n seen.add(key)\n merged.push({ field, id })\n }\n }\n return merged\n}\n\n/**\n * The saved-view clause \"No next activity\" is stored as — one clause,\n * spelled once, so the toggle a list draws and the matcher it runs agree\n * on the name. `isEmpty` is honest here: a recompute writes `null` for a\n * record with nothing scheduled, and a record never written reads blank\n * too (see `readNextTaskAtMs`).\n */\nexport const CRM_NO_NEXT_ACTIVITY_CLAUSE: Readonly<CrmViewFilterClause> = Object.freeze({\n field: CRM_NEXT_ACTIVITY_FIELD,\n op: 'isEmpty',\n value: '',\n})\n\nexport function isNoNextActivityClause(clause: Pick<CrmViewFilterClause, 'field' | 'op'>): boolean {\n return clause.field === CRM_NEXT_ACTIVITY_FIELD && clause.op === 'isEmpty'\n}\n\n/** A view's filters with the clause switched on or off, the rest untouched. */\nexport function withNoNextActivity(\n filters: readonly CrmViewFilterClause[],\n on: boolean,\n): CrmViewFilterClause[] {\n const rest = filters.filter((clause) => !isNoNextActivityClause(clause))\n return on ? [...rest, { ...CRM_NO_NEXT_ACTIVITY_CLAUSE }] : rest\n}\n\n/**\n * The rows a \"No next activity\" clause keeps: every one when the view does\n * not carry it, else those with nothing scheduled. What a list that\n * filters through a query — companies, deals — runs over the page it\n * loaded, the way the contacts list runs its window-only clauses.\n */\nexport function filterByNextActivity<T extends { nextTaskAtMs?: unknown }>(\n rows: readonly T[],\n filters: readonly CrmViewFilterClause[],\n): T[] {\n return filters.some(isNoNextActivityClause) ? rows.filter(hasNoNextActivity) : [...rows]\n}\n\n/**\n * The report figure: how many OPEN deals have nothing scheduled, and which\n * (AGL-2661). A won deal needs no next step and a lost one has none, so\n * only open deals are stuck; the caller hands in the window it already\n * reads for the pipeline card, so this costs no read of its own.\n */\nexport function stuckDeals<T extends Pick<CrmDeal, 'status' | 'nextTaskAtMs'>>(\n deals: readonly T[],\n): { count: number; deals: T[] } {\n const stuck = deals.filter((deal) => deal.status === 'open' && hasNoNextActivity(deal))\n return { count: stuck.length, deals: stuck }\n}\n\n/**\n * \"No next activity\" as a filter of the grid's own panel (AGL-3313): the\n * Next activity column offers \"is empty\", which stores the clause\n * `isNoNextActivityClause` names, so a view saved with the old toggle on\n * reopens filtered the same way. Asked of the query as `nextTaskAtMs ==\n * null` (AGL-3321): every contact, company and deal is created carrying\n * `null`, and the task writers put `null` back when nothing is left\n * scheduled.\n */\nexport const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField = {\n column: CRM_NEXT_ACTIVITY_FIELD,\n kind: 'date',\n path: CRM_NEXT_ACTIVITY_FIELD,\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n}\n\n/** What the clause reads as on a chip. */\nexport const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity'\n"],"names":["CRM_NEXT_ACTIVITY_FIELD","CRM_NEXT_ACTIVITY_LINK_FIELDS","nextTaskAtMsOf","tasks","earliest","task","status","due","dueAtMs","Number","isFinite","readNextTaskAtMs","record","value","nextTaskAtMs","hasNoNextActivity","nextActivityLinksOfTask","link","field","id","String","trim","mergeNextActivityLinks","links","seen","Set","merged","key","has","add","push","CRM_NO_NEXT_ACTIVITY_CLAUSE","Object","freeze","op","isNoNextActivityClause","clause","withNoNextActivity","filters","on","rest","filter","filterByNextActivity","rows","some","stuckDeals","deals","stuck","deal","count","length","CRM_NEXT_ACTIVITY_FILTER_FIELD","column","kind","path","presence","storedAs","operators","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"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"}
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type
|
|
18
|
-
import type
|
|
19
|
-
import type {
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
17
|
+
import { type CrmLeadStatus, type CrmPicklist } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import { type EmailStateStatus } from '@aglyn/aglyn/app-utils/email-state';
|
|
19
|
+
import type { CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm';
|
|
20
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
21
|
+
import { type ListGridFilterCodec } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
|
|
22
|
+
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
22
23
|
import { type CrmClauseAsked } from './crm-list-query';
|
|
23
24
|
/**
|
|
24
25
|
* What the Leads section's `Show` control offers (AGL-2608): the two
|
|
@@ -14,9 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { CRM_LEAD_STATUS_LABELS, CRM_LEAD_OPEN_STATUSES, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_SOURCE_DIRECTION_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey, crmPicklistKey } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES } from "@aglyn/aglyn/app-utils/email-state";
|
|
18
19
|
import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
|
|
19
|
-
import { listSelectCodec } from "@aglyn/shared-
|
|
20
|
+
import { listSelectCodec } from "@aglyn/shared-util-tools/list-query/list-filter-codecs";
|
|
20
21
|
import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
|
|
21
22
|
import { isLeadSourceDirection, LEAD_SOURCE_DIRECTION_LABELS, LEAD_SOURCE_DIRECTIONS } from "./lead-source-direction.js";
|
|
22
23
|
export const LEAD_FILTERS = [
|