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

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 (249) hide show
  1. package/package.json +14 -13
  2. package/src/index.d.ts +1 -0
  3. package/src/index.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +12 -4
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/company-detail-page.js +11 -0
  7. package/src/lib/components/company-detail-page.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +12 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/company-import-drawer.d.ts +1 -1
  11. package/src/lib/components/company-picker.js +10 -3
  12. package/src/lib/components/company-picker.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -4
  14. package/src/lib/components/contact-detail-page.js +50 -53
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-list-columns.d.ts +3 -0
  17. package/src/lib/components/contact-list-columns.js +21 -5
  18. package/src/lib/components/contact-list-columns.js.map +1 -1
  19. package/src/lib/components/contact-merge-dialog.js +2 -1
  20. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  21. package/src/lib/components/contact-timeline-card.js +4 -3
  22. package/src/lib/components/contact-timeline-card.js.map +1 -1
  23. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  24. package/src/lib/components/contacts-bulk-bar.js +24 -51
  25. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  26. package/src/lib/components/contacts-section.js +9 -3
  27. package/src/lib/components/contacts-section.js.map +1 -1
  28. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  29. package/src/lib/components/crm-export-all-button.js.map +1 -1
  30. package/src/lib/components/crm-send-email-dialog.js +9 -6
  31. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  32. package/src/lib/components/csv-import-drawer.js +2 -2
  33. package/src/lib/components/csv-import-drawer.js.map +1 -1
  34. package/src/lib/components/deal-detail-page.js +18 -2
  35. package/src/lib/components/deal-detail-page.js.map +1 -1
  36. package/src/lib/components/deal-edit-drawer.js +10 -0
  37. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  38. package/src/lib/components/deals-bulk-bar.js +13 -5
  39. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +6 -0
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-detail-page.js +25 -5
  43. package/src/lib/components/lead-detail-page.js.map +1 -1
  44. package/src/lib/components/lead-properties-card.js +6 -0
  45. package/src/lib/components/lead-properties-card.js.map +1 -1
  46. package/src/lib/components/lead-surfaces-note.js +4 -5
  47. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  48. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  49. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  50. package/src/lib/components/leads-bulk-bar.js +28 -3
  51. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  52. package/src/lib/components/leads-section.js +36 -7
  53. package/src/lib/components/leads-section.js.map +1 -1
  54. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  55. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  56. package/src/lib/components/recipes-card.js +9 -4
  57. package/src/lib/components/recipes-card.js.map +1 -1
  58. package/src/lib/components/record-sharing-card.d.ts +57 -0
  59. package/src/lib/components/record-sharing-card.js +244 -0
  60. package/src/lib/components/record-sharing-card.js.map +1 -0
  61. package/src/lib/components/settings-section.js +7 -0
  62. package/src/lib/components/settings-section.js.map +1 -1
  63. package/src/lib/components/share-records-dialog.d.ts +28 -0
  64. package/src/lib/components/share-records-dialog.js +184 -0
  65. package/src/lib/components/share-records-dialog.js.map +1 -0
  66. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  67. package/src/lib/components/sharing-rule-drawer.js +374 -0
  68. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  69. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  70. package/src/lib/components/sharing-rules-card.js +287 -0
  71. package/src/lib/components/sharing-rules-card.js.map +1 -0
  72. package/src/lib/components/use-crm-api.d.ts +1 -1
  73. package/src/lib/components/use-crm-api.js.map +1 -1
  74. package/src/lib/components/use-site-route-context.d.ts +25 -0
  75. package/src/lib/components/use-site-route-context.js +58 -0
  76. package/src/lib/components/use-site-route-context.js.map +1 -0
  77. package/src/lib/constants/api-routes.d.ts +31 -0
  78. package/src/lib/constants/api-routes.js +27 -1
  79. package/src/lib/constants/api-routes.js.map +1 -1
  80. package/src/lib/declarations.console-server.d.ts +37 -0
  81. package/src/lib/declarations.console-server.js +103 -0
  82. package/src/lib/declarations.console-server.js.map +1 -0
  83. package/src/lib/declarations.server.d.ts +6 -0
  84. package/src/lib/declarations.server.js +33 -0
  85. package/src/lib/declarations.server.js.map +1 -1
  86. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  87. package/src/lib/hooks/use-contact-remove.js +77 -0
  88. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  89. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  90. package/src/lib/hooks/use-crm-sharing.js +109 -0
  91. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  92. package/src/lib/model/companies-csv.d.ts +1 -1
  93. package/src/lib/model/companies-csv.js +2 -2
  94. package/src/lib/model/companies-csv.js.map +1 -1
  95. package/src/lib/model/contact-holder.d.ts +68 -0
  96. package/src/lib/model/contact-holder.js +129 -0
  97. package/src/lib/model/contact-holder.js.map +1 -0
  98. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  99. package/src/lib/model/contact-interaction-links.js +65 -0
  100. package/src/lib/model/contact-interaction-links.js.map +1 -0
  101. package/src/lib/model/contact-merge.d.ts +136 -0
  102. package/src/lib/model/contact-merge.js +412 -0
  103. package/src/lib/model/contact-merge.js.map +1 -0
  104. package/src/lib/model/contact-record.d.ts +7 -1
  105. package/src/lib/model/contact-record.js +6 -5
  106. package/src/lib/model/contact-record.js.map +1 -1
  107. package/src/lib/model/contact-remove.d.ts +48 -0
  108. package/src/lib/model/contact-remove.js +22 -0
  109. package/src/lib/model/contact-remove.js.map +1 -0
  110. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  111. package/src/lib/model/contacts-bulk-writes.js +2 -82
  112. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  113. package/src/lib/model/contacts-csv.d.ts +2 -2
  114. package/src/lib/model/contacts-csv.js +2 -2
  115. package/src/lib/model/contacts-csv.js.map +1 -1
  116. package/src/lib/model/crm-csv.d.ts +239 -0
  117. package/src/lib/model/crm-csv.js +467 -0
  118. package/src/lib/model/crm-csv.js.map +1 -0
  119. package/src/lib/model/crm-digest.d.ts +185 -0
  120. package/src/lib/model/crm-digest.js +272 -0
  121. package/src/lib/model/crm-digest.js.map +1 -0
  122. package/src/lib/model/crm-import.d.ts +7 -1
  123. package/src/lib/model/crm-import.js +2 -1
  124. package/src/lib/model/crm-import.js.map +1 -1
  125. package/src/lib/model/crm-record-routes.d.ts +5 -0
  126. package/src/lib/model/crm-record-routes.js +7 -0
  127. package/src/lib/model/crm-record-routes.js.map +1 -1
  128. package/src/lib/model/crm-routes.d.ts +6 -5
  129. package/src/lib/model/crm-routes.js +7 -7
  130. package/src/lib/model/crm-routes.js.map +1 -1
  131. package/src/lib/model/crm-search-sources.d.ts +15 -0
  132. package/src/lib/model/crm-search-sources.js +162 -0
  133. package/src/lib/model/crm-search-sources.js.map +1 -0
  134. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  135. package/src/lib/model/crm-sharing-api.js +59 -0
  136. package/src/lib/model/crm-sharing-api.js.map +1 -0
  137. package/src/lib/model/crm-sharing.d.ts +333 -0
  138. package/src/lib/model/crm-sharing.js +603 -0
  139. package/src/lib/model/crm-sharing.js.map +1 -0
  140. package/src/lib/model/deals-csv.d.ts +1 -1
  141. package/src/lib/model/deals-csv.js +4 -5
  142. package/src/lib/model/deals-csv.js.map +1 -1
  143. package/src/lib/model/lead-filters.d.ts +1 -1
  144. package/src/lib/model/leads-csv.d.ts +1 -1
  145. package/src/lib/model/leads-csv.js +1 -1
  146. package/src/lib/model/leads-csv.js.map +1 -1
  147. package/src/lib/model/tasks-csv.d.ts +1 -1
  148. package/src/lib/model/tasks-csv.js +1 -1
  149. package/src/lib/model/tasks-csv.js.map +1 -1
  150. package/src/lib/plan-entitlements.d.ts +75 -0
  151. package/src/lib/plan-entitlements.js +56 -0
  152. package/src/lib/plan-entitlements.js.map +1 -0
  153. package/src/lib/plugin.js +3 -0
  154. package/src/lib/plugin.js.map +1 -1
  155. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  156. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  157. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  158. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  159. package/src/lib/server/api-v1/contacts.js +1076 -0
  160. package/src/lib/server/api-v1/contacts.js.map +1 -0
  161. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  162. package/src/lib/server/api-v1/crm-activities.js +269 -0
  163. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  164. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  165. package/src/lib/server/api-v1/crm-companies.js +391 -0
  166. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  167. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  168. package/src/lib/server/api-v1/crm-deals.js +631 -0
  169. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  170. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  171. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  172. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  173. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  174. package/src/lib/server/api-v1/crm-leads.js +832 -0
  175. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  176. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  177. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  178. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  179. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  180. package/src/lib/server/api-v1/crm-shared.js +436 -0
  181. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  182. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  183. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  184. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  186. package/src/lib/server/api-v1/openapi.js +985 -0
  187. package/src/lib/server/api-v1/openapi.js.map +1 -0
  188. package/src/lib/server/api-v1/usage.d.ts +26 -0
  189. package/src/lib/server/api-v1/usage.js +73 -0
  190. package/src/lib/server/api-v1/usage.js.map +1 -0
  191. package/src/lib/server/consent-group-participant.js +1 -1
  192. package/src/lib/server/consent-group-participant.js.map +1 -1
  193. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  194. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  195. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  196. package/src/lib/server/contact-remove.d.ts +75 -0
  197. package/src/lib/server/contact-remove.js +244 -0
  198. package/src/lib/server/contact-remove.js.map +1 -0
  199. package/src/lib/server/contact-update.js +2 -1
  200. package/src/lib/server/contact-update.js.map +1 -1
  201. package/src/lib/server/contacts-import.d.ts +47 -11
  202. package/src/lib/server/contacts-import.js +171 -71
  203. package/src/lib/server/contacts-import.js.map +1 -1
  204. package/src/lib/server/contacts-merge.d.ts +1 -1
  205. package/src/lib/server/contacts-merge.js +3 -2
  206. package/src/lib/server/contacts-merge.js.map +1 -1
  207. package/src/lib/server/convert-host-lead.d.ts +189 -0
  208. package/src/lib/server/convert-host-lead.js +518 -0
  209. package/src/lib/server/convert-host-lead.js.map +1 -0
  210. package/src/lib/server/crm-sharing.d.ts +108 -0
  211. package/src/lib/server/crm-sharing.js +790 -0
  212. package/src/lib/server/crm-sharing.js.map +1 -0
  213. package/src/lib/server/daily-digest-route.d.ts +107 -0
  214. package/src/lib/server/daily-digest-route.js +588 -0
  215. package/src/lib/server/daily-digest-route.js.map +1 -0
  216. package/src/lib/server/export-route.d.ts +60 -0
  217. package/src/lib/server/export-route.js +447 -0
  218. package/src/lib/server/export-route.js.map +1 -0
  219. package/src/lib/server/inbound-route.d.ts +74 -0
  220. package/src/lib/server/inbound-route.js +257 -0
  221. package/src/lib/server/inbound-route.js.map +1 -0
  222. package/src/lib/server/lead-convert.d.ts +1 -1
  223. package/src/lib/server/lead-convert.js +2 -2
  224. package/src/lib/server/lead-convert.js.map +1 -1
  225. package/src/lib/server/merge-contacts.d.ts +55 -0
  226. package/src/lib/server/merge-contacts.js +274 -0
  227. package/src/lib/server/merge-contacts.js.map +1 -0
  228. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  229. package/src/lib/server/pipeline-record-index.js +87 -0
  230. package/src/lib/server/pipeline-record-index.js.map +1 -0
  231. package/src/lib/server/record-facts.js +2 -1
  232. package/src/lib/server/record-facts.js.map +1 -1
  233. package/src/lib/server/task-reminders-route.d.ts +82 -0
  234. package/src/lib/server/task-reminders-route.js +497 -0
  235. package/src/lib/server/task-reminders-route.js.map +1 -0
  236. package/src/lib/server-deal-stage.js +2 -1
  237. package/src/lib/server-deal-stage.js.map +1 -1
  238. package/src/lib/server.d.ts +5 -2
  239. package/src/lib/server.js +47 -2
  240. package/src/lib/server.js.map +1 -1
  241. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  242. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  243. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  244. package/src/lib/usage-axes.d.ts +35 -0
  245. package/src/lib/usage-axes.js +68 -0
  246. package/src/lib/usage-axes.js.map +1 -0
  247. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  248. package/src/lib/components/use-emails-hub-path.js +0 -59
  249. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-import.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 * BRINGING A SPREADSHEET OF PEOPLE INTO THE CRM — the pure half (AGL-2602).\n *\n * A merchant arriving from another CRM has a CSV: a header row naming\n * columns in that product's vocabulary, and one person per line. Nothing\n * about that file is in our shape. This module is every decision about the\n * file that needs no Firestore and no React — which columns mean what, how a\n * cell becomes a stored value, and what to say about a row that cannot be\n * stored — so that the console drawer and the server route read the same\n * rules rather than two copies of them.\n *\n * ## Three stages, and only the last one touches a record\n *\n * 1. {@link guessContactImportMapping} reads the header and proposes a\n * column → field mapping from a table of aliases. It is a PROPOSAL: the\n * drawer shows it and the operator corrects it, because an export's\n * \"Company\" column may hold a job title and no alias table can know.\n * 2. {@link mapContactImportRow} applies the mapping to one line, producing\n * the raw row the browser posts. Strings only — the browser trims nothing\n * and validates nothing, so the server is the one place the file is\n * judged and a stale drawer cannot judge it differently.\n * 3. {@link normalizeContactImportRow} turns a raw row into the values that\n * will be written, through the SAME normalizers every other door uses:\n * `normalizeContactEmail` for the dedupe key, `normalizePhone` for E.164,\n * `normalizeAddress` for the postal shape, `isContactLifecycleStage` for\n * the stage. An importer with its own idea of a valid phone number is how\n * one collection ends up storing a value three ways.\n *\n * ## A bad cell is dropped and named; a bad row is skipped and named\n *\n * The only thing that makes a row unstorable is an unusable address, because\n * the address is the identity the shared row is keyed by. Everything else is\n * a value on the row: a phone that cannot be read as a number is left off\n * the contact and REPORTED under {@link ContactImportRow.dropped}, so the\n * operator learns that forty rows had a phone in a format we could not read\n * rather than discovering, weeks later, that the phone column silently went\n * nowhere. `list-import.ts` applies the same rule to a line that is not an\n * address: reported, never quietly discarded.\n *\n * ## Ceilings are on the work, never on the audience\n *\n * {@link CONTACT_IMPORT_MAX_ROWS} bounds one FILE, and the drawer says so\n * before anything is sent. {@link CONTACT_IMPORT_CHUNK_SIZE} bounds one\n * REQUEST, so a five-thousand-row file is twenty-five requests with a\n * progress bar between them rather than one that times out halfway with no\n * record of what it did. Neither one ever trims a person to fit: a row past\n * the file ceiling is refused with the file, and a row is never dropped to\n * make a chunk smaller. The audience band is enforced where every capture\n * enforces it — inside `upsertHostContact` — and an import reports that\n * refusal per row rather than pre-empting it with a count of its own.\n */\n\nimport type { AglynPostalAddress } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport { normalizeAddress, normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CSV_IMPORT_CHUNK_SIZE,\n CSV_IMPORT_MAX_BODY_BYTES,\n CSV_IMPORT_MAX_ROWS,\n CSV_IMPORT_PREVIEW_ROWS,\n customImportTarget,\n customImportTargetKey,\n emptyImportResult,\n guessImportMapping,\n importAliasKeys,\n type ImportChunkResult,\n type ImportDroppedValue,\n importSkippedCsv,\n importTextValue,\n mapImportRow,\n mergeImportResults,\n parseImportFlag,\n parseImportTags,\n} from '@aglyn/aglyn/app-utils/csv-import'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ContactCustomValue,\n type ContactFieldDefinition,\n type ContactLifecycleStage,\n isContactLifecycleStage,\n} from '@aglyn/aglyn/app-utils/crm'\n\n/*\n * The custom-target grammar is the shared module's; it is re-exported here\n * because every reader of a contact mapping has always taken it from this\n * file, and the companies import reads the same two functions from theirs.\n */\nexport { customImportTarget, customImportTargetKey } from '@aglyn/aglyn/app-utils/csv-import'\n\n/**\n * The ceilings are the shared ones (`csv-import.ts`), under the names this\n * file has always exported: one file, one request and one preview are\n * bounded the same way for every collection, because the bounds are about\n * the browser's memory and a request's budget rather than about people.\n */\nexport const CONTACT_IMPORT_MAX_ROWS = CSV_IMPORT_MAX_ROWS\n\nexport const CONTACT_IMPORT_CHUNK_SIZE = CSV_IMPORT_CHUNK_SIZE\n\nexport const CONTACT_IMPORT_MAX_BODY_BYTES = CSV_IMPORT_MAX_BODY_BYTES\n\nexport const CONTACT_IMPORT_PREVIEW_ROWS = CSV_IMPORT_PREVIEW_ROWS\n\n/**\n * The most tags one row may carry — the profile drawer's own cap, so an\n * imported person cannot hold more tags than a hand-edited one.\n */\nexport const CONTACT_IMPORT_TAGS_MAX = 20\n\n/** The longest text a name, title, company name or custom text value keeps. */\nconst NAME_MAX = 120\nconst CUSTOM_TEXT_MAX = 1_000\n\n/**\n * The standard fields a column may be mapped to, in the order the mapping\n * menu lists them. A custom field is mapped by its key — see\n * {@link ContactImportTargetId}.\n */\nexport const CONTACT_IMPORT_FIELDS = [\n 'email',\n 'name',\n 'phone',\n 'jobTitle',\n 'companyName',\n 'addressLine1',\n 'addressLine2',\n 'addressCity',\n 'addressState',\n 'addressPostalCode',\n 'addressCountry',\n 'tags',\n 'ownerEmail',\n 'lifecycleStage',\n 'marketingConsent',\n] as const\n\nexport type ContactImportField = (typeof CONTACT_IMPORT_FIELDS)[number]\n\n/** How each field reads in the mapping menu. Typed so a field cannot ship unlabeled. */\nexport const CONTACT_IMPORT_FIELD_LABELS: Record<ContactImportField, string> = {\n email: 'Email (required)',\n name: 'Name',\n phone: 'Phone',\n jobTitle: 'Job title',\n companyName: 'Company name',\n addressLine1: 'Address line 1',\n addressLine2: 'Address line 2',\n addressCity: 'City',\n addressState: 'State or region',\n addressPostalCode: 'Postal code',\n addressCountry: 'Country (two-letter code)',\n tags: 'Tags (comma or | separated)',\n ownerEmail: 'Owner (team member email)',\n lifecycleStage: 'Lifecycle stage',\n marketingConsent: 'Marketing consent (yes/no)',\n}\n\n/**\n * Header aliases per field, matched after `importHeaderKey` normalization.\n *\n * The vocabulary of the exports people actually arrive with — HubSpot's\n * \"First Name\"/\"Last Name\" pair is deliberately NOT here, because two columns\n * cannot map to one field and a guess that took only the first name would\n * store half a name and look right in the preview. An operator with a split\n * name maps one of the two by hand, and the preview shows what they chose.\n */\nconst FIELD_ALIASES: Record<ContactImportField, readonly string[]> = {\n email: [\n 'email',\n 'email address',\n 'e-mail',\n 'e-mail address',\n 'contact email',\n 'primary email',\n 'work email',\n ],\n name: ['name', 'full name', 'contact name', 'display name', 'person'],\n phone: [\n 'phone',\n 'phone number',\n 'telephone',\n 'mobile',\n 'mobile phone',\n 'cell',\n 'work phone',\n ],\n jobTitle: ['job title', 'title', 'position', 'role'],\n companyName: ['company', 'company name', 'organization', 'organisation', 'account'],\n addressLine1: ['address', 'address line 1', 'street', 'street address', 'address 1'],\n addressLine2: ['address line 2', 'address 2', 'street 2', 'apartment', 'suite'],\n addressCity: ['city', 'town', 'locality'],\n addressState: ['state', 'region', 'province', 'county', 'state/region'],\n addressPostalCode: ['postal code', 'postcode', 'zip', 'zip code', 'post code'],\n addressCountry: ['country', 'country code', 'country/region'],\n tags: ['tags', 'tag', 'labels', 'groups', 'segments'],\n ownerEmail: ['owner', 'owner email', 'contact owner', 'assigned to', 'account owner'],\n lifecycleStage: ['lifecycle stage', 'stage', 'lifecycle', 'status', 'lead status'],\n marketingConsent: [\n 'marketing consent',\n 'consent',\n 'opt in',\n 'opt-in',\n 'opted in',\n 'subscribed',\n 'marketing opt in',\n 'email consent',\n ],\n}\n\n/**\n * What a column is mapped to: a standard field by name, or a custom field\n * as `custom:<key>` — see `customImportTarget`.\n */\nexport type ContactImportTargetId = ContactImportField | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type ContactImportMapping = Record<number, ContactImportTargetId>\n\n/** {@link FIELD_ALIASES}, normalized once for the matcher. */\nconst FIELD_ALIAS_KEYS = importAliasKeys(CONTACT_IMPORT_FIELDS, FIELD_ALIASES)\n\n/**\n * A proposed mapping from a file's header row — the shared matcher over the\n * contact vocabulary. Custom fields win over standard aliases when a header\n * matches a definition's label or key exactly: a merchant who defined a\n * field called \"Status\" meant THEIR status, not our lifecycle stage.\n */\nexport function guessContactImportMapping(\n columns: readonly string[],\n fields: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[] = [],\n): ContactImportMapping {\n return guessImportMapping(columns, CONTACT_IMPORT_FIELDS, FIELD_ALIAS_KEYS, fields)\n}\n\n/**\n * What the browser posts for one line: the cells the mapping selected,\n * under the field they were mapped to, verbatim.\n *\n * Values are `unknown` because the server reads this off an untrusted body;\n * the mapper below only ever writes strings into it.\n */\nexport interface ContactImportRawRow {\n email?: unknown\n name?: unknown\n phone?: unknown\n jobTitle?: unknown\n companyName?: unknown\n addressLine1?: unknown\n addressLine2?: unknown\n addressCity?: unknown\n addressState?: unknown\n addressPostalCode?: unknown\n addressCountry?: unknown\n tags?: unknown\n ownerEmail?: unknown\n lifecycleStage?: unknown\n marketingConsent?: unknown\n /** Custom values keyed by definition key. */\n custom?: unknown\n}\n\n/** One parsed line under the mapping. Empty cells are left absent. */\nexport function mapContactImportRow(\n cells: readonly string[],\n mapping: ContactImportMapping,\n): ContactImportRawRow {\n return mapImportRow(cells, mapping)\n}\n\n/** Why one row was not stored. */\nexport type ContactImportSkipReason =\n | 'invalid-email'\n | 'duplicate'\n | 'audience-band'\n /** The person was erased from this workspace at their request (AGL-2623). */\n | 'erased'\n | 'write-failed'\n\n/** How a skip reason reads on screen and in the downloaded file. */\nexport const CONTACT_IMPORT_SKIP_LABELS: Record<ContactImportSkipReason, string> = {\n 'invalid-email': 'Not a valid email address',\n duplicate: 'Appears earlier in the file',\n 'audience-band': 'Contact limit reached',\n erased: 'Erased from this workspace at their request',\n 'write-failed': 'Could not be saved',\n}\n\n/** A cell the file carried that could not be read as the field it was mapped to. */\nexport type ContactImportDroppedValue = ImportDroppedValue\n\n/** One row, ready to be written. */\nexport interface ContactImportRow {\n email: string\n name?: string\n /** E.164. */\n phone?: string\n jobTitle?: string\n companyName?: string\n address?: AglynPostalAddress\n /** Lowercased, deduplicated, capped at {@link CONTACT_IMPORT_TAGS_MAX}. */\n tags: string[]\n /** Normalized, for the server to resolve against the org's members. */\n ownerEmail?: string\n lifecycleStage?: ContactLifecycleStage\n marketingConsent: boolean\n /** Only the keys whose definition the caller supplied and whose value read. */\n custom: Record<string, ContactCustomValue>\n dropped: ContactImportDroppedValue[]\n}\n\nexport type ContactImportRowVerdict =\n | { ok: true; row: ContactImportRow }\n | { ok: false; reason: 'invalid-email'; input: string }\n\n/**\n * A tag cell as the tags it names.\n *\n * `|` or `,` separated — the CSV export writes `|`, most other products\n * write `,` — lowercased and trimmed like the profile drawer's own tag\n * field, so an imported `VIP` and a typed `vip` are one tag and not two.\n */\nexport function parseContactImportTags(value: unknown): string[] {\n return parseImportTags(value, CONTACT_IMPORT_TAGS_MAX)\n}\n\n/**\n * A yes/no cell as a boolean, or `null` when it is neither.\n *\n * The affirmatives are the ones consent and checkbox columns actually\n * carry; the negatives are listed so that an explicit `no` is a `false`\n * rather than an unreadable value that gets reported as dropped.\n */\nexport function parseContactImportFlag(value: unknown): boolean | null {\n return parseImportFlag(value)\n}\n\n/**\n * A stage cell as a stage, accepting the stored id (`sales-qualified`), its\n * label (\"Sales qualified\") and the spellings between them\n * (`Sales_Qualified`), or `null` when it names none of them.\n */\nexport function parseContactImportLifecycleStage(\n value: unknown,\n): ContactLifecycleStage | null {\n const text = String(value ?? '')\n .trim()\n .toLowerCase()\n if (!text) return null\n const key = text.replace(/[\\s_]+/g, '-')\n if (isContactLifecycleStage(key)) return key\n for (const stage of CONTACT_LIFECYCLE_STAGES) {\n if (CONTACT_LIFECYCLE_STAGE_LABELS[stage].toLowerCase() === text) return stage\n }\n return null\n}\n\n/**\n * A cell as the value a custom field of this type stores.\n *\n * `{ value }` when it read, `{ value: undefined }` for a blank cell (nothing\n * to write and nothing to report), and `null` when the cell held something\n * the type cannot hold — a word in a number field, a choice a select does\n * not offer. A date is stored as epoch milliseconds, the shape every other\n * timestamp on a CRM record takes (`dueAtMs`, `expectedCloseAtMs`).\n */\nexport function parseContactImportCustomValue(\n definition: Pick<ContactFieldDefinition, 'type' | 'options'>,\n value: unknown,\n): { value: ContactCustomValue | undefined } | null {\n const text = String(value ?? '').trim()\n if (!text) return { value: undefined }\n switch (definition.type) {\n case 'text':\n case 'url':\n return { value: text.slice(0, CUSTOM_TEXT_MAX) }\n case 'number': {\n const number = Number(text.replace(/[,\\s$€£]/g, ''))\n return Number.isFinite(number) ? { value: number } : null\n }\n case 'date': {\n const ms = Date.parse(text)\n return Number.isFinite(ms) ? { value: ms } : null\n }\n case 'select': {\n const wanted = text.toLowerCase()\n const option = (definition.options ?? []).find(\n (candidate) => String(candidate).trim().toLowerCase() === wanted,\n )\n return option === undefined ? null : { value: String(option) }\n }\n case 'checkbox': {\n const flag = parseContactImportFlag(text)\n return flag === null ? null : { value: flag }\n }\n default:\n return null\n }\n}\n\n/**\n * One raw row as the values that will be written, or the reason it cannot\n * be.\n *\n * `fields` is the holder's live definitions: a custom value under a key\n * with no definition is dropped and reported, because a value nobody\n * defined a field for is a value no form will ever show.\n */\nexport function normalizeContactImportRow(\n raw: ContactImportRawRow,\n fields: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'options'>[] = [],\n): ContactImportRowVerdict {\n const email = normalizeContactEmail(raw.email)\n if (!email) {\n return { ok: false, reason: 'invalid-email', input: String(raw.email ?? '').trim() }\n }\n const dropped: ContactImportDroppedValue[] = []\n const drop = (field: string, value: unknown) => {\n dropped.push({ field, value: String(value ?? '').trim() })\n }\n\n const row: ContactImportRow = {\n email,\n tags: parseContactImportTags(raw.tags),\n marketingConsent: false,\n custom: {},\n dropped,\n }\n\n const name = importTextValue(raw.name, NAME_MAX)\n if (name) row.name = name\n const jobTitle = importTextValue(raw.jobTitle, NAME_MAX)\n if (jobTitle) row.jobTitle = jobTitle\n const companyName = importTextValue(raw.companyName, NAME_MAX)\n if (companyName) row.companyName = companyName\n\n const phoneText = importTextValue(raw.phone, 64)\n if (phoneText) {\n const phone = normalizePhone(phoneText)\n if (phone) row.phone = phone\n else drop('phone', phoneText)\n }\n\n const address = normalizeAddress({\n line1: importTextValue(raw.addressLine1, 200),\n line2: importTextValue(raw.addressLine2, 200),\n city: importTextValue(raw.addressCity, 120),\n state: importTextValue(raw.addressState, 120),\n postalCode: importTextValue(raw.addressPostalCode, 32),\n country: importTextValue(raw.addressCountry, 8),\n })\n if (address) row.address = address\n // The country is the one address part the normalizer drops silently — a\n // typed name is not a code — so it is the one part the report has to name.\n const countryText = importTextValue(raw.addressCountry, 64)\n if (countryText && !address?.country) drop('addressCountry', countryText)\n\n const ownerText = importTextValue(raw.ownerEmail, 320)\n if (ownerText) {\n const owner = normalizeContactEmail(ownerText)\n if (owner) row.ownerEmail = owner\n else drop('ownerEmail', ownerText)\n }\n\n const stageText = importTextValue(raw.lifecycleStage, 64)\n if (stageText) {\n const stage = parseContactImportLifecycleStage(stageText)\n if (stage) row.lifecycleStage = stage\n else drop('lifecycleStage', stageText)\n }\n\n const consentText = raw.marketingConsent\n if (consentText !== undefined && consentText !== null && String(consentText).trim()) {\n const flag = parseContactImportFlag(consentText)\n if (flag === null) drop('marketingConsent', consentText)\n else row.marketingConsent = flag\n }\n\n const custom =\n raw.custom && typeof raw.custom === 'object' && !Array.isArray(raw.custom)\n ? (raw.custom as Record<string, unknown>)\n : {}\n const definitions = new Map(fields.map((field) => [field.key, field]))\n for (const [key, value] of Object.entries(custom)) {\n const definition = definitions.get(key)\n if (!definition) {\n drop(customImportTarget(key), value)\n continue\n }\n const parsed = parseContactImportCustomValue(definition, value)\n if (!parsed) drop(customImportTarget(key), value)\n else if (parsed.value !== undefined) row.custom[key] = parsed.value\n }\n\n return { ok: true, row }\n}\n\n/** One row the server did not store, by its index in the request, named by address. */\nexport interface ContactImportSkippedRow {\n index: number\n email: string\n reason: ContactImportSkipReason\n}\n\n/**\n * What one request did — the shared tally plus how many companies the\n * company column brought into being. The drawer sums these across a file.\n */\nexport interface ContactImportChunkResult\n extends ImportChunkResult<ContactImportSkippedRow> {\n companiesCreated: number\n}\n\n/** An empty tally, for the drawer to fold chunk results into. */\nexport function emptyContactImportResult(): ContactImportChunkResult {\n return { ...emptyImportResult<ContactImportSkippedRow>(), companiesCreated: 0 }\n}\n\n/**\n * Two results as one — the shared fold, with the company tally beside it.\n * `offset` is where the chunk started in the file, so a skipped row's index\n * comes back as its position in the FILE rather than in the request.\n */\nexport function mergeContactImportResults(\n total: ContactImportChunkResult,\n chunk: ContactImportChunkResult,\n offset = 0,\n): ContactImportChunkResult {\n return {\n ...mergeImportResults(total, chunk, offset),\n companiesCreated: total.companiesCreated + Number(chunk.companiesCreated ?? 0),\n }\n}\n\n/**\n * The skipped rows as a file the operator can fix and re-import: the\n * original columns verbatim plus a trailing `Skipped because` column.\n */\nexport function contactImportSkippedCsv(\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: ContactImportSkipReason }[],\n): string {\n return importSkippedCsv(columns, entries, CONTACT_IMPORT_SKIP_LABELS)\n}\n"],"names":["normalizeAddress","normalizePhone","normalizeContactEmail","CSV_IMPORT_CHUNK_SIZE","CSV_IMPORT_MAX_BODY_BYTES","CSV_IMPORT_MAX_ROWS","CSV_IMPORT_PREVIEW_ROWS","customImportTarget","emptyImportResult","guessImportMapping","importAliasKeys","importSkippedCsv","importTextValue","mapImportRow","mergeImportResults","parseImportFlag","parseImportTags","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","isContactLifecycleStage","customImportTargetKey","CONTACT_IMPORT_MAX_ROWS","CONTACT_IMPORT_CHUNK_SIZE","CONTACT_IMPORT_MAX_BODY_BYTES","CONTACT_IMPORT_PREVIEW_ROWS","CONTACT_IMPORT_TAGS_MAX","NAME_MAX","CUSTOM_TEXT_MAX","CONTACT_IMPORT_FIELDS","CONTACT_IMPORT_FIELD_LABELS","email","name","phone","jobTitle","companyName","addressLine1","addressLine2","addressCity","addressState","addressPostalCode","addressCountry","tags","ownerEmail","lifecycleStage","marketingConsent","FIELD_ALIASES","FIELD_ALIAS_KEYS","guessContactImportMapping","columns","fields","mapContactImportRow","cells","mapping","CONTACT_IMPORT_SKIP_LABELS","duplicate","erased","parseContactImportTags","value","parseContactImportFlag","parseContactImportLifecycleStage","text","String","trim","toLowerCase","key","replace","stage","parseContactImportCustomValue","definition","undefined","type","slice","number","Number","isFinite","ms","Date","parse","wanted","option","options","find","candidate","flag","normalizeContactImportRow","raw","ok","reason","input","dropped","drop","field","push","row","custom","phoneText","address","line1","line2","city","state","postalCode","country","countryText","ownerText","owner","stageText","consentText","Array","isArray","definitions","Map","map","Object","entries","get","parsed","emptyContactImportResult","companiesCreated","mergeContactImportResults","total","chunk","offset","contactImportSkippedCsv"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-import.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 * BRINGING A SPREADSHEET OF PEOPLE INTO THE CRM — the pure half (AGL-2602).\n *\n * A merchant arriving from another CRM has a CSV: a header row naming\n * columns in that product's vocabulary, and one person per line. Nothing\n * about that file is in our shape. This module is every decision about the\n * file that needs no Firestore and no React — which columns mean what, how a\n * cell becomes a stored value, and what to say about a row that cannot be\n * stored — so that the console drawer and the server route read the same\n * rules rather than two copies of them.\n *\n * ## Three stages, and only the last one touches a record\n *\n * 1. {@link guessContactImportMapping} reads the header and proposes a\n * column → field mapping from a table of aliases. It is a PROPOSAL: the\n * drawer shows it and the operator corrects it, because an export's\n * \"Company\" column may hold a job title and no alias table can know.\n * 2. {@link mapContactImportRow} applies the mapping to one line, producing\n * the raw row the browser posts. Strings only — the browser trims nothing\n * and validates nothing, so the server is the one place the file is\n * judged and a stale drawer cannot judge it differently.\n * 3. {@link normalizeContactImportRow} turns a raw row into the values that\n * will be written, through the SAME normalizers every other door uses:\n * `normalizeContactEmail` for the dedupe key, `normalizePhone` for E.164,\n * `normalizeAddress` for the postal shape, `isContactLifecycleStage` for\n * the stage. An importer with its own idea of a valid phone number is how\n * one collection ends up storing a value three ways.\n *\n * ## A bad cell is dropped and named; a bad row is skipped and named\n *\n * The only thing that makes a row unstorable is an unusable address, because\n * the address is the identity the shared row is keyed by. Everything else is\n * a value on the row: a phone that cannot be read as a number is left off\n * the contact and REPORTED under {@link ContactImportRow.dropped}, so the\n * operator learns that forty rows had a phone in a format we could not read\n * rather than discovering, weeks later, that the phone column silently went\n * nowhere. `list-import.ts` applies the same rule to a line that is not an\n * address: reported, never quietly discarded.\n *\n * ## Ceilings are on the work, never on the audience\n *\n * {@link CONTACT_IMPORT_MAX_ROWS} bounds one FILE, and the drawer says so\n * before anything is sent. {@link CONTACT_IMPORT_CHUNK_SIZE} bounds one\n * REQUEST, so a five-thousand-row file is twenty-five requests with a\n * progress bar between them rather than one that times out halfway with no\n * record of what it did. Neither one ever trims a person to fit: a row past\n * the file ceiling is refused with the file, and a row is never dropped to\n * make a chunk smaller. The audience band is enforced where every capture\n * enforces it — inside `upsertHostContact` — and an import reports that\n * refusal per row rather than pre-empting it with a count of its own.\n */\n\nimport type { AglynPostalAddress } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport { normalizeAddress, normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CSV_IMPORT_CHUNK_SIZE,\n CSV_IMPORT_MAX_BODY_BYTES,\n CSV_IMPORT_MAX_ROWS,\n CSV_IMPORT_PREVIEW_ROWS,\n customImportTarget,\n customImportTargetKey,\n emptyImportResult,\n guessImportMapping,\n importAliasKeys,\n type ImportChunkResult,\n type ImportDroppedValue,\n importSkippedCsv,\n importTextValue,\n mapImportRow,\n mergeImportResults,\n parseImportFlag,\n parseImportTags,\n} from '@aglyn/aglyn/app-utils/csv-import'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ContactCustomValue,\n type ContactFieldDefinition,\n type ContactLifecycleStage,\n isContactLifecycleStage,\n} from '@aglyn/aglyn/app-utils/crm'\n\n/*\n * The custom-target grammar is the shared module's; it is re-exported here\n * because every reader of a contact mapping has always taken it from this\n * file, and the companies import reads the same two functions from theirs.\n */\nexport { customImportTarget, customImportTargetKey } from '@aglyn/aglyn/app-utils/csv-import'\n\n/**\n * The ceilings are the shared ones (`csv-import.ts`), under the names this\n * file has always exported: one file, one request and one preview are\n * bounded the same way for every collection, because the bounds are about\n * the browser's memory and a request's budget rather than about people.\n */\nexport const CONTACT_IMPORT_MAX_ROWS = CSV_IMPORT_MAX_ROWS\n\nexport const CONTACT_IMPORT_CHUNK_SIZE = CSV_IMPORT_CHUNK_SIZE\n\nexport const CONTACT_IMPORT_MAX_BODY_BYTES = CSV_IMPORT_MAX_BODY_BYTES\n\nexport const CONTACT_IMPORT_PREVIEW_ROWS = CSV_IMPORT_PREVIEW_ROWS\n\n/**\n * The most tags one row may carry — the profile drawer's own cap, so an\n * imported person cannot hold more tags than a hand-edited one.\n */\nexport const CONTACT_IMPORT_TAGS_MAX = 20\n\n/** The longest text a name, title, company name or custom text value keeps. */\nconst NAME_MAX = 120\nconst CUSTOM_TEXT_MAX = 1_000\n\n/**\n * The standard fields a column may be mapped to, in the order the mapping\n * menu lists them. A custom field is mapped by its key — see\n * {@link ContactImportTargetId}.\n */\nexport const CONTACT_IMPORT_FIELDS = [\n 'email',\n 'name',\n 'phone',\n 'jobTitle',\n 'companyName',\n 'addressLine1',\n 'addressLine2',\n 'addressCity',\n 'addressState',\n 'addressPostalCode',\n 'addressCountry',\n 'tags',\n 'ownerEmail',\n 'lifecycleStage',\n 'marketingConsent',\n] as const\n\nexport type ContactImportField = (typeof CONTACT_IMPORT_FIELDS)[number]\n\n/** How each field reads in the mapping menu. Typed so a field cannot ship unlabeled. */\nexport const CONTACT_IMPORT_FIELD_LABELS: Record<ContactImportField, string> = {\n email: 'Email (required)',\n name: 'Name',\n phone: 'Phone',\n jobTitle: 'Job title',\n companyName: 'Company name',\n addressLine1: 'Address line 1',\n addressLine2: 'Address line 2',\n addressCity: 'City',\n addressState: 'State or region',\n addressPostalCode: 'Postal code',\n addressCountry: 'Country (two-letter code)',\n tags: 'Tags (comma or | separated)',\n ownerEmail: 'Owner (team member email)',\n lifecycleStage: 'Lifecycle stage',\n marketingConsent: 'Marketing consent (yes/no)',\n}\n\n/**\n * Header aliases per field, matched after `importHeaderKey` normalization.\n *\n * The vocabulary of the exports people actually arrive with — HubSpot's\n * \"First Name\"/\"Last Name\" pair is deliberately NOT here, because two columns\n * cannot map to one field and a guess that took only the first name would\n * store half a name and look right in the preview. An operator with a split\n * name maps one of the two by hand, and the preview shows what they chose.\n */\nconst FIELD_ALIASES: Record<ContactImportField, readonly string[]> = {\n email: [\n 'email',\n 'email address',\n 'e-mail',\n 'e-mail address',\n 'contact email',\n 'primary email',\n 'work email',\n ],\n name: ['name', 'full name', 'contact name', 'display name', 'person'],\n phone: [\n 'phone',\n 'phone number',\n 'telephone',\n 'mobile',\n 'mobile phone',\n 'cell',\n 'work phone',\n ],\n jobTitle: ['job title', 'title', 'position', 'role'],\n companyName: ['company', 'company name', 'organization', 'organisation', 'account'],\n addressLine1: ['address', 'address line 1', 'street', 'street address', 'address 1'],\n addressLine2: ['address line 2', 'address 2', 'street 2', 'apartment', 'suite'],\n addressCity: ['city', 'town', 'locality'],\n addressState: ['state', 'region', 'province', 'county', 'state/region'],\n addressPostalCode: ['postal code', 'postcode', 'zip', 'zip code', 'post code'],\n addressCountry: ['country', 'country code', 'country/region'],\n tags: ['tags', 'tag', 'labels', 'groups', 'segments'],\n ownerEmail: ['owner', 'owner email', 'contact owner', 'assigned to', 'account owner'],\n lifecycleStage: ['lifecycle stage', 'stage', 'lifecycle', 'status', 'lead status'],\n marketingConsent: [\n 'marketing consent',\n 'consent',\n 'opt in',\n 'opt-in',\n 'opted in',\n 'subscribed',\n 'marketing opt in',\n 'email consent',\n ],\n}\n\n/**\n * What a column is mapped to: a standard field by name, or a custom field\n * as `custom:<key>` — see `customImportTarget`.\n */\nexport type ContactImportTargetId = ContactImportField | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type ContactImportMapping = Record<number, ContactImportTargetId>\n\n/** {@link FIELD_ALIASES}, normalized once for the matcher. */\nconst FIELD_ALIAS_KEYS = importAliasKeys(CONTACT_IMPORT_FIELDS, FIELD_ALIASES)\n\n/**\n * A proposed mapping from a file's header row — the shared matcher over the\n * contact vocabulary. Custom fields win over standard aliases when a header\n * matches a definition's label or key exactly: a merchant who defined a\n * field called \"Status\" meant THEIR status, not our lifecycle stage.\n */\nexport function guessContactImportMapping(\n columns: readonly string[],\n fields: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[] = [],\n): ContactImportMapping {\n return guessImportMapping(columns, CONTACT_IMPORT_FIELDS, FIELD_ALIAS_KEYS, fields)\n}\n\n/**\n * What the browser posts for one line: the cells the mapping selected,\n * under the field they were mapped to, verbatim.\n *\n * Values are `unknown` because the server reads this off an untrusted body;\n * the mapper below only ever writes strings into it.\n */\nexport interface ContactImportRawRow {\n email?: unknown\n name?: unknown\n phone?: unknown\n jobTitle?: unknown\n companyName?: unknown\n addressLine1?: unknown\n addressLine2?: unknown\n addressCity?: unknown\n addressState?: unknown\n addressPostalCode?: unknown\n addressCountry?: unknown\n tags?: unknown\n ownerEmail?: unknown\n lifecycleStage?: unknown\n marketingConsent?: unknown\n /** Custom values keyed by definition key. */\n custom?: unknown\n}\n\n/** One parsed line under the mapping. Empty cells are left absent. */\nexport function mapContactImportRow(\n cells: readonly string[],\n mapping: ContactImportMapping,\n): ContactImportRawRow {\n return mapImportRow(cells, mapping)\n}\n\n/** Why one row was not stored. */\nexport type ContactImportSkipReason =\n | 'invalid-email'\n | 'duplicate'\n | 'audience-band'\n /** The person was erased from this workspace at their request (AGL-2623). */\n | 'erased'\n | 'write-failed'\n /**\n * The request ran out of time before it reached the row (AGL-3423). Nothing\n * was written for it, and importing the skipped file again is safe: a row\n * that did land merges into its record rather than creating a second one.\n */\n | 'not-reached'\n\n/** How a skip reason reads on screen and in the downloaded file. */\nexport const CONTACT_IMPORT_SKIP_LABELS: Record<ContactImportSkipReason, string> = {\n 'invalid-email': 'Not a valid email address',\n duplicate: 'Appears earlier in the file',\n 'audience-band': 'Contact limit reached',\n erased: 'Erased from this workspace at their request',\n 'write-failed': 'Could not be saved',\n 'not-reached': 'Not reached in time; import the skipped rows again',\n}\n\n/** A cell the file carried that could not be read as the field it was mapped to. */\nexport type ContactImportDroppedValue = ImportDroppedValue\n\n/** One row, ready to be written. */\nexport interface ContactImportRow {\n email: string\n name?: string\n /** E.164. */\n phone?: string\n jobTitle?: string\n companyName?: string\n address?: AglynPostalAddress\n /** Lowercased, deduplicated, capped at {@link CONTACT_IMPORT_TAGS_MAX}. */\n tags: string[]\n /** Normalized, for the server to resolve against the org's members. */\n ownerEmail?: string\n lifecycleStage?: ContactLifecycleStage\n marketingConsent: boolean\n /** Only the keys whose definition the caller supplied and whose value read. */\n custom: Record<string, ContactCustomValue>\n dropped: ContactImportDroppedValue[]\n}\n\nexport type ContactImportRowVerdict =\n | { ok: true; row: ContactImportRow }\n | { ok: false; reason: 'invalid-email'; input: string }\n\n/**\n * A tag cell as the tags it names.\n *\n * `|` or `,` separated — the CSV export writes `|`, most other products\n * write `,` — lowercased and trimmed like the profile drawer's own tag\n * field, so an imported `VIP` and a typed `vip` are one tag and not two.\n */\nexport function parseContactImportTags(value: unknown): string[] {\n return parseImportTags(value, CONTACT_IMPORT_TAGS_MAX)\n}\n\n/**\n * A yes/no cell as a boolean, or `null` when it is neither.\n *\n * The affirmatives are the ones consent and checkbox columns actually\n * carry; the negatives are listed so that an explicit `no` is a `false`\n * rather than an unreadable value that gets reported as dropped.\n */\nexport function parseContactImportFlag(value: unknown): boolean | null {\n return parseImportFlag(value)\n}\n\n/**\n * A stage cell as a stage, accepting the stored id (`sales-qualified`), its\n * label (\"Sales qualified\") and the spellings between them\n * (`Sales_Qualified`), or `null` when it names none of them.\n */\nexport function parseContactImportLifecycleStage(\n value: unknown,\n): ContactLifecycleStage | null {\n const text = String(value ?? '')\n .trim()\n .toLowerCase()\n if (!text) return null\n const key = text.replace(/[\\s_]+/g, '-')\n if (isContactLifecycleStage(key)) return key\n for (const stage of CONTACT_LIFECYCLE_STAGES) {\n if (CONTACT_LIFECYCLE_STAGE_LABELS[stage].toLowerCase() === text) return stage\n }\n return null\n}\n\n/**\n * A cell as the value a custom field of this type stores.\n *\n * `{ value }` when it read, `{ value: undefined }` for a blank cell (nothing\n * to write and nothing to report), and `null` when the cell held something\n * the type cannot hold — a word in a number field, a choice a select does\n * not offer. A date is stored as epoch milliseconds, the shape every other\n * timestamp on a CRM record takes (`dueAtMs`, `expectedCloseAtMs`).\n */\nexport function parseContactImportCustomValue(\n definition: Pick<ContactFieldDefinition, 'type' | 'options'>,\n value: unknown,\n): { value: ContactCustomValue | undefined } | null {\n const text = String(value ?? '').trim()\n if (!text) return { value: undefined }\n switch (definition.type) {\n case 'text':\n case 'url':\n return { value: text.slice(0, CUSTOM_TEXT_MAX) }\n case 'number': {\n const number = Number(text.replace(/[,\\s$€£]/g, ''))\n return Number.isFinite(number) ? { value: number } : null\n }\n case 'date': {\n const ms = Date.parse(text)\n return Number.isFinite(ms) ? { value: ms } : null\n }\n case 'select': {\n const wanted = text.toLowerCase()\n const option = (definition.options ?? []).find(\n (candidate) => String(candidate).trim().toLowerCase() === wanted,\n )\n return option === undefined ? null : { value: String(option) }\n }\n case 'checkbox': {\n const flag = parseContactImportFlag(text)\n return flag === null ? null : { value: flag }\n }\n default:\n return null\n }\n}\n\n/**\n * One raw row as the values that will be written, or the reason it cannot\n * be.\n *\n * `fields` is the holder's live definitions: a custom value under a key\n * with no definition is dropped and reported, because a value nobody\n * defined a field for is a value no form will ever show.\n */\nexport function normalizeContactImportRow(\n raw: ContactImportRawRow,\n fields: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'options'>[] = [],\n): ContactImportRowVerdict {\n const email = normalizeContactEmail(raw.email)\n if (!email) {\n return { ok: false, reason: 'invalid-email', input: String(raw.email ?? '').trim() }\n }\n const dropped: ContactImportDroppedValue[] = []\n const drop = (field: string, value: unknown) => {\n dropped.push({ field, value: String(value ?? '').trim() })\n }\n\n const row: ContactImportRow = {\n email,\n tags: parseContactImportTags(raw.tags),\n marketingConsent: false,\n custom: {},\n dropped,\n }\n\n const name = importTextValue(raw.name, NAME_MAX)\n if (name) row.name = name\n const jobTitle = importTextValue(raw.jobTitle, NAME_MAX)\n if (jobTitle) row.jobTitle = jobTitle\n const companyName = importTextValue(raw.companyName, NAME_MAX)\n if (companyName) row.companyName = companyName\n\n const phoneText = importTextValue(raw.phone, 64)\n if (phoneText) {\n const phone = normalizePhone(phoneText)\n if (phone) row.phone = phone\n else drop('phone', phoneText)\n }\n\n const address = normalizeAddress({\n line1: importTextValue(raw.addressLine1, 200),\n line2: importTextValue(raw.addressLine2, 200),\n city: importTextValue(raw.addressCity, 120),\n state: importTextValue(raw.addressState, 120),\n postalCode: importTextValue(raw.addressPostalCode, 32),\n country: importTextValue(raw.addressCountry, 8),\n })\n if (address) row.address = address\n // The country is the one address part the normalizer drops silently — a\n // typed name is not a code — so it is the one part the report has to name.\n const countryText = importTextValue(raw.addressCountry, 64)\n if (countryText && !address?.country) drop('addressCountry', countryText)\n\n const ownerText = importTextValue(raw.ownerEmail, 320)\n if (ownerText) {\n const owner = normalizeContactEmail(ownerText)\n if (owner) row.ownerEmail = owner\n else drop('ownerEmail', ownerText)\n }\n\n const stageText = importTextValue(raw.lifecycleStage, 64)\n if (stageText) {\n const stage = parseContactImportLifecycleStage(stageText)\n if (stage) row.lifecycleStage = stage\n else drop('lifecycleStage', stageText)\n }\n\n const consentText = raw.marketingConsent\n if (consentText !== undefined && consentText !== null && String(consentText).trim()) {\n const flag = parseContactImportFlag(consentText)\n if (flag === null) drop('marketingConsent', consentText)\n else row.marketingConsent = flag\n }\n\n const custom =\n raw.custom && typeof raw.custom === 'object' && !Array.isArray(raw.custom)\n ? (raw.custom as Record<string, unknown>)\n : {}\n const definitions = new Map(fields.map((field) => [field.key, field]))\n for (const [key, value] of Object.entries(custom)) {\n const definition = definitions.get(key)\n if (!definition) {\n drop(customImportTarget(key), value)\n continue\n }\n const parsed = parseContactImportCustomValue(definition, value)\n if (!parsed) drop(customImportTarget(key), value)\n else if (parsed.value !== undefined) row.custom[key] = parsed.value\n }\n\n return { ok: true, row }\n}\n\n/** One row the server did not store, by its index in the request, named by address. */\nexport interface ContactImportSkippedRow {\n index: number\n email: string\n reason: ContactImportSkipReason\n}\n\n/**\n * What one request did — the shared tally plus how many companies the\n * company column brought into being. The drawer sums these across a file.\n */\nexport interface ContactImportChunkResult\n extends ImportChunkResult<ContactImportSkippedRow> {\n companiesCreated: number\n}\n\n/** An empty tally, for the drawer to fold chunk results into. */\nexport function emptyContactImportResult(): ContactImportChunkResult {\n return { ...emptyImportResult<ContactImportSkippedRow>(), companiesCreated: 0 }\n}\n\n/**\n * Two results as one — the shared fold, with the company tally beside it.\n * `offset` is where the chunk started in the file, so a skipped row's index\n * comes back as its position in the FILE rather than in the request.\n */\nexport function mergeContactImportResults(\n total: ContactImportChunkResult,\n chunk: ContactImportChunkResult,\n offset = 0,\n): ContactImportChunkResult {\n return {\n ...mergeImportResults(total, chunk, offset),\n companiesCreated: total.companiesCreated + Number(chunk.companiesCreated ?? 0),\n }\n}\n\n/**\n * The skipped rows as a file the operator can fix and re-import: the\n * original columns verbatim plus a trailing `Skipped because` column.\n */\nexport function contactImportSkippedCsv(\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: ContactImportSkipReason }[],\n): string {\n return importSkippedCsv(columns, entries, CONTACT_IMPORT_SKIP_LABELS)\n}\n"],"names":["normalizeAddress","normalizePhone","normalizeContactEmail","CSV_IMPORT_CHUNK_SIZE","CSV_IMPORT_MAX_BODY_BYTES","CSV_IMPORT_MAX_ROWS","CSV_IMPORT_PREVIEW_ROWS","customImportTarget","emptyImportResult","guessImportMapping","importAliasKeys","importSkippedCsv","importTextValue","mapImportRow","mergeImportResults","parseImportFlag","parseImportTags","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","isContactLifecycleStage","customImportTargetKey","CONTACT_IMPORT_MAX_ROWS","CONTACT_IMPORT_CHUNK_SIZE","CONTACT_IMPORT_MAX_BODY_BYTES","CONTACT_IMPORT_PREVIEW_ROWS","CONTACT_IMPORT_TAGS_MAX","NAME_MAX","CUSTOM_TEXT_MAX","CONTACT_IMPORT_FIELDS","CONTACT_IMPORT_FIELD_LABELS","email","name","phone","jobTitle","companyName","addressLine1","addressLine2","addressCity","addressState","addressPostalCode","addressCountry","tags","ownerEmail","lifecycleStage","marketingConsent","FIELD_ALIASES","FIELD_ALIAS_KEYS","guessContactImportMapping","columns","fields","mapContactImportRow","cells","mapping","CONTACT_IMPORT_SKIP_LABELS","duplicate","erased","parseContactImportTags","value","parseContactImportFlag","parseContactImportLifecycleStage","text","String","trim","toLowerCase","key","replace","stage","parseContactImportCustomValue","definition","undefined","type","slice","number","Number","isFinite","ms","Date","parse","wanted","option","options","find","candidate","flag","normalizeContactImportRow","raw","ok","reason","input","dropped","drop","field","push","row","custom","phoneText","address","line1","line2","city","state","postalCode","country","countryText","ownerText","owner","stageText","consentText","Array","isArray","definitions","Map","map","Object","entries","get","parsed","emptyContactImportResult","companiesCreated","mergeContactImportResults","total","chunk","offset","contactImportSkippedCsv"],"mappings":"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"}
@@ -11,6 +11,11 @@
11
11
  * See the License for the specific language governing permissions and
12
12
  * limitations under the License.
13
13
  */
14
+ import { type PluginRecordRouteContext } from '@aglyn/aglyn/plugin-manager/plugin-record-routes';
15
+ import { type CrmRoutes } from './crm-routes';
16
+ declare function hub(context: PluginRecordRouteContext): CrmRoutes;
17
+ /** The hub's routes at the scope the context names — the site's, or the org's. */
18
+ export { hub as crmHubRoutes };
14
19
  /** The one narrowing the contacts list publishes: the form that captured them. */
15
20
  export declare const CRM_CONTACT_FILTER_FORM = "form";
16
21
  /**
@@ -35,6 +35,7 @@ function hub(context) {
35
35
  pluginSlug: CRM_SLUG
36
36
  }));
37
37
  }
38
+ /** The hub's routes at the scope the context names — the site's, or the org's. */ export { hub as crmHubRoutes };
38
39
  /** The one narrowing the contacts list publishes: the form that captured them. */ export const CRM_CONTACT_FILTER_FORM = 'form';
39
40
  /**
40
41
  * Called from the console registrar. The owner is named rather than read off
@@ -65,6 +66,12 @@ function hub(context) {
65
66
  list: (context)=>hub(context).section('deals'),
66
67
  record: (context, id)=>hub(context).deal(id)
67
68
  }, owner);
69
+ // A task has no page of its own: it is a row in the Tasks list, at both
70
+ // levels, so a task's record answers the list.
71
+ registerPluginRecordRoute('task', {
72
+ list: (context)=>hub(context).section('tasks'),
73
+ record: (context)=>hub(context).section('tasks')
74
+ }, owner);
68
75
  }
69
76
 
70
77
  //# sourceMappingURL=crm-record-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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 { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { crmRoutes, type CrmRoutes } from './crm-routes'\n\n/**\n * Where the CRM's records are read, published for every other surface.\n *\n * The Inbox links a lead, a submission links its sender, a form links the\n * people it captured. Each used to import {@link crmRoutes} and spell the\n * CRM's nav slug to do it, which made every one of them a reader of this\n * plugin's address table. They ask the record-route registry now, and get\n * `null` — text instead of a link — in a workspace where the CRM is not\n * loaded, which is also the first time that link stopped pointing at a page\n * the workspace cannot open.\n */\n\n/** The nav slug the shell resolves this plugin's hub by, at either scope. */\nconst CRM_SLUG = 'crm'\n\nfunction hub(context: PluginRecordRouteContext): CrmRoutes {\n return crmRoutes(\n context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_SLUG,\n })\n : buildRoute(Route.ORG_PLUGIN, {\n orgSlug: context.orgSlug,\n pluginSlug: CRM_SLUG,\n }),\n )\n}\n\n/** The one narrowing the contacts list publishes: the form that captured them. */\nexport const CRM_CONTACT_FILTER_FORM = 'form'\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the routes register the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCrmRecordRoutes(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginRecordRoute('contact', {\n list: (context) => hub(context).section('contacts'),\n record: (context, id) => hub(context).contact(id),\n byEmail: (context, email) => hub(context).contactByEmail(email),\n filtered: (context, filter, value) =>\n filter === CRM_CONTACT_FILTER_FORM\n ? hub(context).contactsByForm(value)\n : null,\n }, owner)\n registerPluginRecordRoute('lead', {\n list: (context) => hub(context).section('leads'),\n // One org row per person since AGL-3275, so the id alone addresses a\n // lead at both levels — the organization's hub included, where the\n // organization's Inbox links its leads (AGL-3303).\n record: (context, id) => hub(context).lead(id),\n }, owner)\n registerPluginRecordRoute('company', {\n list: (context) => hub(context).section('companies'),\n record: (context, id) => hub(context).company(id),\n }, owner)\n registerPluginRecordRoute('deal', {\n list: (context) => hub(context).section('deals'),\n record: (context, id) => hub(context).deal(id),\n }, owner)\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","crmRoutes","CRM_SLUG","hub","context","host","HOST_PLUGIN","orgSlug","pluginSlug","ORG_PLUGIN","CRM_CONTACT_FILTER_FORM","registerCrmRecordRoutes","owner","pluginId","list","section","record","id","contact","byEmail","email","contactByEmail","filtered","filter","value","contactsByForm","lead","company","deal"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,SAAS,QAAwB,kBAAc;AAExD;;;;;;;;;;CAUC,GAED,2EAA2E,GAC3E,MAAMC,WAAW;AAEjB,SAASC,IAAIC,OAAiC;IAC5C,OAAOH,UACLG,QAAQC,IAAI,GACRR,WAAWC,MAAMQ,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYN;IACd,KACAL,WAAWC,MAAMW,UAAU,EAAE;QAC3BF,SAASH,QAAQG,OAAO;QACxBC,YAAYN;IACd;AAER;AAEA,gFAAgF,GAChF,OAAO,MAAMQ,0BAA0B,OAAM;AAE7C;;;;CAIC,GACD,OAAO,SAASC;IACd,MAAMC,QAAQ;QAAEC,UAAUb;IAAU;IACpCD,0BAA0B,WAAW;QACnCe,MAAM,CAACV,UAAYD,IAAIC,SAASW,OAAO,CAAC;QACxCC,QAAQ,CAACZ,SAASa,KAAOd,IAAIC,SAASc,OAAO,CAACD;QAC9CE,SAAS,CAACf,SAASgB,QAAUjB,IAAIC,SAASiB,cAAc,CAACD;QACzDE,UAAU,CAAClB,SAASmB,QAAQC,QAC1BD,WAAWb,0BACPP,IAAIC,SAASqB,cAAc,CAACD,SAC5B;IACR,GAAGZ;IACHb,0BAA0B,QAAQ;QAChCe,MAAM,CAACV,UAAYD,IAAIC,SAASW,OAAO,CAAC;QACxC,qEAAqE;QACrE,mEAAmE;QACnE,mDAAmD;QACnDC,QAAQ,CAACZ,SAASa,KAAOd,IAAIC,SAASsB,IAAI,CAACT;IAC7C,GAAGL;IACHb,0BAA0B,WAAW;QACnCe,MAAM,CAACV,UAAYD,IAAIC,SAASW,OAAO,CAAC;QACxCC,QAAQ,CAACZ,SAASa,KAAOd,IAAIC,SAASuB,OAAO,CAACV;IAChD,GAAGL;IACHb,0BAA0B,QAAQ;QAChCe,MAAM,CAACV,UAAYD,IAAIC,SAASW,OAAO,CAAC;QACxCC,QAAQ,CAACZ,SAASa,KAAOd,IAAIC,SAASwB,IAAI,CAACX;IAC7C,GAAGL;AACL"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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 { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { crmRoutes, type CrmRoutes } from './crm-routes'\n\n/**\n * Where the CRM's records are read, published for every other surface.\n *\n * The Inbox links a lead, a submission links its sender, a form links the\n * people it captured. Each used to import {@link crmRoutes} and spell the\n * CRM's nav slug to do it, which made every one of them a reader of this\n * plugin's address table. They ask the record-route registry now, and get\n * `null` — text instead of a link — in a workspace where the CRM is not\n * loaded, which is also the first time that link stopped pointing at a page\n * the workspace cannot open.\n */\n\n/** The nav slug the shell resolves this plugin's hub by, at either scope. */\nconst CRM_SLUG = 'crm'\n\nfunction hub(context: PluginRecordRouteContext): CrmRoutes {\n return crmRoutes(\n context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_SLUG,\n })\n : buildRoute(Route.ORG_PLUGIN, {\n orgSlug: context.orgSlug,\n pluginSlug: CRM_SLUG,\n }),\n )\n}\n\n/** The hub's routes at the scope the context names — the site's, or the org's. */\nexport { hub as crmHubRoutes }\n\n/** The one narrowing the contacts list publishes: the form that captured them. */\nexport const CRM_CONTACT_FILTER_FORM = 'form'\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the routes register the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCrmRecordRoutes(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginRecordRoute('contact', {\n list: (context) => hub(context).section('contacts'),\n record: (context, id) => hub(context).contact(id),\n byEmail: (context, email) => hub(context).contactByEmail(email),\n filtered: (context, filter, value) =>\n filter === CRM_CONTACT_FILTER_FORM\n ? hub(context).contactsByForm(value)\n : null,\n }, owner)\n registerPluginRecordRoute('lead', {\n list: (context) => hub(context).section('leads'),\n // One org row per person since AGL-3275, so the id alone addresses a\n // lead at both levels — the organization's hub included, where the\n // organization's Inbox links its leads (AGL-3303).\n record: (context, id) => hub(context).lead(id),\n }, owner)\n registerPluginRecordRoute('company', {\n list: (context) => hub(context).section('companies'),\n record: (context, id) => hub(context).company(id),\n }, owner)\n registerPluginRecordRoute('deal', {\n list: (context) => hub(context).section('deals'),\n record: (context, id) => hub(context).deal(id),\n }, owner)\n // A task has no page of its own: it is a row in the Tasks list, at both\n // levels, so a task's record answers the list.\n registerPluginRecordRoute('task', {\n list: (context) => hub(context).section('tasks'),\n record: (context) => hub(context).section('tasks'),\n }, owner)\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","crmRoutes","CRM_SLUG","hub","context","host","HOST_PLUGIN","orgSlug","pluginSlug","ORG_PLUGIN","crmHubRoutes","CRM_CONTACT_FILTER_FORM","registerCrmRecordRoutes","owner","pluginId","list","section","record","id","contact","byEmail","email","contactByEmail","filtered","filter","value","contactsByForm","lead","company","deal"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,SAAS,QAAwB,kBAAc;AAExD;;;;;;;;;;CAUC,GAED,2EAA2E,GAC3E,MAAMC,WAAW;AAEjB,SAASC,IAAIC,OAAiC;IAC5C,OAAOH,UACLG,QAAQC,IAAI,GACRR,WAAWC,MAAMQ,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYN;IACd,KACAL,WAAWC,MAAMW,UAAU,EAAE;QAC3BF,SAASH,QAAQG,OAAO;QACxBC,YAAYN;IACd;AAER;AAEA,gFAAgF,GAChF,SAASC,OAAOO,YAAY,GAAE;AAE9B,gFAAgF,GAChF,OAAO,MAAMC,0BAA0B,OAAM;AAE7C;;;;CAIC,GACD,OAAO,SAASC;IACd,MAAMC,QAAQ;QAAEC,UAAUd;IAAU;IACpCD,0BAA0B,WAAW;QACnCgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASe,OAAO,CAACD;QAC9CE,SAAS,CAAChB,SAASiB,QAAUlB,IAAIC,SAASkB,cAAc,CAACD;QACzDE,UAAU,CAACnB,SAASoB,QAAQC,QAC1BD,WAAWb,0BACPR,IAAIC,SAASsB,cAAc,CAACD,SAC5B;IACR,GAAGZ;IACHd,0BAA0B,QAAQ;QAChCgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxC,qEAAqE;QACrE,mEAAmE;QACnE,mDAAmD;QACnDC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASuB,IAAI,CAACT;IAC7C,GAAGL;IACHd,0BAA0B,WAAW;QACnCgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASwB,OAAO,CAACV;IAChD,GAAGL;IACHd,0BAA0B,QAAQ;QAChCgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASyB,IAAI,CAACX;IAC7C,GAAGL;IACH,wEAAwE;IACxE,+CAA+C;IAC/Cd,0BAA0B,QAAQ;QAChCgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,UAAYD,IAAIC,SAASY,OAAO,CAAC;IAC5C,GAAGH;AACL"}
@@ -82,11 +82,12 @@ export declare function crmRoutes(basePath: string): {
82
82
  export declare const CONTACTS_LIST_SOURCE_PARAM = "source";
83
83
  export declare const CONTACTS_LIST_FORM_PARAM = "formId";
84
84
  /**
85
- * The email key is the SHARED constant (AGL-2622): the console app — which
86
- * the module boundaries keep from importing this plugin — builds the same
87
- * address through `crmContactByEmailHref`, and `crm-routes.spec.ts` pins
88
- * `contactByEmail` against it so the plugin and the app cannot spell the
89
- * key two ways.
85
+ * The query key the Contacts list OPENS the one person with an address by
86
+ * (AGL-2612). A surface that holds only an email — an order, a booking, a
87
+ * site user — asks the record-route registry for `contact` by email
88
+ * (`crm-record-routes.ts`), which builds the list with this key; the list
89
+ * filters on the address and moves on to the record when exactly one
90
+ * matches.
90
91
  */
91
92
  export declare const CONTACTS_LIST_EMAIL_PARAM = "email";
92
93
  export type CrmRoutes = ReturnType<typeof crmRoutes>;
@@ -16,7 +16,6 @@
16
16
  */ // Each from its own module, not the `@aglyn/aglyn` barrel: the server-side
17
17
  // person matcher builds its links here, and the barrel reaches the client
18
18
  // contexts no server graph may load.
19
- import { CRM_CONTACTS_EMAIL_PARAM } from "@aglyn/aglyn/app-utils/console-record-links";
20
19
  import { CRM_VIEW_PARAM } from "./crm-view-param.js";
21
20
  /**
22
21
  * Every address inside the Contacts surface, built from the one string the
@@ -88,11 +87,12 @@ import { CRM_VIEW_PARAM } from "./crm-view-param.js";
88
87
  */ export const CONTACTS_LIST_SOURCE_PARAM = 'source';
89
88
  export const CONTACTS_LIST_FORM_PARAM = 'formId';
90
89
  /**
91
- * The email key is the SHARED constant (AGL-2622): the console app — which
92
- * the module boundaries keep from importing this plugin — builds the same
93
- * address through `crmContactByEmailHref`, and `crm-routes.spec.ts` pins
94
- * `contactByEmail` against it so the plugin and the app cannot spell the
95
- * key two ways.
96
- */ export const CONTACTS_LIST_EMAIL_PARAM = CRM_CONTACTS_EMAIL_PARAM;
90
+ * The query key the Contacts list OPENS the one person with an address by
91
+ * (AGL-2612). A surface that holds only an email — an order, a booking, a
92
+ * site user — asks the record-route registry for `contact` by email
93
+ * (`crm-record-routes.ts`), which builds the list with this key; the list
94
+ * filters on the address and moves on to the record when exactly one
95
+ * matches.
96
+ */ export const CONTACTS_LIST_EMAIL_PARAM = 'email';
97
97
 
98
98
  //# sourceMappingURL=crm-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-routes.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// Each from its own module, not the `@aglyn/aglyn` barrel: the server-side\n// person matcher builds its links here, and the barrel reaches the client\n// contexts no server graph may load.\nimport { CRM_CONTACTS_EMAIL_PARAM } from '@aglyn/aglyn/app-utils/console-record-links'\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport type { CrmConsoleSectionId } from '../components/crm-console-sections'\nimport { CRM_VIEW_PARAM } from './crm-view-param'\n\n/**\n * Every address inside the Contacts surface, built from the one string the\n * shell hands the page (AGL-2595).\n *\n * The hub owns three kinds of deeper route — `people/{contactId}`,\n * `companies/{companyId}`, `deals/{dealId}` — and eight surfaces link into\n * them: a deal names its contact and company, a task names all three, a\n * report drills into a deal, an activity names whoever it happened with.\n * Eight surfaces each spelling `${basePath}/people/${id}` is eight places\n * for one of them to spell it `contacts/`, and a link that 404s inside the\n * hub is indistinguishable from a record that was deleted.\n *\n * The id is URL-encoded because Firestore ids are opaque: the console mints\n * them, but an import or an API caller may not, and a slash in an id would\n * otherwise read as a further segment.\n */\nexport function crmRoutes(basePath: string) {\n const section = (id: CrmConsoleSectionId) => `${basePath}/${id}`\n return {\n section,\n contact: (id: string) => `${section('contacts')}/${encodeURIComponent(id)}`,\n /**\n * The Contacts list narrowed to the people one form captured (AGL-2612):\n * source `form`, and the `formIds` filter on the form's id. The form's\n * own page links here; the list reads the two keys back through\n * `contactsListSeed`, which is the other half of this address.\n */\n contactsByForm: (formId: string) =>\n `${section('contacts')}?${new URLSearchParams({\n [CONTACTS_LIST_SOURCE_PARAM]: 'form',\n [CONTACTS_LIST_FORM_PARAM]: formId,\n }).toString()}`,\n /**\n * The Contacts list asked to OPEN the one person with this address\n * (AGL-2612). The list is the lookup: a contact's id is minted at\n * capture and nothing outside the CRM holds it, so a surface that has\n * only an email — an Inbox submission row — links here, the list\n * filters on the address (a whole-collection query, under the scope\n * the viewer may read) and moves straight on to the record when exactly\n * one matches. No match leaves the filtered list on screen, which is\n * the honest answer for a submission whose contact the band dropped.\n */\n contactByEmail: (email: string) =>\n `${section('contacts')}?${new URLSearchParams({\n [CONTACTS_LIST_EMAIL_PARAM]: email,\n }).toString()}`,\n /**\n * A lead's page: the id alone, at both levels (AGL-3275).\n *\n * This carried the SITE as a segment before the id at the organization\n * level, and had to: a lead's id is a person key, `hosts/{hostId}/leads`\n * was host-scoped by path, and so an address that two sites had both met\n * was two documents with the same id — which the id alone could not tell\n * apart. One org collection makes the person key unambiguous again, and a\n * lead addresses like every other CRM record.\n */\n lead: (id: string) => `${section('leads')}/${encodeURIComponent(id)}`,\n company: (id: string) => `${section('companies')}/${encodeURIComponent(id)}`,\n deal: (id: string) => `${section('deals')}/${encodeURIComponent(id)}`,\n /**\n * A section opened on one of its saved views (AGL-2617). The list reads\n * the key back through `crmViewIdFromParams`; the same key composes\n * with the Contacts seeds above, which is why it is a query key and not\n * a path segment.\n */\n sectionView: (id: CrmConsoleSectionId, viewId: string) =>\n `${section(id)}?${new URLSearchParams({\n [CRM_VIEW_PARAM]: viewId,\n }).toString()}`,\n }\n}\n\n/**\n * The query keys the Contacts list reads on arrival — written by the two\n * builders above and parsed by `contactsListSeed`, named once so neither\n * side can misspell the other.\n */\nexport const CONTACTS_LIST_SOURCE_PARAM = 'source'\nexport const CONTACTS_LIST_FORM_PARAM = 'formId'\n/**\n * The email key is the SHARED constant (AGL-2622): the console app — which\n * the module boundaries keep from importing this plugin — builds the same\n * address through `crmContactByEmailHref`, and `crm-routes.spec.ts` pins\n * `contactByEmail` against it so the plugin and the app cannot spell the\n * key two ways.\n */\nexport const CONTACTS_LIST_EMAIL_PARAM = CRM_CONTACTS_EMAIL_PARAM\n\nexport type CrmRoutes = ReturnType<typeof crmRoutes>\n\n/**\n * What the hub hands a record page: the shell's context, the record's id and\n * the surface's own path so the page can link back to its list.\n *\n * `basePath` is required here where the shell's prop is optional, because a\n * record page is only ever reached THROUGH the hub, which has already refused\n * to render without one.\n */\nexport type CrmDetailPageProps = Pick<\n ConsolePluginPageProps,\n 'hostId' | 'org' | 'permissions' | 'releaseFlag' | 'hostRole'\n> & {\n id: string\n basePath: string\n}\n"],"names":["CRM_CONTACTS_EMAIL_PARAM","CRM_VIEW_PARAM","crmRoutes","basePath","section","id","contact","encodeURIComponent","contactsByForm","formId","URLSearchParams","CONTACTS_LIST_SOURCE_PARAM","CONTACTS_LIST_FORM_PARAM","toString","contactByEmail","email","CONTACTS_LIST_EMAIL_PARAM","lead","company","deal","sectionView","viewId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,0EAA0E;AAC1E,qCAAqC;AACrC,SAASA,wBAAwB,QAAQ,8CAA6C;AAGtF,SAASC,cAAc,QAAQ,sBAAkB;AAEjD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,UAAUC,QAAgB;IACxC,MAAMC,UAAU,CAACC,KAA4B,GAAGF,SAAS,CAAC,EAAEE,IAAI;IAChE,OAAO;QACLD;QACAE,SAAS,CAACD,KAAe,GAAGD,QAAQ,YAAY,CAAC,EAAEG,mBAAmBF,KAAK;QAC3E;;;;;KAKC,GACDG,gBAAgB,CAACC,SACf,GAAGL,QAAQ,YAAY,CAAC,EAAE,IAAIM,gBAAgB;gBAC5C,CAACC,2BAA2B,EAAE;gBAC9B,CAACC,yBAAyB,EAAEH;YAC9B,GAAGI,QAAQ,IAAI;QACjB;;;;;;;;;KASC,GACDC,gBAAgB,CAACC,QACf,GAAGX,QAAQ,YAAY,CAAC,EAAE,IAAIM,gBAAgB;gBAC5C,CAACM,0BAA0B,EAAED;YAC/B,GAAGF,QAAQ,IAAI;QACjB;;;;;;;;;KASC,GACDI,MAAM,CAACZ,KAAe,GAAGD,QAAQ,SAAS,CAAC,EAAEG,mBAAmBF,KAAK;QACrEa,SAAS,CAACb,KAAe,GAAGD,QAAQ,aAAa,CAAC,EAAEG,mBAAmBF,KAAK;QAC5Ec,MAAM,CAACd,KAAe,GAAGD,QAAQ,SAAS,CAAC,EAAEG,mBAAmBF,KAAK;QACrE;;;;;KAKC,GACDe,aAAa,CAACf,IAAyBgB,SACrC,GAAGjB,QAAQC,IAAI,CAAC,EAAE,IAAIK,gBAAgB;gBACpC,CAACT,eAAe,EAAEoB;YACpB,GAAGR,QAAQ,IAAI;IACnB;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMF,6BAA6B,SAAQ;AAClD,OAAO,MAAMC,2BAA2B,SAAQ;AAChD;;;;;;CAMC,GACD,OAAO,MAAMI,4BAA4BhB,yBAAwB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-routes.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// Each from its own module, not the `@aglyn/aglyn` barrel: the server-side\n// person matcher builds its links here, and the barrel reaches the client\n// contexts no server graph may load.\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport type { CrmConsoleSectionId } from '../components/crm-console-sections'\nimport { CRM_VIEW_PARAM } from './crm-view-param'\n\n/**\n * Every address inside the Contacts surface, built from the one string the\n * shell hands the page (AGL-2595).\n *\n * The hub owns three kinds of deeper route — `people/{contactId}`,\n * `companies/{companyId}`, `deals/{dealId}` — and eight surfaces link into\n * them: a deal names its contact and company, a task names all three, a\n * report drills into a deal, an activity names whoever it happened with.\n * Eight surfaces each spelling `${basePath}/people/${id}` is eight places\n * for one of them to spell it `contacts/`, and a link that 404s inside the\n * hub is indistinguishable from a record that was deleted.\n *\n * The id is URL-encoded because Firestore ids are opaque: the console mints\n * them, but an import or an API caller may not, and a slash in an id would\n * otherwise read as a further segment.\n */\nexport function crmRoutes(basePath: string) {\n const section = (id: CrmConsoleSectionId) => `${basePath}/${id}`\n return {\n section,\n contact: (id: string) => `${section('contacts')}/${encodeURIComponent(id)}`,\n /**\n * The Contacts list narrowed to the people one form captured (AGL-2612):\n * source `form`, and the `formIds` filter on the form's id. The form's\n * own page links here; the list reads the two keys back through\n * `contactsListSeed`, which is the other half of this address.\n */\n contactsByForm: (formId: string) =>\n `${section('contacts')}?${new URLSearchParams({\n [CONTACTS_LIST_SOURCE_PARAM]: 'form',\n [CONTACTS_LIST_FORM_PARAM]: formId,\n }).toString()}`,\n /**\n * The Contacts list asked to OPEN the one person with this address\n * (AGL-2612). The list is the lookup: a contact's id is minted at\n * capture and nothing outside the CRM holds it, so a surface that has\n * only an email — an Inbox submission row — links here, the list\n * filters on the address (a whole-collection query, under the scope\n * the viewer may read) and moves straight on to the record when exactly\n * one matches. No match leaves the filtered list on screen, which is\n * the honest answer for a submission whose contact the band dropped.\n */\n contactByEmail: (email: string) =>\n `${section('contacts')}?${new URLSearchParams({\n [CONTACTS_LIST_EMAIL_PARAM]: email,\n }).toString()}`,\n /**\n * A lead's page: the id alone, at both levels (AGL-3275).\n *\n * This carried the SITE as a segment before the id at the organization\n * level, and had to: a lead's id is a person key, `hosts/{hostId}/leads`\n * was host-scoped by path, and so an address that two sites had both met\n * was two documents with the same id — which the id alone could not tell\n * apart. One org collection makes the person key unambiguous again, and a\n * lead addresses like every other CRM record.\n */\n lead: (id: string) => `${section('leads')}/${encodeURIComponent(id)}`,\n company: (id: string) => `${section('companies')}/${encodeURIComponent(id)}`,\n deal: (id: string) => `${section('deals')}/${encodeURIComponent(id)}`,\n /**\n * A section opened on one of its saved views (AGL-2617). The list reads\n * the key back through `crmViewIdFromParams`; the same key composes\n * with the Contacts seeds above, which is why it is a query key and not\n * a path segment.\n */\n sectionView: (id: CrmConsoleSectionId, viewId: string) =>\n `${section(id)}?${new URLSearchParams({\n [CRM_VIEW_PARAM]: viewId,\n }).toString()}`,\n }\n}\n\n/**\n * The query keys the Contacts list reads on arrival — written by the two\n * builders above and parsed by `contactsListSeed`, named once so neither\n * side can misspell the other.\n */\nexport const CONTACTS_LIST_SOURCE_PARAM = 'source'\nexport const CONTACTS_LIST_FORM_PARAM = 'formId'\n/**\n * The query key the Contacts list OPENS the one person with an address by\n * (AGL-2612). A surface that holds only an email — an order, a booking, a\n * site user — asks the record-route registry for `contact` by email\n * (`crm-record-routes.ts`), which builds the list with this key; the list\n * filters on the address and moves on to the record when exactly one\n * matches.\n */\nexport const CONTACTS_LIST_EMAIL_PARAM = 'email'\n\nexport type CrmRoutes = ReturnType<typeof crmRoutes>\n\n/**\n * What the hub hands a record page: the shell's context, the record's id and\n * the surface's own path so the page can link back to its list.\n *\n * `basePath` is required here where the shell's prop is optional, because a\n * record page is only ever reached THROUGH the hub, which has already refused\n * to render without one.\n */\nexport type CrmDetailPageProps = Pick<\n ConsolePluginPageProps,\n 'hostId' | 'org' | 'permissions' | 'releaseFlag' | 'hostRole'\n> & {\n id: string\n basePath: string\n}\n"],"names":["CRM_VIEW_PARAM","crmRoutes","basePath","section","id","contact","encodeURIComponent","contactsByForm","formId","URLSearchParams","CONTACTS_LIST_SOURCE_PARAM","CONTACTS_LIST_FORM_PARAM","toString","contactByEmail","email","CONTACTS_LIST_EMAIL_PARAM","lead","company","deal","sectionView","viewId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,0EAA0E;AAC1E,qCAAqC;AAGrC,SAASA,cAAc,QAAQ,sBAAkB;AAEjD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,UAAUC,QAAgB;IACxC,MAAMC,UAAU,CAACC,KAA4B,GAAGF,SAAS,CAAC,EAAEE,IAAI;IAChE,OAAO;QACLD;QACAE,SAAS,CAACD,KAAe,GAAGD,QAAQ,YAAY,CAAC,EAAEG,mBAAmBF,KAAK;QAC3E;;;;;KAKC,GACDG,gBAAgB,CAACC,SACf,GAAGL,QAAQ,YAAY,CAAC,EAAE,IAAIM,gBAAgB;gBAC5C,CAACC,2BAA2B,EAAE;gBAC9B,CAACC,yBAAyB,EAAEH;YAC9B,GAAGI,QAAQ,IAAI;QACjB;;;;;;;;;KASC,GACDC,gBAAgB,CAACC,QACf,GAAGX,QAAQ,YAAY,CAAC,EAAE,IAAIM,gBAAgB;gBAC5C,CAACM,0BAA0B,EAAED;YAC/B,GAAGF,QAAQ,IAAI;QACjB;;;;;;;;;KASC,GACDI,MAAM,CAACZ,KAAe,GAAGD,QAAQ,SAAS,CAAC,EAAEG,mBAAmBF,KAAK;QACrEa,SAAS,CAACb,KAAe,GAAGD,QAAQ,aAAa,CAAC,EAAEG,mBAAmBF,KAAK;QAC5Ec,MAAM,CAACd,KAAe,GAAGD,QAAQ,SAAS,CAAC,EAAEG,mBAAmBF,KAAK;QACrE;;;;;KAKC,GACDe,aAAa,CAACf,IAAyBgB,SACrC,GAAGjB,QAAQC,IAAI,CAAC,EAAE,IAAIK,gBAAgB;gBACpC,CAACT,eAAe,EAAEoB;YACpB,GAAGR,QAAQ,IAAI;IACnB;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMF,6BAA6B,SAAQ;AAClD,OAAO,MAAMC,2BAA2B,SAAQ;AAChD;;;;;;;CAOC,GACD,OAAO,MAAMI,4BAA4B,QAAO"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ * Unless required by applicable law or agreed to in writing, software
9
+ * distributed under the License is distributed on an "AS IS" BASIS,
10
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ * See the License for the specific language governing permissions and
12
+ * limitations under the License.
13
+ */
14
+ import type { ConsoleSearchSource } from '@aglyn/aglyn';
15
+ export declare const CRM_SEARCH_SOURCES: readonly ConsoleSearchSource[];
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ * Unless required by applicable law or agreed to in writing, software
9
+ * distributed under the License is distributed on an "AS IS" BASIS,
10
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ * See the License for the specific language governing permissions and
12
+ * limitations under the License.
13
+ */ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
14
+ import { crmHubRoutes } from "./crm-record-routes.js";
15
+ /**
16
+ * What the console's search finds in the CRM (AGL-3080): people, leads,
17
+ * companies, deals, and the team's tasks and activities, each opening on its
18
+ * record in the hub at the scope the reader stands at.
19
+ *
20
+ * Every group is the organization's shared data, read through the reader's
21
+ * `visibleTo` tokens under a site and unfiltered at the organization level,
22
+ * where only an org-wide member is offered it — the same reading the hub's
23
+ * own lists make. The extension's `crm` plan flag and `data.manage`
24
+ * permission gate all six, as they gate the hub the rows open in.
25
+ *
26
+ * Listed after the site's own pages and emails (30) and before its building
27
+ * blocks (100): a person is the thing most often looked up after a page.
28
+ */ /** A row's field as the plain string an address is built from. */ function text(value) {
29
+ return typeof value === 'string' ? value : '';
30
+ }
31
+ /** One record's page, or `null` for a row without an id. */ function recordHref(row, open) {
32
+ const id = text(row['$id']);
33
+ return id ? open(id) : null;
34
+ }
35
+ /**
36
+ * Where a task or an activity opens: the record it was filed under.
37
+ *
38
+ * Neither has a page of its own — a task is a row in a record's Tasks card
39
+ * and in the Tasks list, an activity a line on a timeline — so the useful
40
+ * destination is the deal, company, person or lead it belongs to, in that
41
+ * order: a task filed on a deal is about that deal, and landing on the
42
+ * contact would make the reader find it again. A lead addresses by its id
43
+ * alone at both levels (AGL-3275), so one filed on a lead opens the lead.
44
+ *
45
+ * A task that names no record — a standing task somebody typed into the list
46
+ * — lands on the Tasks section, which is where it is. An activity that names
47
+ * none has nowhere to go and answers `null`, which drops the row.
48
+ */ function workHref(kind, row, context) {
49
+ const routes = crmHubRoutes(context);
50
+ const dealId = text(row['dealId']);
51
+ if (dealId) return routes.deal(dealId);
52
+ const companyId = text(row['companyId']);
53
+ if (companyId) return routes.company(companyId);
54
+ const contactId = text(row['contactId']);
55
+ if (contactId) return routes.contact(contactId);
56
+ const leadId = text(row['leadId']);
57
+ if (leadId) return routes.lead(leadId);
58
+ return kind === 'task' ? routes.section('tasks') : null;
59
+ }
60
+ export const CRM_SEARCH_SOURCES = [
61
+ {
62
+ /*
63
+ * People, by name, email, phone number or company (AGL-2596). The phone
64
+ * and the company name are top-level echoes of the viewing holder's
65
+ * facet, written by every path that sets them, precisely so a read that
66
+ * never resolves a facet can hit them. A person a checkout captured with
67
+ * no name is labeled by the address, as the Contacts list labels them.
68
+ */ id: 'contacts',
69
+ group: 'Contacts',
70
+ noun: 'contacts',
71
+ scope: 'orgData',
72
+ collection: 'contacts',
73
+ nameField: 'name',
74
+ fallbackNameField: 'email',
75
+ extraFields: [
76
+ 'email',
77
+ 'phone',
78
+ 'companyName'
79
+ ],
80
+ order: 40,
81
+ href: (row, context)=>recordHref(row, (id)=>crmHubRoutes(context).contact(id))
82
+ },
83
+ {
84
+ /*
85
+ * People captured and not yet qualified, by name or address. One org
86
+ * document per person, scoped by `visibleTo` like a contact (AGL-3275),
87
+ * so the group reads and gates exactly as contacts do.
88
+ */ id: 'leads',
89
+ group: 'Leads',
90
+ noun: 'leads',
91
+ scope: 'orgData',
92
+ collection: 'leads',
93
+ nameField: 'name',
94
+ fallbackNameField: 'email',
95
+ extraFields: [
96
+ 'email'
97
+ ],
98
+ order: 41,
99
+ href: (row, context)=>recordHref(row, (id)=>crmHubRoutes(context).lead(id))
100
+ },
101
+ {
102
+ // A company by its name or its domain (AGL-2622).
103
+ id: 'companies',
104
+ group: 'Companies',
105
+ noun: 'companies',
106
+ scope: 'orgData',
107
+ collection: CRM_COLLECTIONS.companies,
108
+ nameField: 'name',
109
+ extraFields: [
110
+ 'domain'
111
+ ],
112
+ order: 42,
113
+ href: (row, context)=>recordHref(row, (id)=>crmHubRoutes(context).company(id))
114
+ },
115
+ {
116
+ id: 'deals',
117
+ group: 'Deals',
118
+ noun: 'deals',
119
+ scope: 'orgData',
120
+ collection: CRM_COLLECTIONS.deals,
121
+ nameField: 'title',
122
+ order: 43,
123
+ href: (row, context)=>recordHref(row, (id)=>crmHubRoutes(context).deal(id))
124
+ },
125
+ {
126
+ /*
127
+ * The team's own work (AGL-2662): a task by its title or notes. The
128
+ * collection is the PREFIXED name — `crmTasks` — because `tasks` is a
129
+ * word the org document wants for other things.
130
+ */ id: 'tasks',
131
+ group: 'Tasks',
132
+ noun: 'tasks',
133
+ scope: 'orgData',
134
+ collection: CRM_COLLECTIONS.tasks,
135
+ nameField: 'title',
136
+ extraFields: [
137
+ 'notes'
138
+ ],
139
+ order: 44,
140
+ href: (row, context)=>workHref('task', row, context)
141
+ },
142
+ {
143
+ // A sent email carries a subject; everything a person logs by hand
144
+ // carries only the body it was written into, and a row labeled by its
145
+ // document id is a row nobody recognizes.
146
+ id: 'activities',
147
+ group: 'Activities',
148
+ noun: 'activities',
149
+ scope: 'orgData',
150
+ collection: CRM_COLLECTIONS.activities,
151
+ nameField: 'subject',
152
+ fallbackNameField: 'body',
153
+ extraFields: [
154
+ 'body',
155
+ 'outcome'
156
+ ],
157
+ order: 45,
158
+ href: (row, context)=>workHref('activity', row, context)
159
+ }
160
+ ];
161
+
162
+ //# sourceMappingURL=crm-search-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-search-sources.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleSearchSource } from '@aglyn/aglyn'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport type { PluginRecordRouteContext } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { crmHubRoutes } from './crm-record-routes'\n\n/**\n * What the console's search finds in the CRM (AGL-3080): people, leads,\n * companies, deals, and the team's tasks and activities, each opening on its\n * record in the hub at the scope the reader stands at.\n *\n * Every group is the organization's shared data, read through the reader's\n * `visibleTo` tokens under a site and unfiltered at the organization level,\n * where only an org-wide member is offered it — the same reading the hub's\n * own lists make. The extension's `crm` plan flag and `data.manage`\n * permission gate all six, as they gate the hub the rows open in.\n *\n * Listed after the site's own pages and emails (30) and before its building\n * blocks (100): a person is the thing most often looked up after a page.\n */\n\n/** A row's field as the plain string an address is built from. */\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\n/** One record's page, or `null` for a row without an id. */\nfunction recordHref(\n row: Readonly<Record<string, unknown>>,\n open: (id: string) => string,\n): string | null {\n const id = text(row['$id'])\n return id ? open(id) : null\n}\n\n/**\n * Where a task or an activity opens: the record it was filed under.\n *\n * Neither has a page of its own — a task is a row in a record's Tasks card\n * and in the Tasks list, an activity a line on a timeline — so the useful\n * destination is the deal, company, person or lead it belongs to, in that\n * order: a task filed on a deal is about that deal, and landing on the\n * contact would make the reader find it again. A lead addresses by its id\n * alone at both levels (AGL-3275), so one filed on a lead opens the lead.\n *\n * A task that names no record — a standing task somebody typed into the list\n * — lands on the Tasks section, which is where it is. An activity that names\n * none has nowhere to go and answers `null`, which drops the row.\n */\nfunction workHref(\n kind: 'task' | 'activity',\n row: Readonly<Record<string, unknown>>,\n context: PluginRecordRouteContext,\n): string | null {\n const routes = crmHubRoutes(context)\n const dealId = text(row['dealId'])\n if (dealId) return routes.deal(dealId)\n const companyId = text(row['companyId'])\n if (companyId) return routes.company(companyId)\n const contactId = text(row['contactId'])\n if (contactId) return routes.contact(contactId)\n const leadId = text(row['leadId'])\n if (leadId) return routes.lead(leadId)\n return kind === 'task' ? routes.section('tasks') : null\n}\n\nexport const CRM_SEARCH_SOURCES: readonly ConsoleSearchSource[] = [\n {\n /*\n * People, by name, email, phone number or company (AGL-2596). The phone\n * and the company name are top-level echoes of the viewing holder's\n * facet, written by every path that sets them, precisely so a read that\n * never resolves a facet can hit them. A person a checkout captured with\n * no name is labeled by the address, as the Contacts list labels them.\n */\n id: 'contacts',\n group: 'Contacts',\n noun: 'contacts',\n scope: 'orgData',\n collection: 'contacts',\n nameField: 'name',\n fallbackNameField: 'email',\n extraFields: ['email', 'phone', 'companyName'],\n order: 40,\n href: (row, context) =>\n recordHref(row, (id) => crmHubRoutes(context).contact(id)),\n },\n {\n /*\n * People captured and not yet qualified, by name or address. One org\n * document per person, scoped by `visibleTo` like a contact (AGL-3275),\n * so the group reads and gates exactly as contacts do.\n */\n id: 'leads',\n group: 'Leads',\n noun: 'leads',\n scope: 'orgData',\n collection: 'leads',\n nameField: 'name',\n fallbackNameField: 'email',\n extraFields: ['email'],\n order: 41,\n href: (row, context) =>\n recordHref(row, (id) => crmHubRoutes(context).lead(id)),\n },\n {\n // A company by its name or its domain (AGL-2622).\n id: 'companies',\n group: 'Companies',\n noun: 'companies',\n scope: 'orgData',\n collection: CRM_COLLECTIONS.companies,\n nameField: 'name',\n extraFields: ['domain'],\n order: 42,\n href: (row, context) =>\n recordHref(row, (id) => crmHubRoutes(context).company(id)),\n },\n {\n id: 'deals',\n group: 'Deals',\n noun: 'deals',\n scope: 'orgData',\n collection: CRM_COLLECTIONS.deals,\n nameField: 'title',\n order: 43,\n href: (row, context) =>\n recordHref(row, (id) => crmHubRoutes(context).deal(id)),\n },\n {\n /*\n * The team's own work (AGL-2662): a task by its title or notes. The\n * collection is the PREFIXED name — `crmTasks` — because `tasks` is a\n * word the org document wants for other things.\n */\n id: 'tasks',\n group: 'Tasks',\n noun: 'tasks',\n scope: 'orgData',\n collection: CRM_COLLECTIONS.tasks,\n nameField: 'title',\n extraFields: ['notes'],\n order: 44,\n href: (row, context) => workHref('task', row, context),\n },\n {\n // A sent email carries a subject; everything a person logs by hand\n // carries only the body it was written into, and a row labeled by its\n // document id is a row nobody recognizes.\n id: 'activities',\n group: 'Activities',\n noun: 'activities',\n scope: 'orgData',\n collection: CRM_COLLECTIONS.activities,\n nameField: 'subject',\n fallbackNameField: 'body',\n extraFields: ['body', 'outcome'],\n order: 45,\n href: (row, context) => workHref('activity', row, context),\n },\n]\n"],"names":["CRM_COLLECTIONS","crmHubRoutes","text","value","recordHref","row","open","id","workHref","kind","context","routes","dealId","deal","companyId","company","contactId","contact","leadId","lead","section","CRM_SEARCH_SOURCES","group","noun","scope","collection","nameField","fallbackNameField","extraFields","order","href","companies","deals","tasks","activities"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
18
+ import { type CrmSharingAction, type CrmSharingObject, type CrmSharingResponse } from './crm-sharing';
19
+ import type { CrmTaskRouteScope } from './task-routes';
20
+ /**
21
+ * One call to `crm/sharing` (AGL-3336). Answers the route's body, or throws
22
+ * with the route's own sentence so a snackbar can say it.
23
+ */
24
+ export declare function callCrmSharing(user: MaybeTokenSource, scope: CrmTaskRouteScope, action: CrmSharingAction, body?: Record<string, unknown>): Promise<CrmSharingResponse>;
25
+ /**
26
+ * What a CLIENT-DIRECT write to a lead, a company or a deal owes the org's
27
+ * sharing rules: the server writers are followed by the core's
28
+ * record-written seam, and a browser that wrote the record itself asks for
29
+ * the same re-evaluation here. Never throws, and never holds the save — the
30
+ * record is written whether or not a rule then shares it.
31
+ */
32
+ export declare function followUpCrmSharing(user: MaybeTokenSource, scope: CrmTaskRouteScope | null, object: CrmSharingObject, ids: readonly string[]): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
18
+ import { CRM_SHARING_IDS_MAX, crmSharingRouteUrl } from "./crm-sharing.js";
19
+ /**
20
+ * One call to `crm/sharing` (AGL-3336). Answers the route's body, or throws
21
+ * with the route's own sentence so a snackbar can say it.
22
+ */ export async function callCrmSharing(user, scope, action, body = {}) {
23
+ const response = await authorizedFetch(user, crmSharingRouteUrl(), {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json'
27
+ },
28
+ body: JSON.stringify(_extends({}, scope, body, {
29
+ action
30
+ }))
31
+ });
32
+ const answer = await response.json().catch(()=>({}));
33
+ if (!response.ok || answer.ok !== true) {
34
+ throw new Error(answer.error || 'The sharing could not be saved.');
35
+ }
36
+ return answer;
37
+ }
38
+ /**
39
+ * What a CLIENT-DIRECT write to a lead, a company or a deal owes the org's
40
+ * sharing rules: the server writers are followed by the core's
41
+ * record-written seam, and a browser that wrote the record itself asks for
42
+ * the same re-evaluation here. Never throws, and never holds the save — the
43
+ * record is written whether or not a rule then shares it.
44
+ */ export async function followUpCrmSharing(user, scope, object, ids) {
45
+ const unique = [
46
+ ...new Set(ids.filter(Boolean))
47
+ ].slice(0, CRM_SHARING_IDS_MAX);
48
+ if (!scope || !unique.length) return;
49
+ try {
50
+ await callCrmSharing(user, scope, 'evaluate', {
51
+ object,
52
+ ids: unique
53
+ });
54
+ } catch (error) {
55
+ console.warn('[crm] sharing rules were not re-evaluated', error);
56
+ }
57
+ }
58
+
59
+ //# sourceMappingURL=crm-sharing-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-sharing-api.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 authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport {\n CRM_SHARING_IDS_MAX,\n type CrmSharingAction,\n type CrmSharingObject,\n type CrmSharingResponse,\n crmSharingRouteUrl,\n} from './crm-sharing'\nimport type { CrmTaskRouteScope } from './task-routes'\n\n/**\n * One call to `crm/sharing` (AGL-3336). Answers the route's body, or throws\n * with the route's own sentence so a snackbar can say it.\n */\nexport async function callCrmSharing(\n user: MaybeTokenSource,\n scope: CrmTaskRouteScope,\n action: CrmSharingAction,\n body: Record<string, unknown> = {},\n): Promise<CrmSharingResponse> {\n const response = await authorizedFetch(user, crmSharingRouteUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...scope, ...body, action }),\n })\n const answer = (await response.json().catch(() => ({}))) as Partial<CrmSharingResponse> & {\n error?: string\n }\n if (!response.ok || answer.ok !== true) {\n throw new Error(answer.error || 'The sharing could not be saved.')\n }\n return answer as CrmSharingResponse\n}\n\n/**\n * What a CLIENT-DIRECT write to a lead, a company or a deal owes the org's\n * sharing rules: the server writers are followed by the core's\n * record-written seam, and a browser that wrote the record itself asks for\n * the same re-evaluation here. Never throws, and never holds the save — the\n * record is written whether or not a rule then shares it.\n */\nexport async function followUpCrmSharing(\n user: MaybeTokenSource,\n scope: CrmTaskRouteScope | null,\n object: CrmSharingObject,\n ids: readonly string[],\n): Promise<void> {\n const unique = [...new Set(ids.filter(Boolean))].slice(0, CRM_SHARING_IDS_MAX)\n if (!scope || !unique.length) return\n try {\n await callCrmSharing(user, scope, 'evaluate', { object, ids: unique })\n } catch (error) {\n console.warn('[crm] sharing rules were not re-evaluated', error)\n }\n}\n"],"names":["authorizedFetch","CRM_SHARING_IDS_MAX","crmSharingRouteUrl","callCrmSharing","user","scope","action","body","response","method","headers","JSON","stringify","answer","json","catch","ok","Error","error","followUpCrmSharing","object","ids","unique","Set","filter","Boolean","slice","length","console","warn"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,eAAe,QAEV,2CAA0C;AACjD,SACEC,mBAAmB,EAInBC,kBAAkB,QACb,mBAAe;AAGtB;;;CAGC,GACD,OAAO,eAAeC,eACpBC,IAAsB,EACtBC,KAAwB,EACxBC,MAAwB,EACxBC,OAAgC,CAAC,CAAC;IAElC,MAAMC,WAAW,MAAMR,gBAAgBI,MAAMF,sBAAsB;QACjEO,QAAQ;QACRC,SAAS;YAAE,gBAAgB;QAAmB;QAC9CH,MAAMI,KAAKC,SAAS,CAAC,aAAKP,OAAUE;YAAMD;;IAC5C;IACA,MAAMO,SAAU,MAAML,SAASM,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;IAGpD,IAAI,CAACP,SAASQ,EAAE,IAAIH,OAAOG,EAAE,KAAK,MAAM;QACtC,MAAM,IAAIC,MAAMJ,OAAOK,KAAK,IAAI;IAClC;IACA,OAAOL;AACT;AAEA;;;;;;CAMC,GACD,OAAO,eAAeM,mBACpBf,IAAsB,EACtBC,KAA+B,EAC/Be,MAAwB,EACxBC,GAAsB;IAEtB,MAAMC,SAAS;WAAI,IAAIC,IAAIF,IAAIG,MAAM,CAACC;KAAU,CAACC,KAAK,CAAC,GAAGzB;IAC1D,IAAI,CAACI,SAAS,CAACiB,OAAOK,MAAM,EAAE;IAC9B,IAAI;QACF,MAAMxB,eAAeC,MAAMC,OAAO,YAAY;YAAEe;YAAQC,KAAKC;QAAO;IACtE,EAAE,OAAOJ,OAAO;QACdU,QAAQC,IAAI,CAAC,6CAA6CX;IAC5D;AACF"}