@aglyn/plugins-crm 1.0.0-beta.219 → 1.0.0-beta.221

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -15
  3. package/src/lib/components/company-import-drawer.d.ts +1 -1
  4. package/src/lib/components/contact-associations-card.js +4 -3
  5. package/src/lib/components/contact-associations-card.js.map +1 -1
  6. package/src/lib/components/crm-booking-zone.d.ts +8 -2
  7. package/src/lib/components/crm-booking-zone.js.map +1 -1
  8. package/src/lib/components/crm-glance-card.js +2 -1
  9. package/src/lib/components/crm-glance-card.js.map +1 -1
  10. package/src/lib/components/deal-products-card.js +27 -16
  11. package/src/lib/components/deal-products-card.js.map +1 -1
  12. package/src/lib/components/lead-campaigns-card.d.ts +4 -4
  13. package/src/lib/components/lead-campaigns-card.js +9 -8
  14. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  15. package/src/lib/components/lead-properties-card.js +4 -0
  16. package/src/lib/components/lead-properties-card.js.map +1 -1
  17. package/src/lib/components/lead-status-chip.js +3 -1
  18. package/src/lib/components/lead-status-chip.js.map +1 -1
  19. package/src/lib/components/leads-bulk-bar.js +7 -9
  20. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  21. package/src/lib/components/leads-section.js +5 -1
  22. package/src/lib/components/leads-section.js.map +1 -1
  23. package/src/lib/components/new-lead-drawer.js +5 -4
  24. package/src/lib/components/new-lead-drawer.js.map +1 -1
  25. package/src/lib/components/reports/activity-card.js +4 -3
  26. package/src/lib/components/reports/activity-card.js.map +1 -1
  27. package/src/lib/components/reports/closed-deals-card.js +4 -3
  28. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  29. package/src/lib/components/reports/contacts-mix-card.js +3 -2
  30. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  31. package/src/lib/components/reports/contacts-trend-card.js +2 -1
  32. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  33. package/src/lib/components/reports/forecast-card.js +3 -2
  34. package/src/lib/components/reports/forecast-card.js.map +1 -1
  35. package/src/lib/components/reports/lead-funnel-card.js +8 -6
  36. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  37. package/src/lib/components/reports/pipeline-card.js +4 -3
  38. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  39. package/src/lib/components/reports/report-format.d.ts +1 -1
  40. package/src/lib/components/reports/report-format.js.map +1 -1
  41. package/src/lib/components/reports/report-scope.d.ts +1 -1
  42. package/src/lib/components/reports/report-scope.js.map +1 -1
  43. package/src/lib/components/reports/source-conversion-card.js +2 -1
  44. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  45. package/src/lib/components/reports/tasks-card.js +2 -1
  46. package/src/lib/components/reports/tasks-card.js.map +1 -1
  47. package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
  48. package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
  49. package/src/lib/components/reports-section.js +4 -4
  50. package/src/lib/components/reports-section.js.map +1 -1
  51. package/src/lib/constants/bundle-common.d.ts +2 -0
  52. package/src/lib/constants/bundle-common.js +1 -0
  53. package/src/lib/constants/bundle-common.js.map +1 -1
  54. package/src/lib/declarations.console-server.d.ts +4 -0
  55. package/src/lib/declarations.console-server.js +12 -0
  56. package/src/lib/declarations.console-server.js.map +1 -1
  57. package/src/lib/declarations.server.d.ts +6 -0
  58. package/src/lib/declarations.server.js +51 -1
  59. package/src/lib/declarations.server.js.map +1 -1
  60. package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
  61. package/src/lib/hooks/use-crm-campaigns.js +3 -3
  62. package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
  63. package/src/lib/model/campaign-filing-activity.d.ts +1 -1
  64. package/src/lib/model/campaign-filing-activity.js +1 -1
  65. package/src/lib/model/campaign-filing-activity.js.map +1 -1
  66. package/src/lib/model/contact-record.js +1 -1
  67. package/src/lib/model/contact-record.js.map +1 -1
  68. package/src/lib/model/crm-digest.d.ts +45 -3
  69. package/src/lib/model/crm-digest.js +75 -11
  70. package/src/lib/model/crm-digest.js.map +1 -1
  71. package/src/lib/model/crm-inbound.d.ts +162 -0
  72. package/src/lib/model/crm-inbound.js +208 -0
  73. package/src/lib/model/crm-inbound.js.map +1 -0
  74. package/src/lib/model/crm-lead-import.d.ts +1 -1
  75. package/src/lib/model/crm-lead-import.js +4 -4
  76. package/src/lib/model/crm-lead-import.js.map +1 -1
  77. package/src/lib/model/crm-reports.d.ts +467 -0
  78. package/src/lib/model/crm-reports.js +667 -0
  79. package/src/lib/model/crm-reports.js.map +1 -0
  80. package/src/lib/model/lead-filters.d.ts +4 -3
  81. package/src/lib/model/lead-filters.js +7 -5
  82. package/src/lib/model/lead-filters.js.map +1 -1
  83. package/src/lib/plugin.js +36 -0
  84. package/src/lib/plugin.js.map +1 -1
  85. package/src/lib/server/api-v1/crm-leads.js +1 -0
  86. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  87. package/src/lib/server/api-v1/openapi.js +1 -1
  88. package/src/lib/server/api-v1/openapi.js.map +1 -1
  89. package/src/lib/server/campaign-filing-activity.d.ts +1 -1
  90. package/src/lib/server/campaign-filing-activity.js +7 -11
  91. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  92. package/src/lib/server/capture-contact.js +19 -5
  93. package/src/lib/server/capture-contact.js.map +1 -1
  94. package/src/lib/server/contact-email-engagement.d.ts +33 -0
  95. package/src/lib/server/contact-email-engagement.js +146 -0
  96. package/src/lib/server/contact-email-engagement.js.map +1 -0
  97. package/src/lib/server/contact-update.js +3 -3
  98. package/src/lib/server/contact-update.js.map +1 -1
  99. package/src/lib/server/container-detach.d.ts +31 -0
  100. package/src/lib/server/container-detach.js +115 -0
  101. package/src/lib/server/container-detach.js.map +1 -0
  102. package/src/lib/server/crm-inbound-email.d.ts +162 -0
  103. package/src/lib/server/crm-inbound-email.js +259 -0
  104. package/src/lib/server/crm-inbound-email.js.map +1 -0
  105. package/src/lib/server/crm-records-meter.d.ts +50 -0
  106. package/src/lib/server/crm-records-meter.js +126 -0
  107. package/src/lib/server/crm-records-meter.js.map +1 -0
  108. package/src/lib/server/daily-digest-route.d.ts +2 -8
  109. package/src/lib/server/daily-digest-route.js +15 -25
  110. package/src/lib/server/daily-digest-route.js.map +1 -1
  111. package/src/lib/server/inbound-address.js +5 -3
  112. package/src/lib/server/inbound-address.js.map +1 -1
  113. package/src/lib/server/inbound-route.js +7 -4
  114. package/src/lib/server/inbound-route.js.map +1 -1
  115. package/src/lib/server/lead-campaign-carry.js +3 -3
  116. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  117. package/src/lib/server/lead-create.d.ts +1 -1
  118. package/src/lib/server/lead-create.js +10 -15
  119. package/src/lib/server/lead-create.js.map +1 -1
  120. package/src/lib/server/lead-nurturing.d.ts +23 -0
  121. package/src/lib/server/lead-nurturing.js +82 -0
  122. package/src/lib/server/lead-nurturing.js.map +1 -0
  123. package/src/lib/server/leads-import.js +7 -7
  124. package/src/lib/server/leads-import.js.map +1 -1
  125. package/src/lib/server/record-email-state.js +23 -0
  126. package/src/lib/server/record-email-state.js.map +1 -1
  127. package/src/lib/server/record-timeline.d.ts +12 -0
  128. package/src/lib/server/record-timeline.js +127 -15
  129. package/src/lib/server/record-timeline.js.map +1 -1
  130. package/src/lib/server/task-reminders-route.js +4 -2
  131. package/src/lib/server/task-reminders-route.js.map +1 -1
  132. package/src/lib/server/task-routes.d.ts +2 -0
  133. package/src/lib/server/task-routes.js +22 -3
  134. package/src/lib/server/task-routes.js.map +1 -1
  135. package/src/lib/usage-axes.js +8 -1
  136. package/src/lib/usage-axes.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-reports.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 * CRM reporting arithmetic (AGL-2604).\n *\n * The Reports section and the dashboard glance card read Firestore\n * aggregates and bounded windows and then have to turn them into a period, a\n * set of weekly bars, a funnel and a pipeline total. Every one of those is a\n * decision about numbers that a card could get subtly wrong in its own JSX —\n * a bucket boundary that is closed on both ends counts a midnight contact\n * twice, a funnel that divides by the step before reads 100% where nobody\n * arrived, a forecast that weights a closed deal forecasts a sale twice. So\n * they live here, pure and specified, and the cards only draw.\n *\n * Pure data module: no Firestore, no React, no clock — every function that\n * needs \"now\" is handed it.\n */\n\nimport type { ContactFacet, ContactSource } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CRM_ACTIVITY_KINDS,\n CRM_LEAD_STATUSES,\n type ContactLifecycleStage,\n type CrmActivity,\n type CrmActivityKind,\n type CrmDeal,\n type CrmDealStage,\n type CrmLeadFields,\n type CrmLeadStatus,\n type CrmPipeline,\n type CrmTask,\n crmLeadStatus,\n dealStageById,\n isCrmActivityKind,\n isPipelineArchived,\n weightedDealAmountCents,\n} from '@aglyn/aglyn/app-utils/crm'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst WEEK_MS = 7 * DAY_MS\n\n/**\n * The periods a report can be read over.\n *\n * A fixed list rather than a free date range because every period here is\n * also a READ COST: the contact trend runs one count aggregate per week of\n * the period, and the closed-deal chart reads the deals closed within it. A\n * picker that offered \"all time\" would offer a read nobody has sized.\n */\nexport const CRM_REPORT_PERIODS = ['7d', '30d', '90d', 'month'] as const\n\nexport type CrmReportPeriod = (typeof CRM_REPORT_PERIODS)[number]\n\nexport const CRM_REPORT_PERIOD_LABELS: Record<CrmReportPeriod, string> = {\n '7d': 'Last 7 days',\n '30d': 'Last 30 days',\n '90d': 'Last 90 days',\n month: 'This month',\n}\n\n/**\n * A period and the period before it, as half-open millisecond ranges\n * `[from, to)`.\n *\n * The previous period is what a delta is measured against, so it is always\n * the same LENGTH as the current one and ends exactly where the current one\n * begins: \"this month\" against \"last month\", thirty days against the thirty\n * before them. A gap or an overlap between the two would make the comparison\n * a comparison of different things.\n */\nexport interface CrmReportRange {\n from: number\n to: number\n previousFrom: number\n previousTo: number\n}\n\n/**\n * The range a period names at a given moment.\n *\n * `to` is the moment itself, so a period is always \"up to now\" and a report\n * read twice in one sitting shows the same figures with the newer minutes\n * added. \"This month\" starts on the first of the LOCAL month, because that\n * is the month the reader sees on their calendar; the day periods count back\n * from the moment rather than from midnight, so \"last 7 days\" is a full\n * seven days rather than six and a fraction.\n */\nexport function crmReportRange(\n period: CrmReportPeriod,\n nowMs: number,\n): CrmReportRange {\n if (period === 'month') {\n const now = new Date(nowMs)\n const from = new Date(now.getFullYear(), now.getMonth(), 1).getTime()\n const previousFrom = new Date(\n now.getFullYear(),\n now.getMonth() - 1,\n 1,\n ).getTime()\n return { from, to: nowMs, previousFrom, previousTo: from }\n }\n const days = period === '7d' ? 7 : period === '30d' ? 30 : 90\n const from = nowMs - days * DAY_MS\n return {\n from,\n to: nowMs,\n previousFrom: from - days * DAY_MS,\n previousTo: from,\n }\n}\n\n/** One week of a period, `[start, end)`; the last week of a period may be short. */\nexport interface WeekBucketBounds {\n start: number\n end: number\n}\n\n/**\n * The weeks a range divides into: seven-day strides from `from`, the last\n * clipped at `to`.\n *\n * Strides from the START of the period rather than calendar weeks, so that\n * \"last 7 days\" is one bar and \"last 30 days\" is four full weeks and two\n * days — the period the reader picked, in the reader's units — instead of\n * whatever fragment of a Monday-to-Sunday week the period happened to start\n * inside. An empty or inverted range has no weeks.\n */\nexport function weekBuckets(from: number, to: number): WeekBucketBounds[] {\n const buckets: WeekBucketBounds[] = []\n if (!(to > from)) return buckets\n for (let start = from; start < to; start += WEEK_MS) {\n buckets.push({ start, end: Math.min(start + WEEK_MS, to) })\n }\n return buckets\n}\n\nexport interface WeekBucket<T> extends WeekBucketBounds {\n items: T[]\n}\n\n/**\n * The items of a range, grouped into its weeks.\n *\n * Half-open on every boundary, so an item at exactly midnight between two\n * weeks is in the later one and only the later one, and an item at `to` is\n * outside the period the way an item at `from` is inside it. Items with no\n * usable time are dropped rather than placed anywhere — an undated deal is\n * not a deal closed this week. Every week is returned, empty ones included,\n * because a chart wants a bar per week and a gap where a week had nothing is\n * a chart with the wrong number of bars.\n */\nexport function bucketByWeek<T>(\n items: readonly T[],\n atMs: (item: T) => number | null | undefined,\n from: number,\n to: number,\n): WeekBucket<T>[] {\n const buckets: WeekBucket<T>[] = weekBuckets(from, to).map((bounds) => ({\n ...bounds,\n items: [] as T[],\n }))\n if (!buckets.length) return buckets\n for (const item of items) {\n const at = atMs(item)\n if (typeof at !== 'number' || !Number.isFinite(at)) continue\n if (at < from || at >= to) continue\n const index = Math.min(\n buckets.length - 1,\n Math.floor((at - from) / WEEK_MS),\n )\n buckets[index].items.push(item)\n }\n return buckets\n}\n\n/** One step of the lifecycle funnel. */\nexport interface LifecycleFunnelStep {\n stage: ContactLifecycleStage\n label: string\n /** People AT this stage. */\n count: number\n /** People at this stage or any later one — everyone who got at least this far. */\n reached: number\n /**\n * `reached` over the previous step's `reached`, 0–1, or `null` on the first\n * step and wherever nobody reached the step before.\n */\n conversion: number | null\n}\n\nexport interface LifecycleFunnel {\n /** The progression, in `CONTACT_LIFECYCLE_STAGES` order, `other` left out. */\n steps: LifecycleFunnelStep[]\n /** People at the `other` stage, which is beside the funnel rather than in it. */\n other: number\n /** Every counted person, `other` included. */\n total: number\n}\n\n/**\n * The lifecycle funnel a set of per-stage counts describes.\n *\n * A funnel is read CUMULATIVELY: a customer was once a lead, so the \"lead\"\n * step is everyone who is a lead now or has moved past it, not only the\n * people currently sitting there. That is what makes the steps narrow as\n * they go down and what makes a step-to-step conversion mean something.\n * `other` is a stage a merchant chose for a person the funnel does not\n * describe, so it is reported beside the funnel and never folded into a\n * step. A conversion where the previous step was empty is `null` rather than\n * 0 or 100 — there is nothing to divide by, and either number would be read\n * as a measurement.\n */\nexport function funnelFromStages(\n counts: Partial<Record<ContactLifecycleStage, number>>,\n): LifecycleFunnel {\n const countOf = (stage: ContactLifecycleStage): number => {\n const value = Number(counts[stage] ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.round(value) : 0\n }\n const progression = CONTACT_LIFECYCLE_STAGES.filter(\n (stage) => stage !== 'other',\n )\n const steps: LifecycleFunnelStep[] = []\n let reachedAfter = 0\n for (let index = progression.length - 1; index >= 0; index -= 1) {\n const stage = progression[index]\n const count = countOf(stage)\n const reached = count + reachedAfter\n steps.unshift({\n stage,\n label: CONTACT_LIFECYCLE_STAGE_LABELS[stage],\n count,\n reached,\n conversion: null,\n })\n reachedAfter = reached\n }\n for (let index = 1; index < steps.length; index += 1) {\n const previous = steps[index - 1].reached\n steps[index].conversion = previous > 0 ? steps[index].reached / previous : null\n }\n const other = countOf('other')\n return {\n steps,\n other,\n total: steps.reduce((sum, step) => sum + step.count, 0) + other,\n }\n}\n\n/** The open deals sitting in one stage of a pipeline. */\nexport interface PipelineStageTotal {\n stage: CrmDealStage\n count: number\n amountCents: number\n weightedCents: number\n}\n\nexport interface PipelineTotals {\n /** Open deals, wherever they sit. */\n count: number\n amountCents: number\n /** The forecast: each open deal at its stage's odds. */\n weightedCents: number\n /** The pipeline's open stages in order, every one present even when empty. */\n stages: PipelineStageTotal[]\n /**\n * Open deals in a stage the pipeline does not have — retired, renamed, or\n * a closed-kind stage the status disagrees with. Counted in the totals,\n * worth nothing to the forecast, and reported so a merchant can find them.\n */\n unplaced: { count: number; amountCents: number }\n}\n\ntype PipelineDeal = Pick<CrmDeal, 'status' | 'stageId' | 'amountCents'>\n\n/**\n * What a pipeline holds, by stage.\n *\n * Only OPEN deals: a won deal is revenue and a lost one is history, and a\n * pipeline report that counted either would show a stage full of deals that\n * cannot move. Only the pipeline's open-kind stages are rows, for the same\n * reason from the other side — a \"Won\" column in a pipeline chart is the\n * closed-deals report drawn in the wrong place. The weighting is\n * `weightedDealAmountCents`, applied here rather than restated.\n */\nexport function pipelineTotals(\n deals: readonly PipelineDeal[],\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): PipelineTotals {\n const stages = [...(pipeline?.stages ?? [])]\n .filter((stage) => stage.kind === 'open')\n .sort((a, b) => a.order - b.order)\n const rows = new Map<string, PipelineStageTotal>(\n stages.map((stage) => [\n stage.id,\n { stage, count: 0, amountCents: 0, weightedCents: 0 },\n ]),\n )\n const totals: PipelineTotals = {\n count: 0,\n amountCents: 0,\n weightedCents: 0,\n stages: stages.map((stage) => rows.get(stage.id) as PipelineStageTotal),\n unplaced: { count: 0, amountCents: 0 },\n }\n for (const deal of deals) {\n if (deal.status !== 'open') continue\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n totals.count += 1\n totals.amountCents += amount\n const row = rows.get(deal.stageId)\n if (!row) {\n totals.unplaced.count += 1\n totals.unplaced.amountCents += amount\n continue\n }\n const weighted = weightedDealAmountCents(deal, row.stage)\n row.count += 1\n row.amountCents += amount\n row.weightedCents += weighted\n totals.weightedCents += weighted\n }\n return totals\n}\n\n/** How many months ahead the close-month forecast looks. */\nexport const FORECAST_MONTHS = 6\n\n/** One calendar month of the forecast, `[start, end)` in local time. */\nexport interface CloseMonthBucket {\n /** `2026-09` — stable across locales, what a row is keyed by. */\n key: string\n start: number\n end: number\n}\n\n/**\n * The next `months` calendar months from the one `nowMs` falls in, each\n * `[first of the month, first of the next)` on the reader's calendar.\n *\n * The current month is the first bucket, whole: a deal expected to close\n * on the 3rd when today is the 20th is late, not next month's, and a\n * forecast that started at \"now\" would drop it between the rows. Local\n * months rather than UTC, because `expectedCloseAtMs` is stored at local\n * noon of the day picked (`dateInputMs`) and a UTC boundary would put a\n * deal picked for the 1st into the month before in half the world.\n */\nexport function closeMonthBuckets(\n nowMs: number,\n months = FORECAST_MONTHS,\n): CloseMonthBucket[] {\n const now = new Date(nowMs)\n const buckets: CloseMonthBucket[] = []\n for (let offset = 0; offset < months; offset += 1) {\n const start = new Date(now.getFullYear(), now.getMonth() + offset, 1)\n const end = new Date(now.getFullYear(), now.getMonth() + offset + 1, 1)\n buckets.push({\n key: `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`,\n start: start.getTime(),\n end: end.getTime(),\n })\n }\n return buckets\n}\n\n/** One pipeline's open deals expected to close in one month. */\nexport interface ForecastCell {\n count: number\n amountCents: number\n weightedCents: number\n}\n\nexport interface PipelineForecast {\n pipelineId: string\n name: string\n /** One cell per bucket, in `buckets` order, every one present even when empty. */\n months: ForecastCell[]\n /** Open deals with no expected close — their own row, never a guess. */\n undated: ForecastCell\n /** Expected before the first bucket — overdue to close. */\n overdue: ForecastCell\n /** Expected past the last bucket. */\n later: ForecastCell\n total: ForecastCell\n}\n\nexport interface CloseMonthForecast {\n buckets: CloseMonthBucket[]\n pipelines: PipelineForecast[]\n /** Every pipeline's cells added together, in `buckets` order. */\n months: ForecastCell[]\n undated: ForecastCell\n overdue: ForecastCell\n later: ForecastCell\n total: ForecastCell\n}\n\ntype ForecastDeal = Pick<\n CrmDeal,\n 'status' | 'pipelineId' | 'stageId' | 'amountCents' | 'expectedCloseAtMs'\n>\n\nconst emptyCell = (): ForecastCell => ({ count: 0, amountCents: 0, weightedCents: 0 })\n\nfunction addToCell(cell: ForecastCell, amountCents: number, weightedCents: number) {\n cell.count += 1\n cell.amountCents += amountCents\n cell.weightedCents += weightedCents\n}\n\n/**\n * The open pipeline laid out by the month each deal is expected to close\n * (AGL-2620): per pipeline, one cell per month for the next\n * {@link FORECAST_MONTHS}, at face value and weighted by the deal's stage.\n *\n * Only open deals, for the reason `pipelineTotals` gives — a closed deal\n * has closed. A deal with no `expectedCloseAtMs` is its own row rather than\n * being dropped or dated \"now\": a forecast that quietly omitted undated\n * deals would understate the pipeline by exactly the deals nobody has\n * scheduled, which is the number a sales lead most wants to see. A deal\n * dated before the first month is overdue and a deal dated past the last\n * is \"later\"; both are reported so the rows add up to the pipeline.\n *\n * Every ACTIVE pipeline is a row even with nothing open — an empty\n * pipeline is a fact about the forecast too — while an archived one is a\n * row only if a deal still sits open in it. A deal in a pipeline the list\n * does not carry at all is grouped under its id with no name, so the\n * total still counts it; its stage cannot be resolved, so it weighs\n * nothing, as everywhere else.\n */\nexport function forecastByCloseMonth(\n deals: readonly ForecastDeal[],\n pipelines: ReadonlyArray<Pick<CrmPipeline, 'name' | 'stages' | 'archivedAt'> & { $id: string }>,\n nowMs: number,\n months = FORECAST_MONTHS,\n): CloseMonthForecast {\n const buckets = closeMonthBuckets(nowMs, months)\n const byId = new Map<string, PipelineForecast>()\n const pipelineRow = (pipelineId: string): PipelineForecast => {\n let row = byId.get(pipelineId)\n if (!row) {\n const pipeline = pipelines.find((entry) => entry.$id === pipelineId)\n row = {\n pipelineId,\n name: pipeline?.name ?? '',\n months: buckets.map(emptyCell),\n undated: emptyCell(),\n overdue: emptyCell(),\n later: emptyCell(),\n total: emptyCell(),\n }\n byId.set(pipelineId, row)\n }\n return row\n }\n for (const pipeline of pipelines) {\n if (!isPipelineArchived(pipeline)) pipelineRow(pipeline.$id)\n }\n\n const totals: CloseMonthForecast = {\n buckets,\n pipelines: [],\n months: buckets.map(emptyCell),\n undated: emptyCell(),\n overdue: emptyCell(),\n later: emptyCell(),\n total: emptyCell(),\n }\n const first = buckets[0]?.start ?? nowMs\n const last = buckets[buckets.length - 1]?.end ?? nowMs\n for (const deal of deals) {\n if (deal.status !== 'open') continue\n const pipelineId = String(deal.pipelineId ?? '')\n const row = pipelineRow(pipelineId)\n const pipeline = pipelines.find((entry) => entry.$id === pipelineId)\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n const weighted = weightedDealAmountCents(deal, dealStageById(pipeline, deal.stageId))\n const at = deal.expectedCloseAtMs\n const cells: Array<[ForecastCell, ForecastCell]> = [[row.total, totals.total]]\n if (typeof at !== 'number' || !Number.isFinite(at)) {\n cells.push([row.undated, totals.undated])\n } else if (at < first) {\n cells.push([row.overdue, totals.overdue])\n } else if (at >= last) {\n cells.push([row.later, totals.later])\n } else {\n const index = buckets.findIndex((bucket) => at >= bucket.start && at < bucket.end)\n cells.push([row.months[index], totals.months[index]])\n }\n for (const [own, all] of cells) {\n addToCell(own, amount, weighted)\n addToCell(all, amount, weighted)\n }\n }\n totals.pipelines = [...byId.values()]\n return totals\n}\n\n/**\n * How many of a list share each key, most common first.\n *\n * Ties break on the key so two renders of the same data draw the same order.\n * An item with no key is not \"a key called empty\": a contact whose facet\n * names no source is left out of a by-source chart rather than drawn as a\n * bar labeled with nothing.\n */\nexport function tally<T>(\n items: readonly T[],\n keyOf: (item: T) => string | null | undefined,\n): Array<{ key: string; count: number }> {\n const counts = new Map<string, number>()\n for (const item of items) {\n const key = keyOf(item)\n if (!key) continue\n counts.set(key, (counts.get(key) ?? 0) + 1)\n }\n return [...counts.entries()]\n .map(([key, count]) => ({ key, count }))\n .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))\n}\n\n/**\n * Percentage change against the previous period, to one decimal, or `null`\n * when the previous period had nothing.\n *\n * `null` rather than a number because every number is wrong: \"+100%\" reads\n * as growth, \"+0%\" reads as flat, and a first contact has no growth rate.\n * The tile that renders this draws nothing for `null`, which is the honest\n * rendering of \"there is no baseline\".\n */\nexport function deltaPercent(current: number, previous: number): number | null {\n if (!(previous > 0)) return null\n return Math.round(((current - previous) / previous) * 1000) / 10\n}\n\n/**\n * The currency a set of deals is in, and whether they are actually in more\n * than one.\n *\n * A pipeline total is a sum, and a sum of dollars and euros is a number\n * with no unit. The reports still show that number — a merchant with one\n * stray euro deal wants the pipeline figure, not a refusal — but they say\n * it is mixed. The most common currency names the total; `usd` is what a\n * deal with no currency means, per the model.\n */\nexport function currencyOfDeals(\n deals: ReadonlyArray<Pick<CrmDeal, 'currency'>>,\n): { currency: string; mixed: boolean } {\n const ranked = tally(deals, (deal) =>\n String(deal.currency || 'usd').toLowerCase(),\n )\n return { currency: ranked[0]?.key ?? 'usd', mixed: ranked.length > 1 }\n}\n\n/**\n * The local calendar day a moment falls in, `[start, end)`.\n *\n * The same day `taskDueState` decides \"today\" on, so a count of tasks due\n * today and the list's \"today\" chip agree about the same task at 11 p.m.\n */\nexport function localDayBounds(nowMs: number): { start: number; end: number } {\n const now = new Date(nowMs)\n const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)\n return { start: start.getTime(), end: end.getTime() }\n}\n\n/*==========================================\n * ACTIVITY BY TEAMMATE (AGL-2624)\n *=========================================*/\n\n/** One teammate's row of the activity leaderboard. */\nexport interface ActivityLeaderboardRow {\n /**\n * The account uid, or `''` for work no person did: an activity an\n * automation logged, a task ticked off with nobody assigned. One row\n * rather than a row per automation, because the leaderboard ranks the\n * team, and \"nobody\" is the honest name for the rest.\n */\n uid: string\n /**\n * The name the newest of this uid's activities was signed with, or `null`\n * when none carried one — a uid that only completed tasks, say. The\n * activity's own `byName` because a scoped reader cannot resolve a\n * colleague's uid any other way; see `CrmActivity.byName`.\n */\n name: string | null\n /** Activities logged, by kind, every kind present even at zero. */\n kinds: Record<CrmActivityKind, number>\n /** Every activity logged, whatever its kind. */\n activities: number\n /** Tasks ticked off. */\n tasksDone: number\n}\n\n/** An activity as stored — `kind` is whatever was written, not only what the model names. */\ntype LeaderboardActivity = Pick<CrmActivity, 'byUid' | 'atMs' | 'byName'> & {\n kind: string\n}\ntype LeaderboardTask = Pick<CrmTask, 'assigneeUid' | 'completedByUid'>\n\n/**\n * Who did what: the activities and the completed tasks of a period,\n * grouped by the person who did them, busiest first.\n *\n * An activity is credited to whoever logged it. A task is credited to\n * whoever COMPLETED it when the route stamped that, and to its assignee\n * otherwise: the leaderboard measures work done, and a manager closing out\n * a departed teammate's list did that work. A kind the model does not\n * name counts as `other` rather than vanishing — the activity happened.\n *\n * Busiest means activities plus tasks; ties break on activities, then on\n * the uid, so two renders of one window draw one order.\n */\nexport function activityLeaderboard(\n activities: readonly LeaderboardActivity[],\n tasksDone: readonly LeaderboardTask[],\n): ActivityLeaderboardRow[] {\n const rows = new Map<string, ActivityLeaderboardRow>()\n /** When each row's name was signed, so a newer signature replaces an older one. */\n const namedAtMs = new Map<string, number>()\n const rowFor = (uid: string) => {\n let row = rows.get(uid)\n if (!row) {\n row = {\n uid,\n name: null,\n kinds: Object.fromEntries(\n CRM_ACTIVITY_KINDS.map((kind) => [kind, 0]),\n ) as Record<CrmActivityKind, number>,\n activities: 0,\n tasksDone: 0,\n }\n rows.set(uid, row)\n }\n return row\n }\n for (const activity of activities) {\n const row = rowFor(String(activity.byUid ?? ''))\n const kind = isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n row.kinds[kind] += 1\n row.activities += 1\n const name = String(activity.byName ?? '').trim()\n const atMs = Number(activity.atMs)\n if (\n name &&\n Number.isFinite(atMs) &&\n atMs > (namedAtMs.get(row.uid) ?? Number.NEGATIVE_INFINITY)\n ) {\n row.name = name\n namedAtMs.set(row.uid, atMs)\n }\n }\n for (const task of tasksDone) {\n rowFor(String(task.completedByUid || task.assigneeUid || '')).tasksDone += 1\n }\n return [...rows.values()].sort(\n (a, b) =>\n b.activities + b.tasksDone - (a.activities + a.tasksDone) ||\n b.activities - a.activities ||\n a.uid.localeCompare(b.uid),\n )\n}\n\n/*==========================================\n * LEAD FUNNEL (AGL-2624)\n *=========================================*/\n\n/** The key a lead unqualified with no reason is tallied under. */\nexport const LEAD_NO_REASON_KEY = '$none'\nexport const LEAD_NO_REASON_LABEL = 'No reason given'\n\nexport interface LeadFunnelReason {\n /** The reason, case-folded and whitespace-collapsed, or {@link LEAD_NO_REASON_KEY}. */\n key: string\n /** The reason as it was first written. */\n label: string\n count: number\n}\n\nexport interface LeadFunnel {\n /** Every lead handed in. */\n total: number\n /** Leads at each status now, every status present. An absent status is `new`. */\n byStatus: Record<CrmLeadStatus, number>\n /** `new`, `nurturing` and `working` — the leads still needing somebody. */\n open: number\n /** Qualified over every lead, 0–1, or `null` when there were none. */\n qualifiedRate: number | null\n /** Unqualified over every lead, 0–1, or `null` when there were none. */\n unqualifiedRate: number | null\n /** Why leads were unqualified, most common first. */\n reasons: LeadFunnelReason[]\n}\n\n/**\n * Where a set of leads stands, and why the ones that were closed without\n * converting were closed.\n *\n * A status breakdown rather than a cumulative funnel, because a lead's\n * history is not on the document — only where it is now — and a lead can\n * be qualified straight from `new` without ever having been `working`. A\n * step that claimed \"reached working\" would be inventing a past. The two\n * rates are over EVERY lead handed in, so a period in which half the leads\n * are still untouched reads as half unconverted, which is what it is.\n *\n * Reasons are free text — the unqualify dialog requires one but does not\n * offer a list — so `Too small` and `too small` are one reason and the\n * label is whichever spelling came first. An unqualified lead written past\n * the dialog with no reason is counted under its own key rather than\n * dropped: the lead was closed, and \"no reason given\" is the count a\n * manager wants to see.\n */\nexport function leadFunnel(\n leads: ReadonlyArray<Pick<CrmLeadFields, 'status' | 'unqualifiedReason'>>,\n): LeadFunnel {\n const byStatus = Object.fromEntries(\n CRM_LEAD_STATUSES.map((status) => [status, 0]),\n ) as Record<CrmLeadStatus, number>\n const reasons = new Map<string, LeadFunnelReason>()\n for (const lead of leads) {\n const status = crmLeadStatus(lead)\n byStatus[status] += 1\n if (status !== 'unqualified') continue\n const label = String(lead.unqualifiedReason ?? '').replace(/\\s+/g, ' ').trim()\n const key = label ? label.toLowerCase() : LEAD_NO_REASON_KEY\n const row = reasons.get(key) ?? {\n key,\n label: label || LEAD_NO_REASON_LABEL,\n count: 0,\n }\n row.count += 1\n reasons.set(key, row)\n }\n const total = leads.length\n return {\n total,\n byStatus,\n open: byStatus.new + byStatus.nurturing + byStatus.working,\n qualifiedRate: total > 0 ? byStatus.qualified / total : null,\n unqualifiedRate: total > 0 ? byStatus.unqualified / total : null,\n reasons: [...reasons.values()].sort(\n (a, b) => b.count - a.count || a.label.localeCompare(b.label),\n ),\n }\n}\n\n/*==========================================\n * CONVERSION BY SOURCE (AGL-2624)\n *=========================================*/\n\n/**\n * The stages that mean a person has bought, or better.\n *\n * `customer` and `evangelist` only. `other` sits after `customer` in the\n * stage list so that no capture can overwrite it, but it names a step the\n * business chose and the report cannot know whether that step is past a\n * purchase; counting it would credit a source with a sale nobody recorded.\n */\nconst CUSTOMER_STAGES: ReadonlySet<ContactLifecycleStage> = new Set([\n 'customer',\n 'evangelist',\n])\n\ntype ConversionFacet = Pick<ContactFacet, 'sources' | 'lifecycleStage' | 'ordersCount'>\n\n/**\n * Whether this holder's view of a person says they have bought: a stage\n * at `customer` or past it, or an order on the books whatever the stage\n * says — an order path that never advanced the stage still sold something.\n */\nexport function contactIsCustomer(\n facet: Pick<ContactFacet, 'lifecycleStage' | 'ordersCount'>,\n): boolean {\n if (facet.lifecycleStage && CUSTOMER_STAGES.has(facet.lifecycleStage)) return true\n const orders = Number(facet.ordersCount ?? 0)\n return Number.isFinite(orders) && orders > 0\n}\n\nexport interface SourceConversionRow {\n source: ContactSource\n /** People this holder captured through the source. */\n captured: number\n /** Of those, the ones who are customers now. */\n customers: number\n /** `customers / captured`, 0–1. */\n rate: number\n}\n\nexport interface SourceConversion {\n /** One row per source anybody came through, most captured first. */\n rows: SourceConversionRow[]\n /** People with no source under this holder — no door to credit. */\n unsourced: number\n /** Every counted person who is a customer, once each. */\n customers: number\n /** Every person handed in. */\n total: number\n}\n\n/**\n * Which capture surfaces turn into customers.\n *\n * Read through one holder's facet, like every by-source figure: a source\n * is which of THIS business's doors the person came through. A person\n * captured two ways counts under both — the question is \"how well does\n * each door convert\", and a person who came through two is evidence for\n * both — so the rows can add to more than `total`, and `customers` counts\n * each person once for the tile above the table. Rows rank by captured,\n * not by rate, because a door with one visitor who bought is not the best\n * door.\n */\nexport function conversionBySource(\n facets: readonly ConversionFacet[],\n): SourceConversion {\n const rows = new Map<ContactSource, SourceConversionRow>()\n let unsourced = 0\n let customers = 0\n for (const facet of facets) {\n const isCustomer = contactIsCustomer(facet)\n if (isCustomer) customers += 1\n const sources = (Object.keys(facet.sources ?? {}) as ContactSource[]).filter(\n (source) => facet.sources?.[source],\n )\n if (!sources.length) {\n unsourced += 1\n continue\n }\n for (const source of sources) {\n const row = rows.get(source) ?? { source, captured: 0, customers: 0, rate: 0 }\n row.captured += 1\n if (isCustomer) row.customers += 1\n rows.set(source, row)\n }\n }\n return {\n rows: [...rows.values()]\n .map((row) => ({ ...row, rate: row.customers / row.captured }))\n .sort(\n (a, b) =>\n b.captured - a.captured ||\n b.customers - a.customers ||\n a.source.localeCompare(b.source),\n ),\n unsourced,\n customers,\n total: facets.length,\n }\n}\n\n/*==========================================\n * WON AND LOST, BY OWNER (AGL-2662)\n *=========================================*/\n\n/** A closed deal as the by-owner tally reads it. */\ntype OwnerOutcomeDeal = Pick<CrmDeal, 'ownerUid' | 'amountCents'>\n\n/** The key an unowned deal is tallied under — see {@link OwnerOutcomeRow.uid}. */\nexport const OWNER_UNASSIGNED_KEY = ''\n\nexport interface OwnerOutcomeRow {\n /**\n * The owner's account uid, or `''` for a deal nobody owns.\n *\n * One row for the unowned rather than none: a pipeline where a third of\n * what closed had no owner is a fact about the team, and dropping those\n * deals would make the rows add to less than the period's totals without\n * saying why.\n */\n uid: string\n won: number\n lost: number\n /** Value of what this owner won, in the deals' own minor units. */\n wonAmountCents: number\n /** Value of what they lost — the size of the miss, not a negative. */\n lostAmountCents: number\n /** Everything that closed for this owner, won or lost. */\n closed: number\n /**\n * Won over closed, 0–1, or `null` when nothing of theirs closed.\n *\n * Null rather than zero because the two read differently: an owner who\n * closed nothing has no rate, and rendering that as 0% would rank a\n * person who was not in the period below one who lost every deal.\n */\n winRate: number | null\n}\n\n/**\n * Who closed what, in the period's window.\n *\n * The by-owner reading of the same two sets the Won and lost card already\n * holds — everything that closed won, and everything that closed lost — so\n * this adds no read. Credited to `ownerUid`, the field the board and the\n * bulk bar both write, and never to the creator: a deal reassigned mid-cycle\n * belongs to whoever carried it at the close, which is what \"by owner\"\n * is asked to mean when a manager reads it.\n *\n * Ranked by how much CLOSED rather than by win rate, for the reason\n * `conversionBySource` ranks by captured: an owner who closed one deal and\n * won it is not the best closer on the team. Ties break on wins, then on\n * the uid, so two renders of one window draw one order.\n */\nexport function wonLostByOwner(\n won: readonly OwnerOutcomeDeal[],\n lost: readonly OwnerOutcomeDeal[],\n): OwnerOutcomeRow[] {\n const rows = new Map<string, OwnerOutcomeRow>()\n const rowFor = (uid: string) => {\n let row = rows.get(uid)\n if (!row) {\n row = {\n uid,\n won: 0,\n lost: 0,\n wonAmountCents: 0,\n lostAmountCents: 0,\n closed: 0,\n winRate: null,\n }\n rows.set(uid, row)\n }\n return row\n }\n /** A missing or negative amount counts as nothing, never as a debit. */\n const cents = (deal: OwnerOutcomeDeal) =>\n Math.max(0, Number(deal.amountCents ?? 0) || 0)\n for (const deal of won) {\n const row = rowFor(String(deal.ownerUid ?? OWNER_UNASSIGNED_KEY))\n row.won += 1\n row.closed += 1\n row.wonAmountCents += cents(deal)\n }\n for (const deal of lost) {\n const row = rowFor(String(deal.ownerUid ?? OWNER_UNASSIGNED_KEY))\n row.lost += 1\n row.closed += 1\n row.lostAmountCents += cents(deal)\n }\n return [...rows.values()]\n .map((row) => ({\n ...row,\n winRate: row.closed ? row.won / row.closed : null,\n }))\n .sort(\n (a, b) => b.closed - a.closed || b.won - a.won || a.uid.localeCompare(b.uid),\n )\n}\n"],"names":["CONTACT_LIFECYCLE_STAGES","CONTACT_LIFECYCLE_STAGE_LABELS","CRM_ACTIVITY_KINDS","CRM_LEAD_STATUSES","crmLeadStatus","dealStageById","isCrmActivityKind","isPipelineArchived","weightedDealAmountCents","DAY_MS","WEEK_MS","CRM_REPORT_PERIODS","CRM_REPORT_PERIOD_LABELS","month","crmReportRange","period","nowMs","now","Date","from","getFullYear","getMonth","getTime","previousFrom","to","previousTo","days","weekBuckets","buckets","start","push","end","Math","min","bucketByWeek","items","atMs","map","bounds","length","item","at","Number","isFinite","index","floor","funnelFromStages","counts","countOf","stage","value","round","progression","filter","steps","reachedAfter","count","reached","unshift","label","conversion","previous","other","total","reduce","sum","step","pipelineTotals","deals","pipeline","stages","kind","sort","a","b","order","rows","Map","id","amountCents","weightedCents","totals","get","unplaced","deal","status","amount","max","row","stageId","weighted","FORECAST_MONTHS","closeMonthBuckets","months","offset","key","String","padStart","emptyCell","addToCell","cell","forecastByCloseMonth","pipelines","byId","pipelineRow","pipelineId","find","entry","$id","name","undated","overdue","later","set","first","last","expectedCloseAtMs","cells","findIndex","bucket","own","all","values","tally","keyOf","entries","localeCompare","deltaPercent","current","currencyOfDeals","ranked","currency","toLowerCase","mixed","localDayBounds","getDate","activityLeaderboard","activities","tasksDone","namedAtMs","rowFor","uid","kinds","Object","fromEntries","activity","byUid","byName","trim","NEGATIVE_INFINITY","task","completedByUid","assigneeUid","LEAD_NO_REASON_KEY","LEAD_NO_REASON_LABEL","leadFunnel","leads","byStatus","reasons","lead","unqualifiedReason","replace","open","new","nurturing","working","qualifiedRate","qualified","unqualifiedRate","unqualified","CUSTOMER_STAGES","Set","contactIsCustomer","facet","lifecycleStage","has","orders","ordersCount","conversionBySource","facets","unsourced","customers","isCustomer","sources","keys","source","captured","rate","OWNER_UNASSIGNED_KEY","wonLostByOwner","won","lost","wonAmountCents","lostAmountCents","closed","winRate","cents","ownerUid"],"mappings":"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"}
@@ -23,7 +23,8 @@ import { type CrmClauseAsked } from './crm-list-query';
23
23
  /**
24
24
  * What the Leads section's `Show` control offers (AGL-2608): the two
25
25
  * aggregate views and each status on its own. `open` is the default — the
26
- * list opens on the work — asked of the query as `status in [new, working]`,
26
+ * list opens on the work — asked of the query as
27
+ * `status in [new, nurturing, working]`,
27
28
  * which every lead answers because every lead writer stores a status, `new`
28
29
  * for one nobody has touched (`crmLeadListFields`, AGL-3321).
29
30
  */
@@ -50,9 +51,9 @@ export declare const LEAD_SOURCE_FILTER_NONE = "\0none";
50
51
  export declare const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[];
51
52
  /** What each filter field reads as on a chip. */
52
53
  export declare const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
53
- /** The choices for Status: Open (new or working) and each status on its own. */
54
+ /** The choices for Status: Open (new, nurturing or working) and each status on its own. */
54
55
  export declare const LEAD_STATUS_FILTER_OPTIONS: {
55
- value: "new" | "working" | "qualified" | "unqualified" | "open";
56
+ value: "new" | "nurturing" | "working" | "qualified" | "unqualified" | "open";
56
57
  label: string;
57
58
  }[];
58
59
  /** The choices for Email: every verdict, plus the two aggregates. */
@@ -14,13 +14,14 @@ 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 { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
17
+ */ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_OPEN_STATUSES, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
18
18
  import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
19
19
  import { listSelectCodec } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
20
20
  import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
21
21
  export const LEAD_FILTERS = [
22
22
  'open',
23
23
  'new',
24
+ 'nurturing',
24
25
  'working',
25
26
  'qualified',
26
27
  'unqualified',
@@ -30,6 +31,7 @@ export const LEAD_FILTER_LABELS = {
30
31
  open: 'Open',
31
32
  all: 'All',
32
33
  new: CRM_LEAD_STATUS_LABELS.new,
34
+ nurturing: CRM_LEAD_STATUS_LABELS.nurturing,
33
35
  working: CRM_LEAD_STATUS_LABELS.working,
34
36
  qualified: CRM_LEAD_STATUS_LABELS.qualified,
35
37
  unqualified: CRM_LEAD_STATUS_LABELS.unqualified
@@ -117,9 +119,9 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
117
119
  leadSource: 'Lead source',
118
120
  campaignIds: 'Campaign'
119
121
  };
120
- /** The choices for Status: Open (new or working) and each status on its own. */ export const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter((option)=>option !== 'all').map((option)=>({
122
+ /** The choices for Status: Open (new, nurturing or working) and each status on its own. */ export const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter((option)=>option !== 'all').map((option)=>({
121
123
  value: option,
122
- label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option]
124
+ label: option === 'open' ? 'Open (new, nurturing or working)' : LEAD_FILTER_LABELS[option]
123
125
  }));
124
126
  /** The choices for Email: every verdict, plus the two aggregates. */ export const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter((option)=>option !== 'any').map((option)=>({
125
127
  value: option,
@@ -207,7 +209,7 @@ const STATUS_ALL = {
207
209
  * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):
208
210
  *
209
211
  * status `status`, stored `new` on a lead nobody has touched, so
210
- * Open is `status in [new, working]`
212
+ * Open is `status in [new, nurturing, working]`
211
213
  * emailState `emailStatus`, the verdict's status or `none`; "Cannot be
212
214
  * emailed" is every verdict but `ok`
213
215
  * leadSource `leadSourceKey`, the label as the picklist compares it, or
@@ -304,7 +306,7 @@ const ORG_READER = {
304
306
  {
305
307
  const statuses = [];
306
308
  for (const value of values){
307
- if (value === 'open') statuses.push('new', 'working');
309
+ if (value === 'open') statuses.push(...CRM_LEAD_OPEN_STATUSES);
308
310
  else if (CRM_LEAD_STATUSES.includes(value)) statuses.push(value);
309
311
  else return {
310
312
  refused: `${value} is not a lead status`
@@ -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_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\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'\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 `status in [new, 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 '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 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 *\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 { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\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 campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new 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 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\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, 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 * ownerUid as stored\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 {\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('new', 'working')\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 'ownerUid':\n return crmAnyOf('ownerUid', values)\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_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","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_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","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","direction","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","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","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 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_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\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'\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 *\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 { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\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 campaignIds: 'Campaign',\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\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 * ownerUid as stored\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 {\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 'ownerUid':\n return crmAnyOf('ownerUid', values)\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_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","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_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","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","direction","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","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
package/src/lib/plugin.js CHANGED
@@ -204,6 +204,42 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
204
204
  },
205
205
  Component: CrmConsolePage
206
206
  }
207
+ ],
208
+ /*
209
+ * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same
210
+ * sections over every site at once, mounted with no site and the org's
211
+ * sites as `orgMount`. Its listeners carry no scope clause, so the
212
+ * shell's org route refuses a site collaborator on REACH before it asks
213
+ * the `data.manage` declared above; a permission alone, which a
214
+ * collaborator's role can carry, never opens it.
215
+ *
216
+ * It carries the SITE tab's id on purpose: `release_contacts` names
217
+ * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the
218
+ * org half cannot ship while the site half is switched off. First among
219
+ * the organization's plugin tabs, beside Data, the org's other shared
220
+ * collection of records. The help opens the docs heading about this
221
+ * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect
222
+ * (`consoleRedirects`), so it needs no `legacyHrefs` here.
223
+ */ orgNavItems: [
224
+ {
225
+ label: 'CRM',
226
+ href: '/crm',
227
+ sections: CRM_CONSOLE_SECTIONS,
228
+ navTabId: 'nav-tab-contacts',
229
+ tabOrder: -100,
230
+ icon: {
231
+ path: mdiCardAccountDetailsOutline.path
232
+ },
233
+ header: {
234
+ title: 'CRM',
235
+ icon: {
236
+ path: mdiCardAccountDetailsOutline.path
237
+ },
238
+ docsTopic: 'crm',
239
+ docsAnchor: '#at-the-organization-level'
240
+ },
241
+ Component: CrmConsolePage
242
+ }
207
243
  ]
208
244
  });
209
245
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n registerCrmRecordRoutes()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card\n * — absent, not upsold, the treatment every gated card gets.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","withCrmOrgMount","registerCrmRecordRoutes","CRM_SEARCH_SOURCES","registerPluginZone","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","registerCrmConsole","zone","label","surface","layout","description","pluginId","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,4BAA4B,QAAQ,yBAAwB;AACrE,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,oBAAoB,QAAQ,uCAAmC;AACxE,SAASC,aAAa,QAAQ,kCAA8B;AAC5D,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,eAAe,QAAQ,+BAA2B;AAC3D,SAASC,uBAAuB,QAAQ,+BAA2B;AACnE,SAASC,kBAAkB,QAAQ,gCAA4B;AAC/D,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SAASC,2BAA2B,QAAQ,uCAAmC;AAC/E,SAASC,uBAAuB,QAAQ,mCAA+B;AAEvE,oDAAoD,GACpD,MAAMC,iBAAiBV,KACrB,IAAM,MAAM,CAAC;AAEf,iFAAiF,GACjF,2EAA2E,GAC3E,MAAMW,wBAAwBX,KAC5B,IAAM,MAAM,CAAC;AAGf,MAAMY,kBAAkBZ,KACtB,IAAM,MAAM,CAAC;AAGf;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASa;IACdR;IACAE,mBACE;QACEO,MAAML;QACNM,OAAO;QACPC,SAAS;QACT,sEAAsE;QACtE,uEAAuE;QACvEC,QAAQ;QACRC,aACE;IACJ,GACA;QAAEC,UAAUhB;IAAU;IAExBI,mBACE;QACEO,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACA;QAAEC,UAAUhB;IAAU;IAExBL,MAAMsB,wBAAwB,CAAC;QAC7BD,UAAUhB;QACVkB,aAAa;QACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCC,GACDC,YAAY;QACZ;;;;;;;;;;KAUC,GACDC,aAAa;QACb,mEAAmE;QACnEC,eAAelB;QACf;;;;;;;;;;;;;;;;;;;;;KAqBC,GACDmB,SAAS;YACP,sEAAsE;YACtE,gEAAgE;YAChE,sEAAsE;YACtE,UAAU;YACV;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACP,kEAAkE;gBAClE,kDAAkD;gBAClDL,aAAa;gBACbM,WAAWlB;YACb;YACA;gBACEe,MAAM5B,MAAMgC,oBAAoB,CAACC,aAAa;gBAC9CJ,UAAU;gBACVC,OAAO;gBACPL,aAAa;gBACbM,WAAWjB;YACb;YACA;gBACEc,MAAM5B,MAAMgC,oBAAoB,CAACC,aAAa;gBAC9CJ,UAAU;gBACVC,OAAO;gBACPL,aAAa;gBACbM,WAAW3B;YACb;YACA;gBACEwB,MAAM5B,MAAMgC,oBAAoB,CAACE,YAAY;gBAC7CL,UAAU;gBACVC,OAAO;gBACPL,aAAa;gBACbM,WAAWzB,gBAAgBQ;YAC7B;YACA;gBACEc,MAAM5B,MAAMgC,oBAAoB,CAACE,YAAY;gBAC7CL,UAAU;gBACVC,OAAO;gBACPL,aAAa;gBACbM,WAAWzB,gBAAgBF;YAC7B;SACD;QACD+B,UAAU;YACR;gBACElB,OAAO;gBACPmB,MAAM;gBACN,iEAAiE;gBACjE,kEAAkE;gBAClE,sDAAsD;gBACtDC,aAAa;oBAAC;iBAAY;gBAC1B,8DAA8D;gBAC9D,kEAAkE;gBAClE,8DAA8D;gBAC9D,6BAA6B;gBAC7BC,UAAUnC;gBACVoC,UAAU;gBACVC,MAAM;oBAAEC,MAAMxC,6BAA6BwC,IAAI;gBAAC;gBAChDC,QAAQ;oBACNZ,OAAO;oBACPU,MAAM;wBAAEC,MAAMxC,6BAA6BwC,IAAI;oBAAC;oBAChDE,WAAW;gBACb;gBACAZ,WAAWnB;YACb;SACD;IACH;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n registerCrmRecordRoutes()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card\n * — absent, not upsold, the treatment every gated card gets.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","withCrmOrgMount","registerCrmRecordRoutes","CRM_SEARCH_SOURCES","registerPluginZone","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","registerCrmConsole","zone","label","surface","layout","description","pluginId","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
@@ -85,6 +85,7 @@ import { CRM_ID_MAX, CRM_LABEL_MAX, CRM_TEXT_MAX, CRM_TITLE_MAX, crmCustomUpdate
85
85
  * constant they never asked for throws at import in every one of them.
86
86
  */ const LEAD_PATCH_STATUSES = [
87
87
  'new',
88
+ 'nurturing',
88
89
  'working',
89
90
  'unqualified'
90
91
  ];