@aglyn/plugins-crm 1.0.0-beta.229 → 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/deal-figures.d.ts +47 -0
- package/src/lib/server/deal-figures.js +275 -0
- package/src/lib/server/deal-figures.js.map +1 -0
- 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
- package/src/lib/server.d.ts +0 -38
- package/src/lib/server.js +5 -1
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_SOURCE_DIRECTION_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n type CrmPicklist,\n crmLeadSourceKey,\n crmPicklistKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\nimport {\n isLeadSourceDirection,\n LEAD_SOURCE_DIRECTION_LABELS,\n LEAD_SOURCE_DIRECTIONS,\n} from './lead-source-direction'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as\n * `status in [new, nurturing, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'nurturing',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n nurturing: CRM_LEAD_STATUS_LABELS.nurturing,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n * leadSourceDirection `equals inbound|outbound` (AGL-3511): every lead\n * whose lead source is a value of that group\n * industry, rating `equals <key>` (AGL-3513): a value of the org's\n * Industry or Rating list, by the key the query compares\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n {\n column: 'leadSourceDirection',\n kind: 'exact',\n path: 'leadSourceDirection',\n operators: ['equals', 'isAnyOf'],\n },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n // Salesforce's Industry and Rating (AGL-3513), the lists companies keep.\n { column: 'industry', kind: 'exact', path: 'industry', operators: ['equals', 'isAnyOf'] },\n { column: 'rating', kind: 'exact', path: 'rating', operators: ['equals', 'isAnyOf'] },\n]\n\n/**\n * The lead picklist columns the list filters by (AGL-3513), each with the\n * list its choices come from: a choice's value is the KEY the query\n * compares, its caption the label.\n */\nexport const LEAD_PICKLIST_FILTERS = [\n { column: 'industry', picklistId: 'industry', header: 'Industry' },\n { column: 'rating', picklistId: 'rating', header: 'Rating' },\n] as const\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n leadSourceDirection: 'Lead source direction',\n campaignIds: 'Campaign',\n industry: 'Industry',\n rating: 'Rating',\n}\n\n/** The choices for Status: Open (new, nurturing or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new, nurturing or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\n/** The choices for Lead source direction: each group of the Lead source picklist. */\nexport const LEAD_SOURCE_DIRECTION_FILTER_OPTIONS = LEAD_SOURCE_DIRECTIONS.map((direction) => ({\n value: direction,\n label: LEAD_SOURCE_DIRECTION_LABELS[direction],\n}))\n\n/**\n * The Status choices as an org reads them (AGL-3512): still one choice per\n * MEANING — the query asks `status` — each named by the org's values of that\n * meaning, so a list that keeps \"Contacted\" and \"Meeting set\" as Working\n * reads \"Working: Contacted, Meeting set\".\n */\nexport function leadStatusFilterOptions(\n picklist: CrmPicklist,\n): Array<{ value: string; label: string }> {\n const named = (status: CrmLeadStatus): string => {\n const labels = picklist.values\n .filter((value) => value.meaning === status && value.active)\n .map((value) => value.label)\n const meaning = picklistMeaningName(status)\n if (!labels.length) return meaning\n if (labels.length === 1 && labels[0] === meaning) return meaning\n return `${meaning}: ${labels.join(', ')}`\n }\n return LEAD_FILTERS.filter((option) => option !== 'all').map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new, nurturing or working)' : named(option),\n }))\n}\n\nconst picklistMeaningName = (status: CrmLeadStatus): string => CRM_LEAD_STATUS_LABELS[status]\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, nurturing, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * leadSourceDirection `leadSourceDirection` (AGL-3577), the group of\n * the lead's value in the org's list, stamped by the writers\n * from that list and rewritten on every lead holding a value\n * the list regroups — so one equality asks a group however\n * many values it holds\n * ownerUid as stored\n * industry, rating `industryKey`, `ratingKey` (AGL-3513), each label\n * as the picklist compares it\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n // The lead source's group (AGL-3577), as stamped on the lead.\n crmSelectField(CRM_LEAD_SOURCE_DIRECTION_FIELD),\n // Industry and Rating (AGL-3513), by the key each writer stores.\n crmSelectField('industryKey'),\n crmSelectField('ratingKey'),\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push(...CRM_LEAD_OPEN_STATUSES)\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'leadSourceDirection': {\n for (const value of values) {\n if (!isLeadSourceDirection(value)) return { refused: `${value} is not a lead source direction` }\n }\n return crmAnyOf(CRM_LEAD_SOURCE_DIRECTION_FIELD, values)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'industry':\n case 'rating': {\n // A choice is the key; a label typed into a stored view keys the same way.\n const keys = values.map((value) => crmPicklistKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf(clause.field === 'industry' ? 'industryKey' : 'ratingKey', keys)\n }\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_SOURCE_DIRECTION_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","crmPicklistKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","isLeadSourceDirection","LEAD_SOURCE_DIRECTION_LABELS","LEAD_SOURCE_DIRECTIONS","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","nurturing","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_PICKLIST_FILTERS","picklistId","header","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","leadSourceDirection","campaignIds","industry","rating","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","LEAD_SOURCE_DIRECTION_FILTER_OPTIONS","direction","leadStatusFilterOptions","picklist","named","labels","values","meaning","active","picklistMeaningName","length","join","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","statuses","push","includes","refused","keys","key","Boolean","token","leadIndexShapes","equalities"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_SOURCE_DIRECTION_FIELD,\n CRM_LEAD_STATUSES,\n type CrmLeadStatus,\n type CrmPicklist,\n crmLeadSourceKey,\n crmPicklistKey,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, type EmailStateStatus } from '@aglyn/aglyn/app-utils/email-state'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\nimport {\n isLeadSourceDirection,\n LEAD_SOURCE_DIRECTION_LABELS,\n LEAD_SOURCE_DIRECTIONS,\n} from './lead-source-direction'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as\n * `status in [new, nurturing, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'nurturing',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n nurturing: CRM_LEAD_STATUS_LABELS.nurturing,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n * leadSourceDirection `equals inbound|outbound` (AGL-3511): every lead\n * whose lead source is a value of that group\n * industry, rating `equals <key>` (AGL-3513): a value of the org's\n * Industry or Rating list, by the key the query compares\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n {\n column: 'leadSourceDirection',\n kind: 'exact',\n path: 'leadSourceDirection',\n operators: ['equals', 'isAnyOf'],\n },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n // Salesforce's Industry and Rating (AGL-3513), the lists companies keep.\n { column: 'industry', kind: 'exact', path: 'industry', operators: ['equals', 'isAnyOf'] },\n { column: 'rating', kind: 'exact', path: 'rating', operators: ['equals', 'isAnyOf'] },\n]\n\n/**\n * The lead picklist columns the list filters by (AGL-3513), each with the\n * list its choices come from: a choice's value is the KEY the query\n * compares, its caption the label.\n */\nexport const LEAD_PICKLIST_FILTERS = [\n { column: 'industry', picklistId: 'industry', header: 'Industry' },\n { column: 'rating', picklistId: 'rating', header: 'Rating' },\n] as const\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n leadSourceDirection: 'Lead source direction',\n campaignIds: 'Campaign',\n industry: 'Industry',\n rating: 'Rating',\n}\n\n/** The choices for Status: Open (new, nurturing or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new, nurturing or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\n/** The choices for Lead source direction: each group of the Lead source picklist. */\nexport const LEAD_SOURCE_DIRECTION_FILTER_OPTIONS = LEAD_SOURCE_DIRECTIONS.map((direction) => ({\n value: direction,\n label: LEAD_SOURCE_DIRECTION_LABELS[direction],\n}))\n\n/**\n * The Status choices as an org reads them (AGL-3512): still one choice per\n * MEANING — the query asks `status` — each named by the org's values of that\n * meaning, so a list that keeps \"Contacted\" and \"Meeting set\" as Working\n * reads \"Working: Contacted, Meeting set\".\n */\nexport function leadStatusFilterOptions(\n picklist: CrmPicklist,\n): Array<{ value: string; label: string }> {\n const named = (status: CrmLeadStatus): string => {\n const labels = picklist.values\n .filter((value) => value.meaning === status && value.active)\n .map((value) => value.label)\n const meaning = picklistMeaningName(status)\n if (!labels.length) return meaning\n if (labels.length === 1 && labels[0] === meaning) return meaning\n return `${meaning}: ${labels.join(', ')}`\n }\n return LEAD_FILTERS.filter((option) => option !== 'all').map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new, nurturing or working)' : named(option),\n }))\n}\n\nconst picklistMeaningName = (status: CrmLeadStatus): string => CRM_LEAD_STATUS_LABELS[status]\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, nurturing, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * leadSourceDirection `leadSourceDirection` (AGL-3577), the group of\n * the lead's value in the org's list, stamped by the writers\n * from that list and rewritten on every lead holding a value\n * the list regroups — so one equality asks a group however\n * many values it holds\n * ownerUid as stored\n * industry, rating `industryKey`, `ratingKey` (AGL-3513), each label\n * as the picklist compares it\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n // The lead source's group (AGL-3577), as stamped on the lead.\n crmSelectField(CRM_LEAD_SOURCE_DIRECTION_FIELD),\n // Industry and Rating (AGL-3513), by the key each writer stores.\n crmSelectField('industryKey'),\n crmSelectField('ratingKey'),\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push(...CRM_LEAD_OPEN_STATUSES)\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'leadSourceDirection': {\n for (const value of values) {\n if (!isLeadSourceDirection(value)) return { refused: `${value} is not a lead source direction` }\n }\n return crmAnyOf(CRM_LEAD_SOURCE_DIRECTION_FIELD, values)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'industry':\n case 'rating': {\n // A choice is the key; a label typed into a stored view keys the same way.\n const keys = values.map((value) => crmPicklistKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf(clause.field === 'industry' ? 'industryKey' : 'ratingKey', keys)\n }\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["CRM_LEAD_STATUS_LABELS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_SOURCE_DIRECTION_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","crmPicklistKey","EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","isLeadSourceDirection","LEAD_SOURCE_DIRECTION_LABELS","LEAD_SOURCE_DIRECTIONS","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","nurturing","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_PICKLIST_FILTERS","picklistId","header","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","leadSourceDirection","campaignIds","industry","rating","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","LEAD_SOURCE_DIRECTION_FILTER_OPTIONS","direction","leadStatusFilterOptions","picklist","named","labels","values","meaning","active","picklistMeaningName","length","join","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","statuses","push","includes","refused","keys","key","Boolean","token","leadIndexShapes","equalities"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmLeadFields, type CrmLeadStatus, type CrmPicklist } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
/**
|
|
19
|
+
* The meanings a person sets by hand from a lead's status select: every one
|
|
20
|
+
* but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
|
|
21
|
+
*/
|
|
22
|
+
export declare const LEAD_STATUS_HAND_SET: readonly CrmLeadStatus[];
|
|
23
|
+
/** One choice of a lead status select: the org's label, and the meaning it sets. */
|
|
24
|
+
export interface LeadStatusChoice {
|
|
25
|
+
label: string;
|
|
26
|
+
status: CrmLeadStatus;
|
|
27
|
+
inactive: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */
|
|
30
|
+
export declare function leadStatusChoices(picklist: CrmPicklist, lead: Pick<CrmLeadFields, 'status' | 'statusLabel'>): LeadStatusChoice[];
|
|
31
|
+
/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */
|
|
32
|
+
export declare const UNQUALIFY_REASON_MAX = 500;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { crmLeadStatusOptions } from "@aglyn/aglyn/app-utils/crm";
|
|
17
|
+
/**
|
|
18
|
+
* The meanings a person sets by hand from a lead's status select: every one
|
|
19
|
+
* but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
|
|
20
|
+
*/ export const LEAD_STATUS_HAND_SET = [
|
|
21
|
+
'new',
|
|
22
|
+
'nurturing',
|
|
23
|
+
'working',
|
|
24
|
+
'unqualified'
|
|
25
|
+
];
|
|
26
|
+
/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */ export function leadStatusChoices(picklist, lead) {
|
|
27
|
+
return crmLeadStatusOptions(picklist, LEAD_STATUS_HAND_SET, lead);
|
|
28
|
+
}
|
|
29
|
+
/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */ export const UNQUALIFY_REASON_MAX = 500;
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=lead-status-choices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-status-choices.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 */\nimport {\n type CrmLeadFields,\n type CrmLeadStatus,\n type CrmPicklist,\n crmLeadStatusOptions,\n} from '@aglyn/aglyn/app-utils/crm'\n\n/**\n * The meanings a person sets by hand from a lead's status select: every one\n * but Qualified, which only a conversion sets (AGL-2608, AGL-3512).\n */\nexport const LEAD_STATUS_HAND_SET: readonly CrmLeadStatus[] = [\n 'new',\n 'nurturing',\n 'working',\n 'unqualified',\n]\n\n/** One choice of a lead status select: the org's label, and the meaning it sets. */\nexport interface LeadStatusChoice {\n label: string\n status: CrmLeadStatus\n inactive: boolean\n}\n\n/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */\nexport function leadStatusChoices(\n picklist: CrmPicklist,\n lead: Pick<CrmLeadFields, 'status' | 'statusLabel'>,\n): LeadStatusChoice[] {\n return crmLeadStatusOptions(picklist, LEAD_STATUS_HAND_SET, lead)\n}\n\n/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */\nexport const UNQUALIFY_REASON_MAX = 500\n"],"names":["crmLeadStatusOptions","LEAD_STATUS_HAND_SET","leadStatusChoices","picklist","lead","UNQUALIFY_REASON_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,SAIEA,oBAAoB,QACf,6BAA4B;AAEnC;;;CAGC,GACD,OAAO,MAAMC,uBAAiD;IAC5D;IACA;IACA;IACA;CACD,CAAA;AASD,4GAA4G,GAC5G,OAAO,SAASC,kBACdC,QAAqB,EACrBC,IAAmD;IAEnD,OAAOJ,qBAAqBG,UAAUF,sBAAsBG;AAC9D;AAEA,0FAA0F,GAC1F,OAAO,MAAMC,uBAAuB,IAAG"}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type CrmTask, type CrmTaskKind, type CrmTaskPriority, type TaskDueState } from '@aglyn/aglyn';
|
|
18
|
-
import type { ListFilterField } from '@aglyn/shared-
|
|
19
|
-
import type { ListQueryDeclaration, ListQueryFilter, ListQuerySort } from '@aglyn/shared-
|
|
18
|
+
import type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
19
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
20
20
|
import type { CrmRoutes } from './crm-routes';
|
|
21
21
|
/**
|
|
22
22
|
* The tasks section's views and the query each one is (AGL-2599).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/task-views.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type CrmTask,\n type CrmTaskKind,\n type CrmTaskPriority,\n type TaskDueState,\n taskDueState,\n} from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { CRM_LIST_SEARCH, crmSelectField } from './crm-list-query'\nimport type { CrmRoutes } from './crm-routes'\n\n/**\n * The tasks section's views and the query each one is (AGL-2599).\n *\n * Pure: what a view MEANS is written here once, as a plan the hook turns into\n * Firestore constraints and a spec can read back without Firestore. \"Overdue\"\n * and \"today\" are not stored on the task — nothing runs at midnight to stamp\n * them — so each view is a window over `dueAtMs` computed at read time from\n * the reader's own clock, and the same day boundaries {@link taskDueState}\n * paints a row with.\n */\n\nexport type CrmTaskView =\n | 'mine'\n | 'overdue'\n | 'today'\n | 'upcoming'\n | 'open'\n | 'done'\n\nexport const CRM_TASK_VIEWS: ReadonlyArray<{ id: CrmTaskView; label: string }> =\n [\n { id: 'mine', label: 'My tasks' },\n { id: 'overdue', label: 'Overdue' },\n { id: 'today', label: 'Today' },\n { id: 'upcoming', label: 'Upcoming' },\n { id: 'open', label: 'All open' },\n { id: 'done', label: 'Done' },\n ]\n\nexport const CRM_TASK_KINDS: readonly CrmTaskKind[] = [\n 'call',\n 'email',\n 'meeting',\n 'todo',\n]\n\nexport const CRM_TASK_KIND_LABELS: Record<CrmTaskKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n todo: 'To-do',\n}\n\nexport const CRM_TASK_PRIORITIES: readonly CrmTaskPriority[] = [\n 'low',\n 'normal',\n 'high',\n]\n\nexport const CRM_TASK_PRIORITY_LABELS: Record<CrmTaskPriority, string> = {\n low: 'Low',\n normal: 'Normal',\n high: 'High',\n}\n\n\n/** Midnight at the START of the local calendar day `nowMs` falls in. */\nexport function startOfLocalDay(nowMs: number): number {\n const date = new Date(nowMs)\n return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()\n}\n\n/**\n * Midnight at the start of the NEXT local day — built from the calendar\n * rather than by adding 24 hours, because a day that a clock change falls in\n * is 23 or 25 hours long and a task due at 00:30 on that morning belongs to\n * it either way.\n */\nexport function startOfNextLocalDay(nowMs: number): number {\n const date = new Date(nowMs)\n return new Date(\n date.getFullYear(),\n date.getMonth(),\n date.getDate() + 1,\n ).getTime()\n}\n\n/**\n * One view as constraints, in the vocabulary of the `crmTasks` indexes.\n *\n * `assigneeUid` narrows to the reader and rides the\n * `(visibleTo, assigneeUid, status, dueAtMs)` index; every other view rides\n * `(visibleTo, status, dueAtMs)`. `dueFrom` is inclusive and `dueBefore`\n * exclusive so that two adjacent windows share a boundary and no task falls\n * between \"today\" and \"upcoming\". A task with no due date has `dueAtMs:\n * null`, which a range excludes and a bare order sorts first, so it appears\n * in \"My tasks\" and \"All open\" and in none of the dated views.\n */\nexport interface CrmTaskViewPlan {\n status: 'open' | 'done'\n assigneeUid?: string\n dueFrom?: number\n dueBefore?: number\n direction: 'asc' | 'desc'\n}\n\nexport function crmTaskViewPlan(\n view: CrmTaskView,\n options: { nowMs: number; uid: string | null | undefined },\n): CrmTaskViewPlan {\n const { nowMs, uid } = options\n const today = startOfLocalDay(nowMs)\n const tomorrow = startOfNextLocalDay(nowMs)\n switch (view) {\n case 'mine':\n return { status: 'open', assigneeUid: uid ?? '', direction: 'asc' }\n case 'overdue':\n return { status: 'open', dueBefore: today, direction: 'asc' }\n case 'today':\n return { status: 'open', dueFrom: today, dueBefore: tomorrow, direction: 'asc' }\n case 'upcoming':\n return { status: 'open', dueFrom: tomorrow, direction: 'asc' }\n case 'open':\n return { status: 'open', direction: 'asc' }\n case 'done':\n // Most recently due first: what was finished lately is what a reader\n // scrolls the Done view for, and the oldest completed task is the one\n // nobody comes back to.\n return { status: 'done', direction: 'desc' }\n }\n}\n\n/*==========================================\n * THE TASKS LIST'S QUERY (AGL-3321).\n *\n * The view is the query's base — its status, the reader as assignee for\n * \"My tasks\", and the `dueAtMs` window for Overdue, Today and Upcoming —\n * and Kind, Priority, Assignee and the search word are predicates on the\n * same query, paged by it. Every equality rides one `(field, dueAtMs)`\n * composite per direction: ascending for the working views, descending for\n * Done.\n *=========================================*/\n\n/** The orders the list takes: soonest first, and Done's most recently due first. */\nexport const TASK_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'dueAtMs', direction: 'asc' },\n { path: 'dueAtMs', direction: 'desc' },\n]\n\n/** What the Filters panel may name on a task, besides the view. */\nexport const TASK_LIST_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('kind'),\n crmSelectField('priority'),\n crmSelectField('assigneeUid'),\n]\n\nexport const TASK_LIST_DECLARATION: ListQueryDeclaration = {\n fields: TASK_LIST_QUERY_FIELDS,\n sorts: TASK_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The view's own predicates, which the plan takes as its base beside the scope. */\nexport function crmTaskViewBase(plan: CrmTaskViewPlan): ListQueryFilter[] {\n return [\n { path: 'status', op: '==', value: plan.status },\n ...(plan.assigneeUid ? [{ path: 'assigneeUid', op: '==' as const, value: plan.assigneeUid }] : []),\n ...(plan.dueFrom !== undefined ? [{ path: 'dueAtMs', op: '>=' as const, value: plan.dueFrom }] : []),\n ...(plan.dueBefore !== undefined ? [{ path: 'dueAtMs', op: '<' as const, value: plan.dueBefore }] : []),\n ]\n}\n\n/** The base's shape for `listQueryIndexes`: the view's two equalities. */\nexport const TASK_LIST_VIEW_BASE_INDEX = [{ path: 'status' }, { path: 'assigneeUid' }] as const\n\n/**\n * Rows in the order a view should paint them.\n *\n * Firestore sorts `null` before every number, so an ascending order by\n * `dueAtMs` puts the undated tasks at the TOP of \"All open\" — above the one\n * due in an hour. The query stays as it is (the index decides that); the\n * undated rows are moved to the end here, in a stable pass that leaves the\n * dated order the server chose alone.\n */\nexport function orderTaskRows<T extends Pick<CrmTask, 'dueAtMs'>>(\n rows: readonly T[],\n): T[] {\n const dated: T[] = []\n const undated: T[] = []\n for (const row of rows) {\n ;(typeof row.dueAtMs === 'number' ? dated : undated).push(row)\n }\n return [...dated, ...undated]\n}\n\n/** The MUI palette key a due date is painted with, by its state. */\nexport const TASK_DUE_COLORS: Record<TaskDueState, string> = {\n overdue: 'error.main',\n today: 'warning.main',\n upcoming: 'text.primary',\n none: 'text.secondary',\n done: 'text.secondary',\n}\n\n/**\n * The due date as a person reads it: \"Overdue · Tue, Sep 1, 9:00 AM\",\n * \"Today, 3:30 PM\", \"No due date\". The state word comes first because it is\n * the part a reader scans a column for; the timestamp is what they read once\n * a row has their attention.\n */\nexport function describeTaskDue(\n task: Pick<CrmTask, 'status' | 'dueAtMs'>,\n nowMs: number,\n locale?: string,\n): { state: TaskDueState; label: string } {\n const state = taskDueState(task, nowMs)\n const dueAtMs = task.dueAtMs\n if (typeof dueAtMs !== 'number' || !Number.isFinite(dueAtMs)) {\n return { state, label: 'No due date' }\n }\n const due = new Date(dueAtMs)\n const time = due.toLocaleTimeString(locale, {\n hour: 'numeric',\n minute: '2-digit',\n })\n switch (state) {\n case 'today':\n return { state, label: `Today, ${time}` }\n case 'overdue':\n return {\n state,\n label: `Overdue · ${due.toLocaleDateString(locale, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n })}, ${time}`,\n }\n default:\n return {\n state,\n label: `${due.toLocaleDateString(locale, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n })}, ${time}`,\n }\n }\n}\n\n/**\n * `datetime-local` ↔ epoch milliseconds, in the reader's own zone.\n *\n * The input's value is a wall-clock string with no zone; `new Date(string)`\n * reads such a string as LOCAL time, which is what a person typing \"9:00\"\n * into a due-date field means. Built by hand in the other direction because\n * `toISOString()` would render the UTC clock, and a task due at nine would\n * reopen in the drawer at whatever nine is in Greenwich.\n */\nexport function dueAtToLocalInput(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-` +\n `${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`\n )\n}\n\nexport function localInputToDueAt(value: string): number | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n const ms = new Date(text).getTime()\n return Number.isFinite(ms) ? ms : null\n}\n\n/**\n * The two snoozes a row and the drawer offer by name (AGL-2619), in the\n * order a person reaches for them; \"Pick a date\" is the third and takes a\n * date rather than an option.\n */\nexport type CrmTaskSnoozeOption = 'tomorrow' | 'nextWeek'\n\nexport const CRM_TASK_SNOOZE_OPTIONS: ReadonlyArray<{\n id: CrmTaskSnoozeOption\n label: string\n}> = [\n { id: 'tomorrow', label: 'Tomorrow' },\n { id: 'nextWeek', label: 'Next week' },\n]\n\n/** The hour a snoozed task lands on when it had no time of day of its own. */\nexport const CRM_TASK_SNOOZE_DEFAULT_HOUR = 9\n\n/**\n * Where a snoozed task is due next.\n *\n * Counted from NOW, not from the old due date: a task a week overdue\n * snoozed to \"tomorrow\" is due tomorrow, which is what the word means to the\n * person pressing it, and not due six days ago. The time of day is kept —\n * a nine o'clock call stays a nine o'clock call — and a task that had no\n * time lands at {@link CRM_TASK_SNOOZE_DEFAULT_HOUR}, a working hour rather\n * than midnight. Built from the calendar for the reason\n * {@link startOfNextLocalDay} gives.\n */\nexport function snoozeDueAt(\n option: CrmTaskSnoozeOption,\n dueAtMs: number | null | undefined,\n nowMs: number,\n): number {\n const had = typeof dueAtMs === 'number' && Number.isFinite(dueAtMs) ? new Date(dueAtMs) : null\n const hours = had ? had.getHours() : CRM_TASK_SNOOZE_DEFAULT_HOUR\n const minutes = had ? had.getMinutes() : 0\n const now = new Date(nowMs)\n const days = option === 'tomorrow' ? 1 : 7\n return new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate() + days,\n hours,\n minutes,\n ).getTime()\n}\n\n/**\n * The record a task hangs off, as a link into the hub — or `null` for a\n * task that is nobody's in particular.\n *\n * One record per task on screen even when the document names more than one:\n * the contact wins, then the deal, then the company, because that is the\n * order of how specific each is about WHO the work is for. All three ids\n * stay on the document for the record pages' own queries.\n */\nexport function taskRecordLink(\n task: Pick<CrmTask, 'contactId' | 'companyId' | 'dealId'>,\n routes: CrmRoutes,\n): { kind: 'contact' | 'deal' | 'company'; id: string; href: string } | null {\n if (task.contactId) {\n return { kind: 'contact', id: task.contactId, href: routes.contact(task.contactId) }\n }\n if (task.dealId) {\n return { kind: 'deal', id: task.dealId, href: routes.deal(task.dealId) }\n }\n if (task.companyId) {\n return { kind: 'company', id: task.companyId, href: routes.company(task.companyId) }\n }\n return null\n}\n"],"names":["taskDueState","CRM_LIST_SEARCH","crmSelectField","CRM_TASK_VIEWS","id","label","CRM_TASK_KINDS","CRM_TASK_KIND_LABELS","call","email","meeting","todo","CRM_TASK_PRIORITIES","CRM_TASK_PRIORITY_LABELS","low","normal","high","startOfLocalDay","nowMs","date","Date","getFullYear","getMonth","getDate","getTime","startOfNextLocalDay","crmTaskViewPlan","view","options","uid","today","tomorrow","status","assigneeUid","direction","dueBefore","dueFrom","TASK_LIST_SORTS","path","TASK_LIST_QUERY_FIELDS","TASK_LIST_DECLARATION","fields","sorts","search","crmTaskViewBase","plan","op","value","undefined","TASK_LIST_VIEW_BASE_INDEX","orderTaskRows","rows","dated","undated","row","dueAtMs","push","TASK_DUE_COLORS","overdue","upcoming","none","done","describeTaskDue","task","locale","state","Number","isFinite","due","time","toLocaleTimeString","hour","minute","toLocaleDateString","weekday","month","day","dueAtToLocalInput","ms","pad","String","padStart","getHours","getMinutes","localInputToDueAt","text","trim","CRM_TASK_SNOOZE_OPTIONS","CRM_TASK_SNOOZE_DEFAULT_HOUR","snoozeDueAt","option","had","hours","minutes","now","days","taskRecordLink","routes","contactId","kind","href","contact","dealId","deal","companyId","company"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/task-views.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type CrmTask,\n type CrmTaskKind,\n type CrmTaskPriority,\n type TaskDueState,\n taskDueState,\n} from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { CRM_LIST_SEARCH, crmSelectField } from './crm-list-query'\nimport type { CrmRoutes } from './crm-routes'\n\n/**\n * The tasks section's views and the query each one is (AGL-2599).\n *\n * Pure: what a view MEANS is written here once, as a plan the hook turns into\n * Firestore constraints and a spec can read back without Firestore. \"Overdue\"\n * and \"today\" are not stored on the task — nothing runs at midnight to stamp\n * them — so each view is a window over `dueAtMs` computed at read time from\n * the reader's own clock, and the same day boundaries {@link taskDueState}\n * paints a row with.\n */\n\nexport type CrmTaskView =\n | 'mine'\n | 'overdue'\n | 'today'\n | 'upcoming'\n | 'open'\n | 'done'\n\nexport const CRM_TASK_VIEWS: ReadonlyArray<{ id: CrmTaskView; label: string }> =\n [\n { id: 'mine', label: 'My tasks' },\n { id: 'overdue', label: 'Overdue' },\n { id: 'today', label: 'Today' },\n { id: 'upcoming', label: 'Upcoming' },\n { id: 'open', label: 'All open' },\n { id: 'done', label: 'Done' },\n ]\n\nexport const CRM_TASK_KINDS: readonly CrmTaskKind[] = [\n 'call',\n 'email',\n 'meeting',\n 'todo',\n]\n\nexport const CRM_TASK_KIND_LABELS: Record<CrmTaskKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n todo: 'To-do',\n}\n\nexport const CRM_TASK_PRIORITIES: readonly CrmTaskPriority[] = [\n 'low',\n 'normal',\n 'high',\n]\n\nexport const CRM_TASK_PRIORITY_LABELS: Record<CrmTaskPriority, string> = {\n low: 'Low',\n normal: 'Normal',\n high: 'High',\n}\n\n\n/** Midnight at the START of the local calendar day `nowMs` falls in. */\nexport function startOfLocalDay(nowMs: number): number {\n const date = new Date(nowMs)\n return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()\n}\n\n/**\n * Midnight at the start of the NEXT local day — built from the calendar\n * rather than by adding 24 hours, because a day that a clock change falls in\n * is 23 or 25 hours long and a task due at 00:30 on that morning belongs to\n * it either way.\n */\nexport function startOfNextLocalDay(nowMs: number): number {\n const date = new Date(nowMs)\n return new Date(\n date.getFullYear(),\n date.getMonth(),\n date.getDate() + 1,\n ).getTime()\n}\n\n/**\n * One view as constraints, in the vocabulary of the `crmTasks` indexes.\n *\n * `assigneeUid` narrows to the reader and rides the\n * `(visibleTo, assigneeUid, status, dueAtMs)` index; every other view rides\n * `(visibleTo, status, dueAtMs)`. `dueFrom` is inclusive and `dueBefore`\n * exclusive so that two adjacent windows share a boundary and no task falls\n * between \"today\" and \"upcoming\". A task with no due date has `dueAtMs:\n * null`, which a range excludes and a bare order sorts first, so it appears\n * in \"My tasks\" and \"All open\" and in none of the dated views.\n */\nexport interface CrmTaskViewPlan {\n status: 'open' | 'done'\n assigneeUid?: string\n dueFrom?: number\n dueBefore?: number\n direction: 'asc' | 'desc'\n}\n\nexport function crmTaskViewPlan(\n view: CrmTaskView,\n options: { nowMs: number; uid: string | null | undefined },\n): CrmTaskViewPlan {\n const { nowMs, uid } = options\n const today = startOfLocalDay(nowMs)\n const tomorrow = startOfNextLocalDay(nowMs)\n switch (view) {\n case 'mine':\n return { status: 'open', assigneeUid: uid ?? '', direction: 'asc' }\n case 'overdue':\n return { status: 'open', dueBefore: today, direction: 'asc' }\n case 'today':\n return { status: 'open', dueFrom: today, dueBefore: tomorrow, direction: 'asc' }\n case 'upcoming':\n return { status: 'open', dueFrom: tomorrow, direction: 'asc' }\n case 'open':\n return { status: 'open', direction: 'asc' }\n case 'done':\n // Most recently due first: what was finished lately is what a reader\n // scrolls the Done view for, and the oldest completed task is the one\n // nobody comes back to.\n return { status: 'done', direction: 'desc' }\n }\n}\n\n/*==========================================\n * THE TASKS LIST'S QUERY (AGL-3321).\n *\n * The view is the query's base — its status, the reader as assignee for\n * \"My tasks\", and the `dueAtMs` window for Overdue, Today and Upcoming —\n * and Kind, Priority, Assignee and the search word are predicates on the\n * same query, paged by it. Every equality rides one `(field, dueAtMs)`\n * composite per direction: ascending for the working views, descending for\n * Done.\n *=========================================*/\n\n/** The orders the list takes: soonest first, and Done's most recently due first. */\nexport const TASK_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'dueAtMs', direction: 'asc' },\n { path: 'dueAtMs', direction: 'desc' },\n]\n\n/** What the Filters panel may name on a task, besides the view. */\nexport const TASK_LIST_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('kind'),\n crmSelectField('priority'),\n crmSelectField('assigneeUid'),\n]\n\nexport const TASK_LIST_DECLARATION: ListQueryDeclaration = {\n fields: TASK_LIST_QUERY_FIELDS,\n sorts: TASK_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The view's own predicates, which the plan takes as its base beside the scope. */\nexport function crmTaskViewBase(plan: CrmTaskViewPlan): ListQueryFilter[] {\n return [\n { path: 'status', op: '==', value: plan.status },\n ...(plan.assigneeUid ? [{ path: 'assigneeUid', op: '==' as const, value: plan.assigneeUid }] : []),\n ...(plan.dueFrom !== undefined ? [{ path: 'dueAtMs', op: '>=' as const, value: plan.dueFrom }] : []),\n ...(plan.dueBefore !== undefined ? [{ path: 'dueAtMs', op: '<' as const, value: plan.dueBefore }] : []),\n ]\n}\n\n/** The base's shape for `listQueryIndexes`: the view's two equalities. */\nexport const TASK_LIST_VIEW_BASE_INDEX = [{ path: 'status' }, { path: 'assigneeUid' }] as const\n\n/**\n * Rows in the order a view should paint them.\n *\n * Firestore sorts `null` before every number, so an ascending order by\n * `dueAtMs` puts the undated tasks at the TOP of \"All open\" — above the one\n * due in an hour. The query stays as it is (the index decides that); the\n * undated rows are moved to the end here, in a stable pass that leaves the\n * dated order the server chose alone.\n */\nexport function orderTaskRows<T extends Pick<CrmTask, 'dueAtMs'>>(\n rows: readonly T[],\n): T[] {\n const dated: T[] = []\n const undated: T[] = []\n for (const row of rows) {\n ;(typeof row.dueAtMs === 'number' ? dated : undated).push(row)\n }\n return [...dated, ...undated]\n}\n\n/** The MUI palette key a due date is painted with, by its state. */\nexport const TASK_DUE_COLORS: Record<TaskDueState, string> = {\n overdue: 'error.main',\n today: 'warning.main',\n upcoming: 'text.primary',\n none: 'text.secondary',\n done: 'text.secondary',\n}\n\n/**\n * The due date as a person reads it: \"Overdue · Tue, Sep 1, 9:00 AM\",\n * \"Today, 3:30 PM\", \"No due date\". The state word comes first because it is\n * the part a reader scans a column for; the timestamp is what they read once\n * a row has their attention.\n */\nexport function describeTaskDue(\n task: Pick<CrmTask, 'status' | 'dueAtMs'>,\n nowMs: number,\n locale?: string,\n): { state: TaskDueState; label: string } {\n const state = taskDueState(task, nowMs)\n const dueAtMs = task.dueAtMs\n if (typeof dueAtMs !== 'number' || !Number.isFinite(dueAtMs)) {\n return { state, label: 'No due date' }\n }\n const due = new Date(dueAtMs)\n const time = due.toLocaleTimeString(locale, {\n hour: 'numeric',\n minute: '2-digit',\n })\n switch (state) {\n case 'today':\n return { state, label: `Today, ${time}` }\n case 'overdue':\n return {\n state,\n label: `Overdue · ${due.toLocaleDateString(locale, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n })}, ${time}`,\n }\n default:\n return {\n state,\n label: `${due.toLocaleDateString(locale, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n })}, ${time}`,\n }\n }\n}\n\n/**\n * `datetime-local` ↔ epoch milliseconds, in the reader's own zone.\n *\n * The input's value is a wall-clock string with no zone; `new Date(string)`\n * reads such a string as LOCAL time, which is what a person typing \"9:00\"\n * into a due-date field means. Built by hand in the other direction because\n * `toISOString()` would render the UTC clock, and a task due at nine would\n * reopen in the drawer at whatever nine is in Greenwich.\n */\nexport function dueAtToLocalInput(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-` +\n `${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`\n )\n}\n\nexport function localInputToDueAt(value: string): number | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n const ms = new Date(text).getTime()\n return Number.isFinite(ms) ? ms : null\n}\n\n/**\n * The two snoozes a row and the drawer offer by name (AGL-2619), in the\n * order a person reaches for them; \"Pick a date\" is the third and takes a\n * date rather than an option.\n */\nexport type CrmTaskSnoozeOption = 'tomorrow' | 'nextWeek'\n\nexport const CRM_TASK_SNOOZE_OPTIONS: ReadonlyArray<{\n id: CrmTaskSnoozeOption\n label: string\n}> = [\n { id: 'tomorrow', label: 'Tomorrow' },\n { id: 'nextWeek', label: 'Next week' },\n]\n\n/** The hour a snoozed task lands on when it had no time of day of its own. */\nexport const CRM_TASK_SNOOZE_DEFAULT_HOUR = 9\n\n/**\n * Where a snoozed task is due next.\n *\n * Counted from NOW, not from the old due date: a task a week overdue\n * snoozed to \"tomorrow\" is due tomorrow, which is what the word means to the\n * person pressing it, and not due six days ago. The time of day is kept —\n * a nine o'clock call stays a nine o'clock call — and a task that had no\n * time lands at {@link CRM_TASK_SNOOZE_DEFAULT_HOUR}, a working hour rather\n * than midnight. Built from the calendar for the reason\n * {@link startOfNextLocalDay} gives.\n */\nexport function snoozeDueAt(\n option: CrmTaskSnoozeOption,\n dueAtMs: number | null | undefined,\n nowMs: number,\n): number {\n const had = typeof dueAtMs === 'number' && Number.isFinite(dueAtMs) ? new Date(dueAtMs) : null\n const hours = had ? had.getHours() : CRM_TASK_SNOOZE_DEFAULT_HOUR\n const minutes = had ? had.getMinutes() : 0\n const now = new Date(nowMs)\n const days = option === 'tomorrow' ? 1 : 7\n return new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate() + days,\n hours,\n minutes,\n ).getTime()\n}\n\n/**\n * The record a task hangs off, as a link into the hub — or `null` for a\n * task that is nobody's in particular.\n *\n * One record per task on screen even when the document names more than one:\n * the contact wins, then the deal, then the company, because that is the\n * order of how specific each is about WHO the work is for. All three ids\n * stay on the document for the record pages' own queries.\n */\nexport function taskRecordLink(\n task: Pick<CrmTask, 'contactId' | 'companyId' | 'dealId'>,\n routes: CrmRoutes,\n): { kind: 'contact' | 'deal' | 'company'; id: string; href: string } | null {\n if (task.contactId) {\n return { kind: 'contact', id: task.contactId, href: routes.contact(task.contactId) }\n }\n if (task.dealId) {\n return { kind: 'deal', id: task.dealId, href: routes.deal(task.dealId) }\n }\n if (task.companyId) {\n return { kind: 'company', id: task.companyId, href: routes.company(task.companyId) }\n }\n return null\n}\n"],"names":["taskDueState","CRM_LIST_SEARCH","crmSelectField","CRM_TASK_VIEWS","id","label","CRM_TASK_KINDS","CRM_TASK_KIND_LABELS","call","email","meeting","todo","CRM_TASK_PRIORITIES","CRM_TASK_PRIORITY_LABELS","low","normal","high","startOfLocalDay","nowMs","date","Date","getFullYear","getMonth","getDate","getTime","startOfNextLocalDay","crmTaskViewPlan","view","options","uid","today","tomorrow","status","assigneeUid","direction","dueBefore","dueFrom","TASK_LIST_SORTS","path","TASK_LIST_QUERY_FIELDS","TASK_LIST_DECLARATION","fields","sorts","search","crmTaskViewBase","plan","op","value","undefined","TASK_LIST_VIEW_BASE_INDEX","orderTaskRows","rows","dated","undated","row","dueAtMs","push","TASK_DUE_COLORS","overdue","upcoming","none","done","describeTaskDue","task","locale","state","Number","isFinite","due","time","toLocaleTimeString","hour","minute","toLocaleDateString","weekday","month","day","dueAtToLocalInput","ms","pad","String","padStart","getHours","getMinutes","localInputToDueAt","text","trim","CRM_TASK_SNOOZE_OPTIONS","CRM_TASK_SNOOZE_DEFAULT_HOUR","snoozeDueAt","option","had","hours","minutes","now","days","taskRecordLink","routes","contactId","kind","href","contact","dealId","deal","companyId","company"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
|
+
/**
|
|
19
|
+
* The organization's deals as figure tables another plugin reads by id
|
|
20
|
+
* (AGL-3603) — the AI plugin's insights answer "how much is in the pipeline?"
|
|
21
|
+
* and "what did we win this month?" from them.
|
|
22
|
+
*
|
|
23
|
+
* - `crm.pipeline`: the open pipeline as it stands, by pipeline and stage —
|
|
24
|
+
* how many deals, what they are worth, and what they are worth at their
|
|
25
|
+
* stage's odds — with `pipelineTotals`, the Reports page's own arithmetic.
|
|
26
|
+
* - `crm.closed`: deals won and lost over the window, what was won, the win
|
|
27
|
+
* rate, and the change in won deals from the window before.
|
|
28
|
+
*
|
|
29
|
+
* Organization readers: with a site, only the deals shared with that site —
|
|
30
|
+
* `scopedToHost`, the predicate the rules evaluate — and with none, the
|
|
31
|
+
* workspace's. Each read is a Firestore query on `status` (and `closedAtMs`
|
|
32
|
+
* for the window), served by the `deals` composites the index file already
|
|
33
|
+
* carries, projected to the fields counted and capped; a deal's title, its
|
|
34
|
+
* contact and its company never reach a table.
|
|
35
|
+
*/
|
|
36
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
37
|
+
/** Open deals the pipeline reads; a larger pipeline is told its figures read low. */
|
|
38
|
+
export declare const DEAL_FIGURES_OPEN_CEILING = 1000;
|
|
39
|
+
/** Deals one window reads of each outcome. */
|
|
40
|
+
export declare const DEAL_FIGURES_CLOSED_CEILING = 1000;
|
|
41
|
+
/** Pipelines read. */
|
|
42
|
+
export declare const DEAL_FIGURES_PIPELINE_CEILING = 20;
|
|
43
|
+
export declare function crmPipelineFigureReader(firestore: () => Firestore): PluginFigureReader;
|
|
44
|
+
export declare function crmClosedDealsFigureReader(firestore: () => Firestore): PluginFigureReader;
|
|
45
|
+
/** Registers the deal readers from the console surface, where insight jobs run. */
|
|
46
|
+
export declare function registerDealFigureReaders(firestore: () => Firestore): void;
|
|
47
|
+
export {};
|