@aglyn/plugins-crm 1.0.0 → 1.2.0

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 (166) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-link-fields.d.ts +34 -0
  3. package/src/lib/components/activity-link-fields.js +141 -0
  4. package/src/lib/components/activity-link-fields.js.map +1 -0
  5. package/src/lib/components/activity-list.d.ts +8 -0
  6. package/src/lib/components/activity-list.js +4 -2
  7. package/src/lib/components/activity-list.js.map +1 -1
  8. package/src/lib/components/activity-queries.d.ts +7 -2
  9. package/src/lib/components/activity-queries.js +7 -3
  10. package/src/lib/components/activity-queries.js.map +1 -1
  11. package/src/lib/components/company-contact-roles-card.d.ts +42 -0
  12. package/src/lib/components/company-contact-roles-card.js +137 -0
  13. package/src/lib/components/company-contact-roles-card.js.map +1 -0
  14. package/src/lib/components/company-detail-page.js +12 -1
  15. package/src/lib/components/company-detail-page.js.map +1 -1
  16. package/src/lib/components/company-edit-drawer.js +9 -6
  17. package/src/lib/components/company-edit-drawer.js.map +1 -1
  18. package/src/lib/components/company-picker.js +4 -2
  19. package/src/lib/components/company-picker.js.map +1 -1
  20. package/src/lib/components/contact-associations-card.js +15 -1
  21. package/src/lib/components/contact-associations-card.js.map +1 -1
  22. package/src/lib/components/contact-detail-page.js +13 -14
  23. package/src/lib/components/contact-detail-page.js.map +1 -1
  24. package/src/lib/components/contact-known-by-card.d.ts +2 -1
  25. package/src/lib/components/contact-known-by-card.js +16 -3
  26. package/src/lib/components/contact-known-by-card.js.map +1 -1
  27. package/src/lib/components/contact-list-columns.js +5 -4
  28. package/src/lib/components/contact-list-columns.js.map +1 -1
  29. package/src/lib/components/contact-properties-card.js +41 -20
  30. package/src/lib/components/contact-properties-card.js.map +1 -1
  31. package/src/lib/components/contact-roles-editor.d.ts +54 -0
  32. package/src/lib/components/contact-roles-editor.js +270 -0
  33. package/src/lib/components/contact-roles-editor.js.map +1 -0
  34. package/src/lib/components/contacts-section.js +6 -4
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-console-sections.js +1 -1
  37. package/src/lib/components/crm-console-sections.js.map +1 -1
  38. package/src/lib/components/crm-record-picker.d.ts +19 -0
  39. package/src/lib/components/crm-record-picker.js +40 -11
  40. package/src/lib/components/crm-record-picker.js.map +1 -1
  41. package/src/lib/components/crm-related-to-picker.d.ts +33 -0
  42. package/src/lib/components/crm-related-to-picker.js +135 -0
  43. package/src/lib/components/crm-related-to-picker.js.map +1 -0
  44. package/src/lib/components/crm-site-picker.d.ts +13 -1
  45. package/src/lib/components/crm-site-picker.js +15 -6
  46. package/src/lib/components/crm-site-picker.js.map +1 -1
  47. package/src/lib/components/crm-transfer-buttons.d.ts +5 -0
  48. package/src/lib/components/crm-transfer-buttons.js +9 -6
  49. package/src/lib/components/crm-transfer-buttons.js.map +1 -1
  50. package/src/lib/components/deal-contact-roles-card.js +21 -239
  51. package/src/lib/components/deal-contact-roles-card.js.map +1 -1
  52. package/src/lib/components/deal-detail-page.js +2 -1
  53. package/src/lib/components/deal-detail-page.js.map +1 -1
  54. package/src/lib/components/deal-edit-drawer.js +11 -7
  55. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  56. package/src/lib/components/deals-section.js +5 -4
  57. package/src/lib/components/deals-section.js.map +1 -1
  58. package/src/lib/components/log-activity-dialog.d.ts +27 -9
  59. package/src/lib/components/log-activity-dialog.js +96 -32
  60. package/src/lib/components/log-activity-dialog.js.map +1 -1
  61. package/src/lib/components/new-contact-drawer.d.ts +7 -3
  62. package/src/lib/components/new-contact-drawer.js +27 -15
  63. package/src/lib/components/new-contact-drawer.js.map +1 -1
  64. package/src/lib/components/pipeline-stages-dialog.js +5 -1
  65. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  66. package/src/lib/components/record-activity-card.d.ts +11 -2
  67. package/src/lib/components/record-activity-card.js +90 -5
  68. package/src/lib/components/record-activity-card.js.map +1 -1
  69. package/src/lib/components/settings-section.d.ts +4 -2
  70. package/src/lib/components/settings-section.js +7 -5
  71. package/src/lib/components/settings-section.js.map +1 -1
  72. package/src/lib/hooks/crm-activity-client-reads.d.ts +8 -0
  73. package/src/lib/hooks/crm-activity-client-reads.js +50 -0
  74. package/src/lib/hooks/crm-activity-client-reads.js.map +1 -0
  75. package/src/lib/hooks/use-campaign-filing-log.d.ts +2 -1
  76. package/src/lib/hooks/use-campaign-filing-log.js +10 -3
  77. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -1
  78. package/src/lib/hooks/use-crm-org-mount.d.ts +8 -6
  79. package/src/lib/hooks/use-crm-org-mount.js +2 -1
  80. package/src/lib/hooks/use-crm-org-mount.js.map +1 -1
  81. package/src/lib/hooks/use-crm-record-names.d.ts +3 -2
  82. package/src/lib/hooks/use-crm-record-names.js +4 -4
  83. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  84. package/src/lib/hooks/use-crm-scope.d.ts +17 -5
  85. package/src/lib/hooks/use-crm-scope.js +12 -5
  86. package/src/lib/hooks/use-crm-scope.js.map +1 -1
  87. package/src/lib/model/contact-holder.d.ts +12 -0
  88. package/src/lib/model/contact-holder.js +20 -1
  89. package/src/lib/model/contact-holder.js.map +1 -1
  90. package/src/lib/model/contact-record.d.ts +5 -0
  91. package/src/lib/model/contact-record.js +7 -1
  92. package/src/lib/model/contact-record.js.map +1 -1
  93. package/src/lib/model/contact-update.d.ts +6 -0
  94. package/src/lib/model/contact-update.js +1 -0
  95. package/src/lib/model/contact-update.js.map +1 -1
  96. package/src/lib/model/crm-activity-link.d.ts +117 -0
  97. package/src/lib/model/crm-activity-link.js +113 -0
  98. package/src/lib/model/crm-activity-link.js.map +1 -0
  99. package/src/lib/model/crm-api.d.ts +4 -1
  100. package/src/lib/model/crm-api.js +9 -5
  101. package/src/lib/model/crm-api.js.map +1 -1
  102. package/src/lib/model/deal-board-model.js +5 -3
  103. package/src/lib/model/deal-board-model.js.map +1 -1
  104. package/src/lib/model/deal-contact-roles.d.ts +25 -1
  105. package/src/lib/model/deal-contact-roles.js +30 -4
  106. package/src/lib/model/deal-contact-roles.js.map +1 -1
  107. package/src/lib/model/native-vocabulary.d.ts +9 -0
  108. package/src/lib/model/native-vocabulary.js +13 -0
  109. package/src/lib/model/native-vocabulary.js.map +1 -1
  110. package/src/lib/model/settle-stage-names.d.ts +25 -0
  111. package/src/lib/model/settle-stage-names.js +29 -0
  112. package/src/lib/model/settle-stage-names.js.map +1 -0
  113. package/src/lib/native-nav.js +1 -1
  114. package/src/lib/native-nav.js.map +1 -1
  115. package/src/lib/server/add-contact-by-hand.d.ts +84 -0
  116. package/src/lib/server/add-contact-by-hand.js +212 -0
  117. package/src/lib/server/add-contact-by-hand.js.map +1 -0
  118. package/src/lib/server/api-v1/contacts.js +61 -23
  119. package/src/lib/server/api-v1/contacts.js.map +1 -1
  120. package/src/lib/server/api-v1/crm-activities.js +13 -3
  121. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  122. package/src/lib/server/api-v1/crm-pipelines.d.ts +1 -1
  123. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  124. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  125. package/src/lib/server/api-v1/crm-shared.d.ts +16 -9
  126. package/src/lib/server/api-v1/crm-shared.js +23 -11
  127. package/src/lib/server/api-v1/crm-shared.js.map +1 -1
  128. package/src/lib/server/api-v1/openapi.js +3 -3
  129. package/src/lib/server/api-v1/openapi.js.map +1 -1
  130. package/src/lib/server/automation-steps.js +11 -9
  131. package/src/lib/server/automation-steps.js.map +1 -1
  132. package/src/lib/server/campaign-filing-activity.js +7 -2
  133. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  134. package/src/lib/server/contact-update.d.ts +4 -0
  135. package/src/lib/server/contact-update.js +71 -10
  136. package/src/lib/server/contact-update.js.map +1 -1
  137. package/src/lib/server/convert-host-lead.js +13 -2
  138. package/src/lib/server/convert-host-lead.js.map +1 -1
  139. package/src/lib/server/convert-open-lead.js +8 -1
  140. package/src/lib/server/convert-open-lead.js.map +1 -1
  141. package/src/lib/server/crm-activity-company.d.ts +25 -0
  142. package/src/lib/server/crm-activity-company.js +34 -0
  143. package/src/lib/server/crm-activity-company.js.map +1 -0
  144. package/src/lib/server/crm-inbound-email.js +21 -15
  145. package/src/lib/server/crm-inbound-email.js.map +1 -1
  146. package/src/lib/server/crm-sharing.js +9 -2
  147. package/src/lib/server/crm-sharing.js.map +1 -1
  148. package/src/lib/server/deal-contact-roles.d.ts +2 -0
  149. package/src/lib/server/deal-contact-roles.js +38 -1
  150. package/src/lib/server/deal-contact-roles.js.map +1 -1
  151. package/src/lib/server/email-send.js +10 -8
  152. package/src/lib/server/email-send.js.map +1 -1
  153. package/src/lib/server/merge-contacts.js +6 -2
  154. package/src/lib/server/merge-contacts.js.map +1 -1
  155. package/src/lib/server/org-caller.d.ts +8 -0
  156. package/src/lib/server/org-caller.js +10 -0
  157. package/src/lib/server/org-caller.js.map +1 -1
  158. package/src/lib/server/record-timeline.d.ts +3 -1
  159. package/src/lib/server/record-timeline.js +11 -3
  160. package/src/lib/server/record-timeline.js.map +1 -1
  161. package/src/lib/server.js +152 -43
  162. package/src/lib/server.js.map +1 -1
  163. package/src/lib/transfer/contacts.js +83 -48
  164. package/src/lib/transfer/contacts.js.map +1 -1
  165. package/src/lib/transfer/fields.js +1 -2
  166. package/src/lib/transfer/fields.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deal-contact-roles.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type CrmDealContactRole,\n type CrmPicklist,\n DEAL_CONTACT_ROLES_MAX,\n dealContactRoleFields,\n dealContactRolesOf,\n judgeDealContactRoles,\n} from '@aglyn/aglyn'\n\n/*\n * THE DEAL PAGE'S CONTACT ROLES CARD (AGL-3521), as pure functions: what\n * each edit makes of the list, and the fields one save writes.\n */\n\n/** Why a contact cannot be added to the deal, or `null` when it can. */\nexport function dealContactRoleAddProblem(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n): string | null {\n if (!contactId) return 'Pick a contact.'\n if (roles.some((row) => row.contactId === contactId)) return 'That contact is already on this deal.'\n if (roles.length >= DEAL_CONTACT_ROLES_MAX) {\n return `A deal names at most ${DEAL_CONTACT_ROLES_MAX} contacts.`\n }\n return null\n}\n\n/** `roles` with one contact added — as the Primary, when asked, which every other row then is not. */\nexport function dealContactRolesAdded(\n roles: readonly CrmDealContactRole[],\n added: { contactId: string; role?: string; primary: boolean },\n): CrmDealContactRole[] {\n const row: CrmDealContactRole = {\n contactId: added.contactId,\n ...(added.role ? { role: added.role } : {}),\n primary: added.primary,\n }\n return [...roles.map((entry) => (added.primary ? { ...entry, primary: false } : entry)), row]\n}\n\n/** `roles` with one contact's role set — `''` for none. */\nexport function dealContactRolesWithRole(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n role: string,\n): CrmDealContactRole[] {\n return roles.map((row) => {\n if (row.contactId !== contactId) return row\n return { contactId: row.contactId, ...(role ? { role } : {}), primary: row.primary }\n })\n}\n\n/**\n * What a save of the card writes: the roles judged against the org's list\n * (a contact's current role kept), `contactId` as their Primary — `null`\n * for a field the caller deletes — and whether the Primary changed, which\n * is when the name the drawer copied beside `contactId` goes stale.\n */\nexport function dealContactRolesSave(\n deal: { contactId?: unknown; contactRoles?: unknown },\n next: readonly CrmDealContactRole[],\n picklist: CrmPicklist,\n):\n | {\n ok: true\n contactRoles: CrmDealContactRole[]\n contactId: string | null\n primaryChanged: boolean\n }\n | { ok: false; error: string } {\n const judged = judgeDealContactRoles(picklist, next, dealContactRolesOf(deal))\n if (judged.ok === false) return judged\n const fields = dealContactRoleFields(judged.roles)\n const before = typeof deal.contactId === 'string' && deal.contactId ? deal.contactId : null\n return { ok: true, ...fields, primaryChanged: fields.contactId !== before }\n}\n"],"names":["DEAL_CONTACT_ROLES_MAX","dealContactRoleFields","dealContactRolesOf","judgeDealContactRoles","dealContactRoleAddProblem","roles","contactId","some","row","length","dealContactRolesAdded","added","role","primary","map","entry","dealContactRolesWithRole","dealContactRolesSave","deal","next","picklist","judged","ok","fields","before","primaryChanged"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAGEA,sBAAsB,EACtBC,qBAAqB,EACrBC,kBAAkB,EAClBC,qBAAqB,QAChB,eAAc;AAErB;;;CAGC,GAED,sEAAsE,GACtE,OAAO,SAASC,0BACdC,KAAoC,EACpCC,SAAiB;IAEjB,IAAI,CAACA,WAAW,OAAO;IACvB,IAAID,MAAME,IAAI,CAAC,CAACC,MAAQA,IAAIF,SAAS,KAAKA,YAAY,OAAO;IAC7D,IAAID,MAAMI,MAAM,IAAIT,wBAAwB;QAC1C,OAAO,CAAC,qBAAqB,EAAEA,uBAAuB,UAAU,CAAC;IACnE;IACA,OAAO;AACT;AAEA,oGAAoG,GACpG,OAAO,SAASU,sBACdL,KAAoC,EACpCM,KAA6D;IAE7D,MAAMH,MAA0B;QAC9BF,WAAWK,MAAML,SAAS;OACtBK,MAAMC,IAAI,GAAG;QAAEA,MAAMD,MAAMC,IAAI;IAAC,IAAI,CAAC;QACzCC,SAASF,MAAME,OAAO;;IAExB,OAAO;WAAIR,MAAMS,GAAG,CAAC,CAACC,QAAWJ,MAAME,OAAO,GAAG,aAAKE;gBAAOF,SAAS;iBAAUE;QAASP;KAAI;AAC/F;AAEA,yDAAyD,GACzD,OAAO,SAASQ,yBACdX,KAAoC,EACpCC,SAAiB,EACjBM,IAAY;IAEZ,OAAOP,MAAMS,GAAG,CAAC,CAACN;QAChB,IAAIA,IAAIF,SAAS,KAAKA,WAAW,OAAOE;QACxC,OAAO;YAAEF,WAAWE,IAAIF,SAAS;WAAMM,OAAO;YAAEA;QAAK,IAAI,CAAC;YAAIC,SAASL,IAAIK,OAAO;;IACpF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASI,qBACdC,IAAqD,EACrDC,IAAmC,EACnCC,QAAqB;IASrB,MAAMC,SAASlB,sBAAsBiB,UAAUD,MAAMjB,mBAAmBgB;IACxE,IAAIG,OAAOC,EAAE,KAAK,OAAO,OAAOD;IAChC,MAAME,SAAStB,sBAAsBoB,OAAOhB,KAAK;IACjD,MAAMmB,SAAS,OAAON,KAAKZ,SAAS,KAAK,YAAYY,KAAKZ,SAAS,GAAGY,KAAKZ,SAAS,GAAG;IACvF,OAAO;QAAEgB,IAAI;OAASC;QAAQE,gBAAgBF,OAAOjB,SAAS,KAAKkB;;AACrE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deal-contact-roles.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type CrmDealContactRole,\n type CrmPicklist,\n crmDealContactRoleListFields,\n DEAL_CONTACT_ROLES_MAX,\n dealContactRoleFields,\n dealContactRolesOf,\n judgeDealContactRoles,\n readDealContactRoles,\n} from '@aglyn/aglyn'\n\n/*\n * THE DEAL PAGE'S CONTACT ROLES CARD (AGL-3521), as pure functions: what\n * each edit makes of the list, and the fields one save writes.\n */\n\n/** Why a contact cannot be added to the deal, or `null` when it can. */\nexport function dealContactRoleAddProblem(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n noun: 'deal' | 'company' = 'deal',\n): string | null {\n if (!contactId) return 'Pick a contact.'\n if (roles.some((row) => row.contactId === contactId)) {\n return `That contact is already on this ${noun}.`\n }\n if (roles.length >= DEAL_CONTACT_ROLES_MAX) {\n return `A ${noun} names at most ${DEAL_CONTACT_ROLES_MAX} contacts.`\n }\n return null\n}\n\n/** `roles` with one contact added — as the Primary, when asked, which every other row then is not. */\nexport function dealContactRolesAdded(\n roles: readonly CrmDealContactRole[],\n added: { contactId: string; role?: string; primary: boolean },\n): CrmDealContactRole[] {\n const row: CrmDealContactRole = {\n contactId: added.contactId,\n ...(added.role ? { role: added.role } : {}),\n primary: added.primary,\n }\n return [...roles.map((entry) => (added.primary ? { ...entry, primary: false } : entry)), row]\n}\n\n/** `roles` with one contact's role set — `''` for none. */\nexport function dealContactRolesWithRole(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n role: string,\n): CrmDealContactRole[] {\n return roles.map((row) => {\n if (row.contactId !== contactId) return row\n return { contactId: row.contactId, ...(role ? { role } : {}), primary: row.primary }\n })\n}\n\n/**\n * What a save of the card writes: the roles judged against the org's list\n * (a contact's current role kept), `contactId` as their Primary — `null`\n * for a field the caller deletes — and whether the Primary changed, which\n * is when the name the drawer copied beside `contactId` goes stale.\n */\nexport function dealContactRolesSave(\n deal: { contactId?: unknown; contactRoles?: unknown },\n next: readonly CrmDealContactRole[],\n picklist: CrmPicklist,\n):\n | {\n ok: true\n contactRoles: CrmDealContactRole[]\n contactId: string | null\n primaryChanged: boolean\n }\n | { ok: false; error: string } {\n const judged = judgeDealContactRoles(picklist, next, dealContactRolesOf(deal))\n if (judged.ok === false) return judged\n const fields = dealContactRoleFields(judged.roles)\n const before = typeof deal.contactId === 'string' && deal.contactId ? deal.contactId : null\n return { ok: true, ...fields, primaryChanged: fields.contactId !== before }\n}\n\n/**\n * The roles a company holds, as stored. A deal's reader takes the Primary from\n * the deal's `contactId`; a company has none, so its Primary is on the row.\n */\nexport function companyContactRolesOf(company: { contactRoles?: unknown }): CrmDealContactRole[] {\n return readDealContactRoles(company.contactRoles)\n}\n\n/**\n * What a save of a company's card writes: the roles judged against the org's\n * list, the ids a merge or an erasure finds the company by, and the keys a\n * rename of a role does. A company has no `contactId` beside them, so nothing\n * else moves with the Primary.\n */\nexport function companyContactRolesSave(\n company: { contactRoles?: unknown },\n next: readonly CrmDealContactRole[],\n picklist: CrmPicklist,\n):\n | {\n ok: true\n contactRoles: CrmDealContactRole[]\n contactRoleContactIds: string[]\n contactRoleKeys: string[]\n }\n | { ok: false; error: string } {\n const judged = judgeDealContactRoles(picklist, next, companyContactRolesOf(company))\n if (judged.ok === false) return judged\n const { contactRoleContactIds, contactRoleKeys } = crmDealContactRoleListFields({\n contactRoles: judged.roles,\n })\n return { ok: true, contactRoles: judged.roles, contactRoleContactIds, contactRoleKeys }\n}\n"],"names":["crmDealContactRoleListFields","DEAL_CONTACT_ROLES_MAX","dealContactRoleFields","dealContactRolesOf","judgeDealContactRoles","readDealContactRoles","dealContactRoleAddProblem","roles","contactId","noun","some","row","length","dealContactRolesAdded","added","role","primary","map","entry","dealContactRolesWithRole","dealContactRolesSave","deal","next","picklist","judged","ok","fields","before","primaryChanged","companyContactRolesOf","company","contactRoles","companyContactRolesSave","contactRoleContactIds","contactRoleKeys"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAGEA,4BAA4B,EAC5BC,sBAAsB,EACtBC,qBAAqB,EACrBC,kBAAkB,EAClBC,qBAAqB,EACrBC,oBAAoB,QACf,eAAc;AAErB;;;CAGC,GAED,sEAAsE,GACtE,OAAO,SAASC,0BACdC,KAAoC,EACpCC,SAAiB,EACjBC,OAA2B,MAAM;IAEjC,IAAI,CAACD,WAAW,OAAO;IACvB,IAAID,MAAMG,IAAI,CAAC,CAACC,MAAQA,IAAIH,SAAS,KAAKA,YAAY;QACpD,OAAO,CAAC,gCAAgC,EAAEC,KAAK,CAAC,CAAC;IACnD;IACA,IAAIF,MAAMK,MAAM,IAAIX,wBAAwB;QAC1C,OAAO,CAAC,EAAE,EAAEQ,KAAK,eAAe,EAAER,uBAAuB,UAAU,CAAC;IACtE;IACA,OAAO;AACT;AAEA,oGAAoG,GACpG,OAAO,SAASY,sBACdN,KAAoC,EACpCO,KAA6D;IAE7D,MAAMH,MAA0B;QAC9BH,WAAWM,MAAMN,SAAS;OACtBM,MAAMC,IAAI,GAAG;QAAEA,MAAMD,MAAMC,IAAI;IAAC,IAAI,CAAC;QACzCC,SAASF,MAAME,OAAO;;IAExB,OAAO;WAAIT,MAAMU,GAAG,CAAC,CAACC,QAAWJ,MAAME,OAAO,GAAG,aAAKE;gBAAOF,SAAS;iBAAUE;QAASP;KAAI;AAC/F;AAEA,yDAAyD,GACzD,OAAO,SAASQ,yBACdZ,KAAoC,EACpCC,SAAiB,EACjBO,IAAY;IAEZ,OAAOR,MAAMU,GAAG,CAAC,CAACN;QAChB,IAAIA,IAAIH,SAAS,KAAKA,WAAW,OAAOG;QACxC,OAAO;YAAEH,WAAWG,IAAIH,SAAS;WAAMO,OAAO;YAAEA;QAAK,IAAI,CAAC;YAAIC,SAASL,IAAIK,OAAO;;IACpF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASI,qBACdC,IAAqD,EACrDC,IAAmC,EACnCC,QAAqB;IASrB,MAAMC,SAASpB,sBAAsBmB,UAAUD,MAAMnB,mBAAmBkB;IACxE,IAAIG,OAAOC,EAAE,KAAK,OAAO,OAAOD;IAChC,MAAME,SAASxB,sBAAsBsB,OAAOjB,KAAK;IACjD,MAAMoB,SAAS,OAAON,KAAKb,SAAS,KAAK,YAAYa,KAAKb,SAAS,GAAGa,KAAKb,SAAS,GAAG;IACvF,OAAO;QAAEiB,IAAI;OAASC;QAAQE,gBAAgBF,OAAOlB,SAAS,KAAKmB;;AACrE;AAEA;;;CAGC,GACD,OAAO,SAASE,sBAAsBC,OAAmC;IACvE,OAAOzB,qBAAqByB,QAAQC,YAAY;AAClD;AAEA;;;;;CAKC,GACD,OAAO,SAASC,wBACdF,OAAmC,EACnCR,IAAmC,EACnCC,QAAqB;IASrB,MAAMC,SAASpB,sBAAsBmB,UAAUD,MAAMO,sBAAsBC;IAC3E,IAAIN,OAAOC,EAAE,KAAK,OAAO,OAAOD;IAChC,MAAM,EAAES,qBAAqB,EAAEC,eAAe,EAAE,GAAGlC,6BAA6B;QAC9E+B,cAAcP,OAAOjB,KAAK;IAC5B;IACA,OAAO;QAAEkB,IAAI;QAAMM,cAAcP,OAAOjB,KAAK;QAAE0B;QAAuBC;IAAgB;AACxF"}
@@ -24,6 +24,15 @@ export declare const NATIVE_CONTACT_LIFECYCLE_STAGES: string[];
24
24
  export declare const NATIVE_CRM_TASK_KINDS: string[];
25
25
  /** Every activity kind, in order. */
26
26
  export declare const NATIVE_CRM_ACTIVITY_KINDS: string[];
27
+ /**
28
+ * The Material Design glyphs the console's activity timeline draws for the
29
+ * channels that are brands (`KIND_ICONS` in `activity-list`). Neither SF
30
+ * Symbols nor Material Icons carry a brand's mark, so the apps draw these
31
+ * paths through their SVG path icon, and the timeline shows the same marks on
32
+ * every platform. Copied rather than imported, because this module stays pure;
33
+ * `native-vocabulary.spec.ts` holds them to `@aglyn/shared-data-mdi`.
34
+ */
35
+ export declare const NATIVE_CRM_ACTIVITY_ICON_PATHS: Record<string, string>;
27
36
  /** The objects custom fields are kept for. */
28
37
  export declare const NATIVE_CRM_FIELD_OBJECTS: string[];
29
38
  /** The report periods, in the console's order. */
@@ -40,6 +40,19 @@ import { DEAL_CURRENCIES } from "./deal-board-model.js";
40
40
  /** Every activity kind, in order. */ export const NATIVE_CRM_ACTIVITY_KINDS = [
41
41
  ...CRM_ACTIVITY_KINDS
42
42
  ];
43
+ /**
44
+ * The Material Design glyphs the console's activity timeline draws for the
45
+ * channels that are brands (`KIND_ICONS` in `activity-list`). Neither SF
46
+ * Symbols nor Material Icons carry a brand's mark, so the apps draw these
47
+ * paths through their SVG path icon, and the timeline shows the same marks on
48
+ * every platform. Copied rather than imported, because this module stays pure;
49
+ * `native-vocabulary.spec.ts` holds them to `@aglyn/shared-data-mdi`.
50
+ */ export const NATIVE_CRM_ACTIVITY_ICON_PATHS = {
51
+ whatsapp: 'M12.04 2C6.58 2 2.13 6.45 2.13 11.91C2.13 13.66 2.59 15.36 3.45 16.86L2.05 22L7.3 20.62C8.75 21.41 10.38 21.83 12.04 21.83C17.5 21.83 21.95 17.38 21.95 11.92C21.95 9.27 20.92 6.78 19.05 4.91C17.18 3.03 14.69 2 12.04 2M12.05 3.67C14.25 3.67 16.31 4.53 17.87 6.09C19.42 7.65 20.28 9.72 20.28 11.92C20.28 16.46 16.58 20.15 12.04 20.15C10.56 20.15 9.11 19.76 7.85 19L7.55 18.83L4.43 19.65L5.26 16.61L5.06 16.29C4.24 15 3.8 13.47 3.8 11.91C3.81 7.37 7.5 3.67 12.05 3.67M8.53 7.33C8.37 7.33 8.1 7.39 7.87 7.64C7.65 7.89 7 8.5 7 9.71C7 10.93 7.89 12.1 8 12.27C8.14 12.44 9.76 14.94 12.25 16C12.84 16.27 13.3 16.42 13.66 16.53C14.25 16.72 14.79 16.69 15.22 16.63C15.7 16.56 16.68 16.03 16.89 15.45C17.1 14.87 17.1 14.38 17.04 14.27C16.97 14.17 16.81 14.11 16.56 14C16.31 13.86 15.09 13.26 14.87 13.18C14.64 13.1 14.5 13.06 14.31 13.3C14.15 13.55 13.67 14.11 13.53 14.27C13.38 14.44 13.24 14.46 13 14.34C12.74 14.21 11.94 13.95 11 13.11C10.26 12.45 9.77 11.64 9.62 11.39C9.5 11.15 9.61 11 9.73 10.89C9.84 10.78 10 10.6 10.1 10.45C10.23 10.31 10.27 10.2 10.35 10.04C10.43 9.87 10.39 9.73 10.33 9.61C10.27 9.5 9.77 8.26 9.56 7.77C9.36 7.29 9.16 7.35 9 7.34C8.86 7.34 8.7 7.33 8.53 7.33Z',
52
+ messenger: 'M12,2C6.36,2 2,6.13 2,11.7C2,14.61 3.19,17.14 5.14,18.87C5.3,19 5.4,19.22 5.41,19.44L5.46,21.22C5.5,21.79 6.07,22.16 6.59,21.93L8.57,21.06C8.74,21 8.93,20.97 9.1,21C10,21.27 11,21.4 12,21.4C17.64,21.4 22,17.27 22,11.7C22,6.13 17.64,2 12,2M18,9.46L15.07,14.13C14.6,14.86 13.6,15.05 12.9,14.5L10.56,12.77C10.35,12.61 10.05,12.61 9.84,12.77L6.68,15.17C6.26,15.5 5.71,15 6,14.54L8.93,9.87C9.4,9.14 10.4,8.95 11.1,9.47L13.44,11.23C13.66,11.39 13.95,11.39 14.16,11.23L17.32,8.83C17.74,8.5 18.29,9 18,9.46Z',
53
+ instagram: 'M7.8,2H16.2C19.4,2 22,4.6 22,7.8V16.2A5.8,5.8 0 0,1 16.2,22H7.8C4.6,22 2,19.4 2,16.2V7.8A5.8,5.8 0 0,1 7.8,2M7.6,4A3.6,3.6 0 0,0 4,7.6V16.4C4,18.39 5.61,20 7.6,20H16.4A3.6,3.6 0 0,0 20,16.4V7.6C20,5.61 18.39,4 16.4,4H7.6M17.25,5.5A1.25,1.25 0 0,1 18.5,6.75A1.25,1.25 0 0,1 17.25,8A1.25,1.25 0 0,1 16,6.75A1.25,1.25 0 0,1 17.25,5.5M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z',
54
+ linkedin: 'M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z'
55
+ };
43
56
  /** The objects custom fields are kept for. */ export const NATIVE_CRM_FIELD_OBJECTS = [
44
57
  ...CRM_FIELD_OBJECTS
45
58
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/native-vocabulary.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 * THE CRM'S VOCABULARIES, FLAT, FOR THE NATIVE APPS (AGL-3669).\n *\n * The console reads these off `as const` tuples and typed definitions,\n * shapes the native contracts generator does not translate. Each value here\n * is the same data re-read as plain arrays and maps; nothing in the web\n * apps imports this file, so it adds nothing to their bundles.\n */\n\nimport {\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_KINDS,\n CRM_FIELD_OBJECTS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_STATUSES,\n CRM_PICKLIST_DEFINITIONS,\n CRM_TASK_KINDS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CONTACT_FIELD_DEFAULT_CURRENCY,\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n currencyFractionDigits,\n} from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport { CRM_REPORT_PERIODS } from './crm-reports'\nimport { DEAL_CURRENCIES } from './deal-board-model'\n\n/** Every lead status, in the console's order. */\nexport const NATIVE_CRM_LEAD_STATUSES: string[] = [...CRM_LEAD_STATUSES]\n\n/** The statuses a lead still needs working in. */\nexport const NATIVE_CRM_LEAD_OPEN_STATUSES: string[] = [...CRM_LEAD_OPEN_STATUSES]\n\n/** Every contact lifecycle stage, in order. */\nexport const NATIVE_CONTACT_LIFECYCLE_STAGES: string[] = [...CONTACT_LIFECYCLE_STAGES]\n\n/** Every task kind, in order. */\nexport const NATIVE_CRM_TASK_KINDS: string[] = [...CRM_TASK_KINDS]\n\n/** Every activity kind, in order. */\nexport const NATIVE_CRM_ACTIVITY_KINDS: string[] = [...CRM_ACTIVITY_KINDS]\n\n/** The objects custom fields are kept for. */\nexport const NATIVE_CRM_FIELD_OBJECTS: string[] = [...CRM_FIELD_OBJECTS]\n\n/** The report periods, in the console's order. */\nexport const NATIVE_CRM_REPORT_PERIODS: string[] = [...CRM_REPORT_PERIODS]\n\n/** The directions an activity of each kind can take. */\nexport const NATIVE_CRM_ACTIVITY_DIRECTIONS: Record<string, string[]> = Object.fromEntries(\n Object.entries(CRM_ACTIVITY_DIRECTIONS).map(([kind, directions]) => [kind, [...(directions ?? [])]]),\n)\n\n/** One picklist as the native pickers start it: its id, label, object and standard labels. */\nexport interface NativeCrmPicklist {\n id: string\n label: string\n object: string\n standardLabels: string[]\n}\n\n/** Every picklist the CRM keeps, with the labels a workspace starts with. */\nexport const NATIVE_CRM_PICKLISTS: NativeCrmPicklist[] = CRM_PICKLIST_DEFINITIONS.map((definition) => ({\n id: definition.id,\n label: definition.label,\n object: definition.object,\n standardLabels: definition.standardValues.map((value) => value.label),\n}))\n\n/** Every custom field type, in the order the console's field drawer lists them (AGL-3971). */\nexport const NATIVE_CRM_FIELD_TYPES: string[] = [...CONTACT_FIELD_TYPES]\n\n/** How each custom field type reads, the console's own words. */\nexport const NATIVE_CRM_FIELD_TYPE_LABELS: Record<string, string> = { ...CONTACT_FIELD_TYPE_LABELS }\n\n/** The currencies a currency field's picker offers: the deal amount picker's list, lowercase ISO 4217. */\nexport const NATIVE_CRM_FIELD_CURRENCIES: string[] = [...DEAL_CURRENCIES]\n\n/** The currency a currency field is in when it names none. */\nexport const NATIVE_CRM_FIELD_DEFAULT_CURRENCY: string = CONTACT_FIELD_DEFAULT_CURRENCY\n\n/**\n * How many decimals each currency's minor unit has where it is not two, as\n * the console's `Intl` reads them (`currencyFractionDigits`): JPY, KRW and\n * VND 0; BHD, KWD and OMR 3. A code not listed has two, as the console's\n * fallback does.\n */\nexport const NATIVE_CURRENCY_FRACTION_DIGITS: Record<string, number> = Object.fromEntries(\n Intl.supportedValuesOf('currency')\n .map((code) => [code.toLowerCase(), currencyFractionDigits(code)] as const)\n .filter(([, digits]) => digits !== 2),\n)\n"],"names":["CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_KINDS","CRM_FIELD_OBJECTS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_STATUSES","CRM_PICKLIST_DEFINITIONS","CRM_TASK_KINDS","CONTACT_FIELD_DEFAULT_CURRENCY","CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","currencyFractionDigits","CRM_REPORT_PERIODS","DEAL_CURRENCIES","NATIVE_CRM_LEAD_STATUSES","NATIVE_CRM_LEAD_OPEN_STATUSES","NATIVE_CONTACT_LIFECYCLE_STAGES","NATIVE_CRM_TASK_KINDS","NATIVE_CRM_ACTIVITY_KINDS","NATIVE_CRM_FIELD_OBJECTS","NATIVE_CRM_REPORT_PERIODS","NATIVE_CRM_ACTIVITY_DIRECTIONS","Object","fromEntries","entries","map","kind","directions","NATIVE_CRM_PICKLISTS","definition","id","label","object","standardLabels","standardValues","value","NATIVE_CRM_FIELD_TYPES","NATIVE_CRM_FIELD_TYPE_LABELS","NATIVE_CRM_FIELD_CURRENCIES","NATIVE_CRM_FIELD_DEFAULT_CURRENCY","NATIVE_CURRENCY_FRACTION_DIGITS","Intl","supportedValuesOf","code","toLowerCase","filter","digits"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,iBAAiB,EACjBC,sBAAsB,EACtBC,iBAAiB,EACjBC,wBAAwB,EACxBC,cAAc,QACT,6BAA4B;AACnC,SACEC,8BAA8B,EAC9BC,yBAAyB,EACzBC,mBAAmB,EACnBC,sBAAsB,QACjB,+CAA8C;AACrD,SAASC,kBAAkB,QAAQ,mBAAe;AAClD,SAASC,eAAe,QAAQ,wBAAoB;AAEpD,+CAA+C,GAC/C,OAAO,MAAMC,2BAAqC;OAAIT;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMU,gCAA0C;OAAIX;CAAuB,CAAA;AAElF,6CAA6C,GAC7C,OAAO,MAAMY,kCAA4C;OAAIhB;CAAyB,CAAA;AAEtF,+BAA+B,GAC/B,OAAO,MAAMiB,wBAAkC;OAAIV;CAAe,CAAA;AAElE,mCAAmC,GACnC,OAAO,MAAMW,4BAAsC;OAAIhB;CAAmB,CAAA;AAE1E,4CAA4C,GAC5C,OAAO,MAAMiB,2BAAqC;OAAIhB;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMiB,4BAAsC;OAAIR;CAAmB,CAAA;AAE1E,sDAAsD,GACtD,OAAO,MAAMS,iCAA2DC,OAAOC,WAAW,CACxFD,OAAOE,OAAO,CAACvB,yBAAyBwB,GAAG,CAAC,CAAC,CAACC,MAAMC,WAAW,GAAK;QAACD;QAAM;eAAKC,qBAAAA,aAAc,EAAE;SAAE;KAAC,GACpG;AAUD,2EAA2E,GAC3E,OAAO,MAAMC,uBAA4CtB,yBAAyBmB,GAAG,CAAC,CAACI,aAAgB,CAAA;QACrGC,IAAID,WAAWC,EAAE;QACjBC,OAAOF,WAAWE,KAAK;QACvBC,QAAQH,WAAWG,MAAM;QACzBC,gBAAgBJ,WAAWK,cAAc,CAACT,GAAG,CAAC,CAACU,QAAUA,MAAMJ,KAAK;IACtE,CAAA,GAAG;AAEH,4FAA4F,GAC5F,OAAO,MAAMK,yBAAmC;OAAI1B;CAAoB,CAAA;AAExE,+DAA+D,GAC/D,OAAO,MAAM2B,+BAAuD,aAAK5B,2BAA2B;AAEpG,wGAAwG,GACxG,OAAO,MAAM6B,8BAAwC;OAAIzB;CAAgB,CAAA;AAEzE,4DAA4D,GAC5D,OAAO,MAAM0B,oCAA4C/B,+BAA8B;AAEvF;;;;;CAKC,GACD,OAAO,MAAMgC,kCAA0DlB,OAAOC,WAAW,CACvFkB,KAAKC,iBAAiB,CAAC,YACpBjB,GAAG,CAAC,CAACkB,OAAS;QAACA,KAAKC,WAAW;QAAIjC,uBAAuBgC;KAAM,EAChEE,MAAM,CAAC,CAAC,GAAGC,OAAO,GAAKA,WAAW,IACtC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/native-vocabulary.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 * THE CRM'S VOCABULARIES, FLAT, FOR THE NATIVE APPS (AGL-3669).\n *\n * The console reads these off `as const` tuples and typed definitions,\n * shapes the native contracts generator does not translate. Each value here\n * is the same data re-read as plain arrays and maps; nothing in the web\n * apps imports this file, so it adds nothing to their bundles.\n */\n\nimport {\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_KINDS,\n CRM_FIELD_OBJECTS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_STATUSES,\n CRM_PICKLIST_DEFINITIONS,\n CRM_TASK_KINDS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CONTACT_FIELD_DEFAULT_CURRENCY,\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n currencyFractionDigits,\n} from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport { CRM_REPORT_PERIODS } from './crm-reports'\nimport { DEAL_CURRENCIES } from './deal-board-model'\n\n/** Every lead status, in the console's order. */\nexport const NATIVE_CRM_LEAD_STATUSES: string[] = [...CRM_LEAD_STATUSES]\n\n/** The statuses a lead still needs working in. */\nexport const NATIVE_CRM_LEAD_OPEN_STATUSES: string[] = [...CRM_LEAD_OPEN_STATUSES]\n\n/** Every contact lifecycle stage, in order. */\nexport const NATIVE_CONTACT_LIFECYCLE_STAGES: string[] = [...CONTACT_LIFECYCLE_STAGES]\n\n/** Every task kind, in order. */\nexport const NATIVE_CRM_TASK_KINDS: string[] = [...CRM_TASK_KINDS]\n\n/** Every activity kind, in order. */\nexport const NATIVE_CRM_ACTIVITY_KINDS: string[] = [...CRM_ACTIVITY_KINDS]\n\n/**\n * The Material Design glyphs the console's activity timeline draws for the\n * channels that are brands (`KIND_ICONS` in `activity-list`). Neither SF\n * Symbols nor Material Icons carry a brand's mark, so the apps draw these\n * paths through their SVG path icon, and the timeline shows the same marks on\n * every platform. Copied rather than imported, because this module stays pure;\n * `native-vocabulary.spec.ts` holds them to `@aglyn/shared-data-mdi`.\n */\nexport const NATIVE_CRM_ACTIVITY_ICON_PATHS: Record<string, string> = {\n whatsapp:\n 'M12.04 2C6.58 2 2.13 6.45 2.13 11.91C2.13 13.66 2.59 15.36 3.45 16.86L2.05 22L7.3 20.62C8.75 21.41 10.38 21.83 12.04 21.83C17.5 21.83 21.95 17.38 21.95 11.92C21.95 9.27 20.92 6.78 19.05 4.91C17.18 3.03 14.69 2 12.04 2M12.05 3.67C14.25 3.67 16.31 4.53 17.87 6.09C19.42 7.65 20.28 9.72 20.28 11.92C20.28 16.46 16.58 20.15 12.04 20.15C10.56 20.15 9.11 19.76 7.85 19L7.55 18.83L4.43 19.65L5.26 16.61L5.06 16.29C4.24 15 3.8 13.47 3.8 11.91C3.81 7.37 7.5 3.67 12.05 3.67M8.53 7.33C8.37 7.33 8.1 7.39 7.87 7.64C7.65 7.89 7 8.5 7 9.71C7 10.93 7.89 12.1 8 12.27C8.14 12.44 9.76 14.94 12.25 16C12.84 16.27 13.3 16.42 13.66 16.53C14.25 16.72 14.79 16.69 15.22 16.63C15.7 16.56 16.68 16.03 16.89 15.45C17.1 14.87 17.1 14.38 17.04 14.27C16.97 14.17 16.81 14.11 16.56 14C16.31 13.86 15.09 13.26 14.87 13.18C14.64 13.1 14.5 13.06 14.31 13.3C14.15 13.55 13.67 14.11 13.53 14.27C13.38 14.44 13.24 14.46 13 14.34C12.74 14.21 11.94 13.95 11 13.11C10.26 12.45 9.77 11.64 9.62 11.39C9.5 11.15 9.61 11 9.73 10.89C9.84 10.78 10 10.6 10.1 10.45C10.23 10.31 10.27 10.2 10.35 10.04C10.43 9.87 10.39 9.73 10.33 9.61C10.27 9.5 9.77 8.26 9.56 7.77C9.36 7.29 9.16 7.35 9 7.34C8.86 7.34 8.7 7.33 8.53 7.33Z',\n messenger:\n 'M12,2C6.36,2 2,6.13 2,11.7C2,14.61 3.19,17.14 5.14,18.87C5.3,19 5.4,19.22 5.41,19.44L5.46,21.22C5.5,21.79 6.07,22.16 6.59,21.93L8.57,21.06C8.74,21 8.93,20.97 9.1,21C10,21.27 11,21.4 12,21.4C17.64,21.4 22,17.27 22,11.7C22,6.13 17.64,2 12,2M18,9.46L15.07,14.13C14.6,14.86 13.6,15.05 12.9,14.5L10.56,12.77C10.35,12.61 10.05,12.61 9.84,12.77L6.68,15.17C6.26,15.5 5.71,15 6,14.54L8.93,9.87C9.4,9.14 10.4,8.95 11.1,9.47L13.44,11.23C13.66,11.39 13.95,11.39 14.16,11.23L17.32,8.83C17.74,8.5 18.29,9 18,9.46Z',\n instagram:\n 'M7.8,2H16.2C19.4,2 22,4.6 22,7.8V16.2A5.8,5.8 0 0,1 16.2,22H7.8C4.6,22 2,19.4 2,16.2V7.8A5.8,5.8 0 0,1 7.8,2M7.6,4A3.6,3.6 0 0,0 4,7.6V16.4C4,18.39 5.61,20 7.6,20H16.4A3.6,3.6 0 0,0 20,16.4V7.6C20,5.61 18.39,4 16.4,4H7.6M17.25,5.5A1.25,1.25 0 0,1 18.5,6.75A1.25,1.25 0 0,1 17.25,8A1.25,1.25 0 0,1 16,6.75A1.25,1.25 0 0,1 17.25,5.5M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z',\n linkedin:\n 'M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z',\n}\n\n/** The objects custom fields are kept for. */\nexport const NATIVE_CRM_FIELD_OBJECTS: string[] = [...CRM_FIELD_OBJECTS]\n\n/** The report periods, in the console's order. */\nexport const NATIVE_CRM_REPORT_PERIODS: string[] = [...CRM_REPORT_PERIODS]\n\n/** The directions an activity of each kind can take. */\nexport const NATIVE_CRM_ACTIVITY_DIRECTIONS: Record<string, string[]> = Object.fromEntries(\n Object.entries(CRM_ACTIVITY_DIRECTIONS).map(([kind, directions]) => [kind, [...(directions ?? [])]]),\n)\n\n/** One picklist as the native pickers start it: its id, label, object and standard labels. */\nexport interface NativeCrmPicklist {\n id: string\n label: string\n object: string\n standardLabels: string[]\n}\n\n/** Every picklist the CRM keeps, with the labels a workspace starts with. */\nexport const NATIVE_CRM_PICKLISTS: NativeCrmPicklist[] = CRM_PICKLIST_DEFINITIONS.map((definition) => ({\n id: definition.id,\n label: definition.label,\n object: definition.object,\n standardLabels: definition.standardValues.map((value) => value.label),\n}))\n\n/** Every custom field type, in the order the console's field drawer lists them (AGL-3971). */\nexport const NATIVE_CRM_FIELD_TYPES: string[] = [...CONTACT_FIELD_TYPES]\n\n/** How each custom field type reads, the console's own words. */\nexport const NATIVE_CRM_FIELD_TYPE_LABELS: Record<string, string> = { ...CONTACT_FIELD_TYPE_LABELS }\n\n/** The currencies a currency field's picker offers: the deal amount picker's list, lowercase ISO 4217. */\nexport const NATIVE_CRM_FIELD_CURRENCIES: string[] = [...DEAL_CURRENCIES]\n\n/** The currency a currency field is in when it names none. */\nexport const NATIVE_CRM_FIELD_DEFAULT_CURRENCY: string = CONTACT_FIELD_DEFAULT_CURRENCY\n\n/**\n * How many decimals each currency's minor unit has where it is not two, as\n * the console's `Intl` reads them (`currencyFractionDigits`): JPY, KRW and\n * VND 0; BHD, KWD and OMR 3. A code not listed has two, as the console's\n * fallback does.\n */\nexport const NATIVE_CURRENCY_FRACTION_DIGITS: Record<string, number> = Object.fromEntries(\n Intl.supportedValuesOf('currency')\n .map((code) => [code.toLowerCase(), currencyFractionDigits(code)] as const)\n .filter(([, digits]) => digits !== 2),\n)\n"],"names":["CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_KINDS","CRM_FIELD_OBJECTS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_STATUSES","CRM_PICKLIST_DEFINITIONS","CRM_TASK_KINDS","CONTACT_FIELD_DEFAULT_CURRENCY","CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","currencyFractionDigits","CRM_REPORT_PERIODS","DEAL_CURRENCIES","NATIVE_CRM_LEAD_STATUSES","NATIVE_CRM_LEAD_OPEN_STATUSES","NATIVE_CONTACT_LIFECYCLE_STAGES","NATIVE_CRM_TASK_KINDS","NATIVE_CRM_ACTIVITY_KINDS","NATIVE_CRM_ACTIVITY_ICON_PATHS","whatsapp","messenger","instagram","linkedin","NATIVE_CRM_FIELD_OBJECTS","NATIVE_CRM_REPORT_PERIODS","NATIVE_CRM_ACTIVITY_DIRECTIONS","Object","fromEntries","entries","map","kind","directions","NATIVE_CRM_PICKLISTS","definition","id","label","object","standardLabels","standardValues","value","NATIVE_CRM_FIELD_TYPES","NATIVE_CRM_FIELD_TYPE_LABELS","NATIVE_CRM_FIELD_CURRENCIES","NATIVE_CRM_FIELD_DEFAULT_CURRENCY","NATIVE_CURRENCY_FRACTION_DIGITS","Intl","supportedValuesOf","code","toLowerCase","filter","digits"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,iBAAiB,EACjBC,sBAAsB,EACtBC,iBAAiB,EACjBC,wBAAwB,EACxBC,cAAc,QACT,6BAA4B;AACnC,SACEC,8BAA8B,EAC9BC,yBAAyB,EACzBC,mBAAmB,EACnBC,sBAAsB,QACjB,+CAA8C;AACrD,SAASC,kBAAkB,QAAQ,mBAAe;AAClD,SAASC,eAAe,QAAQ,wBAAoB;AAEpD,+CAA+C,GAC/C,OAAO,MAAMC,2BAAqC;OAAIT;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMU,gCAA0C;OAAIX;CAAuB,CAAA;AAElF,6CAA6C,GAC7C,OAAO,MAAMY,kCAA4C;OAAIhB;CAAyB,CAAA;AAEtF,+BAA+B,GAC/B,OAAO,MAAMiB,wBAAkC;OAAIV;CAAe,CAAA;AAElE,mCAAmC,GACnC,OAAO,MAAMW,4BAAsC;OAAIhB;CAAmB,CAAA;AAE1E;;;;;;;CAOC,GACD,OAAO,MAAMiB,iCAAyD;IACpEC,UACE;IACFC,WACE;IACFC,WACE;IACFC,UACE;AACJ,EAAC;AAED,4CAA4C,GAC5C,OAAO,MAAMC,2BAAqC;OAAIrB;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMsB,4BAAsC;OAAIb;CAAmB,CAAA;AAE1E,sDAAsD,GACtD,OAAO,MAAMc,iCAA2DC,OAAOC,WAAW,CACxFD,OAAOE,OAAO,CAAC5B,yBAAyB6B,GAAG,CAAC,CAAC,CAACC,MAAMC,WAAW,GAAK;QAACD;QAAM;eAAKC,qBAAAA,aAAc,EAAE;SAAE;KAAC,GACpG;AAUD,2EAA2E,GAC3E,OAAO,MAAMC,uBAA4C3B,yBAAyBwB,GAAG,CAAC,CAACI,aAAgB,CAAA;QACrGC,IAAID,WAAWC,EAAE;QACjBC,OAAOF,WAAWE,KAAK;QACvBC,QAAQH,WAAWG,MAAM;QACzBC,gBAAgBJ,WAAWK,cAAc,CAACT,GAAG,CAAC,CAACU,QAAUA,MAAMJ,KAAK;IACtE,CAAA,GAAG;AAEH,4FAA4F,GAC5F,OAAO,MAAMK,yBAAmC;OAAI/B;CAAoB,CAAA;AAExE,+DAA+D,GAC/D,OAAO,MAAMgC,+BAAuD,aAAKjC,2BAA2B;AAEpG,wGAAwG,GACxG,OAAO,MAAMkC,8BAAwC;OAAI9B;CAAgB,CAAA;AAEzE,4DAA4D,GAC5D,OAAO,MAAM+B,oCAA4CpC,+BAA8B;AAEvF;;;;;CAKC,GACD,OAAO,MAAMqC,kCAA0DlB,OAAOC,WAAW,CACvFkB,KAAKC,iBAAiB,CAAC,YACpBjB,GAAG,CAAC,CAACkB,OAAS;QAACA,KAAKC,WAAW;QAAItC,uBAAuBqC;KAAM,EAChEE,MAAM,CAAC,CAAC,GAAGC,OAAO,GAAKA,WAAW,IACtC"}
@@ -0,0 +1,25 @@
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 { CrmDealStage } from '@aglyn/aglyn';
18
+ /**
19
+ * The stages as they are stored: every name trimmed, as a save writes them.
20
+ *
21
+ * Its own module because only the stage editor needs it, and everything in
22
+ * `deal-board-model` that a lazy surface reaches for is paid by every console
23
+ * page that loads the plugin.
24
+ */
25
+ export declare function settleStageNames(stages: readonly CrmDealStage[]): CrmDealStage[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /**
18
+ * The stages as they are stored: every name trimmed, as a save writes them.
19
+ *
20
+ * Its own module because only the stage editor needs it, and everything in
21
+ * `deal-board-model` that a lazy surface reaches for is paid by every console
22
+ * page that loads the plugin.
23
+ */ export function settleStageNames(stages) {
24
+ return stages.map((stage)=>_extends({}, stage, {
25
+ name: stage.name.trim()
26
+ }));
27
+ }
28
+
29
+ //# sourceMappingURL=settle-stage-names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/settle-stage-names.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmDealStage } from '@aglyn/aglyn'\n\n/**\n * The stages as they are stored: every name trimmed, as a save writes them.\n *\n * Its own module because only the stage editor needs it, and everything in\n * `deal-board-model` that a lazy surface reaches for is paid by every console\n * page that loads the plugin.\n */\nexport function settleStageNames(stages: readonly CrmDealStage[]): CrmDealStage[] {\n return stages.map((stage) => ({ ...stage, name: stage.name.trim() }))\n}\n"],"names":["settleStageNames","stages","map","stage","name","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID;;;;;;CAMC,GACD,OAAO,SAASA,iBAAiBC,MAA+B;IAC9D,OAAOA,OAAOC,GAAG,CAAC,CAACC,QAAW,aAAKA;YAAOC,MAAMD,MAAMC,IAAI,CAACC,IAAI;;AACjE"}
@@ -43,7 +43,7 @@
43
43
  nativeNavHubSection(scope, '/crm', 'contacts', 'Contacts', 'crm.contacts'),
44
44
  nativeNavHubSection(scope, '/crm', 'leads', 'Leads', 'crm.leads'),
45
45
  nativeNavHubSection(scope, '/crm', 'companies', 'Companies', 'crm.companies'),
46
- nativeNavHubSection(scope, '/crm', 'deals', 'Deals', 'crm.deals'),
46
+ nativeNavHubSection(scope, '/crm', 'deals', 'Deals / Opportunities', 'crm.deals'),
47
47
  nativeNavHubSection(scope, '/crm', 'tasks', 'Tasks', 'crm.tasks'),
48
48
  nativeNavHubSection(scope, '/crm', 'reports', 'Reports', 'crm.reports'),
49
49
  nativeNavHubSection(scope, '/crm', 'fields', 'Fields', 'crm.fields'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/native-nav.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type NativeNavContribution,\n type NativeNavCreateContribution,\n type NativeNavRow,\n nativeNavHubSection,\n nativeNavIcon,\n nativeNavRef,\n} from '@aglyn/aglyn/app-utils/native-nav'\n\n/**\n * The CRM in the native apps' navigation (AGL-3943): the rows this plugin\n * adds to the manifest the shells draw, placed into the core's lists by\n * `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the\n * console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.\n *\n * Imports only the core's plain nav module, so the native contracts read it.\n */\n\n/**\n * The row at either scope. The workspace's opens the console's org page\n * (`orgNavItems`, `/[orgSlug]/crm`), the site's the site's hub; both list\n * the same sections under the same ids.\n */\nfunction row(scope: 'org' | 'host', order: number): NativeNavRow {\n return {\n id: 'crm',\n label: 'CRM',\n icon: nativeNavIcon('person.text.rectangle', 'contact_mail'),\n screen: 'crm.contacts',\n pluginId: 'crm',\n order,\n keywords: ['contacts', 'people', 'deals', 'tasks'],\n sections: [\n nativeNavHubSection(scope, '/crm', 'contacts', 'Contacts', 'crm.contacts'),\n nativeNavHubSection(scope, '/crm', 'leads', 'Leads', 'crm.leads'),\n nativeNavHubSection(scope, '/crm', 'companies', 'Companies', 'crm.companies'),\n nativeNavHubSection(scope, '/crm', 'deals', 'Deals', 'crm.deals'),\n nativeNavHubSection(scope, '/crm', 'tasks', 'Tasks', 'crm.tasks'),\n nativeNavHubSection(scope, '/crm', 'reports', 'Reports', 'crm.reports'),\n nativeNavHubSection(scope, '/crm', 'fields', 'Fields', 'crm.fields'),\n nativeNavHubSection(scope, '/crm', 'settings', 'Settings', 'crm.settings'),\n ],\n docs: scope === 'org' ? '/content-and-data/crm/overview#at-the-organization-level' : '/content-and-data/crm/overview',\n console: nativeNavRef(scope === 'org' ? 'orgPluginTab' : 'hostPluginTab', '/crm'),\n }\n}\n\nexport const NATIVE_NAV_ROWS: readonly NativeNavContribution[] = [\n { list: 'workspace', group: 'workspace', row: row('org', 20) },\n { list: 'site', group: 'customers', row: row('host', 20) },\n]\n\n/** A contact, at either scope: the console's \"New contact\" on the CRM's Contacts. */\nfunction contact(scope: 'org' | 'host', order: number): NativeNavCreateContribution['item'] {\n return {\n id: 'contact',\n label: 'Contact',\n icon: nativeNavIcon('person.badge.plus', 'person_add'),\n screen: 'crm.contacts',\n params: { new: '1' },\n pluginId: 'crm',\n permission: 'data.manage',\n planFeature: 'crm',\n order,\n console: nativeNavRef(scope === 'org' ? 'orgPluginSection' : 'hostPluginSection', '/crm/contacts'),\n }\n}\n\nexport const NATIVE_NAV_CREATE: readonly NativeNavCreateContribution[] = [\n { scope: 'workspace', item: contact('org', 20) },\n { scope: 'site', item: contact('host', 70) },\n]\n"],"names":["nativeNavHubSection","nativeNavIcon","nativeNavRef","row","scope","order","id","label","icon","screen","pluginId","keywords","sections","docs","console","NATIVE_NAV_ROWS","list","group","contact","params","new","permission","planFeature","NATIVE_NAV_CREATE","item"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAIEA,mBAAmB,EACnBC,aAAa,EACbC,YAAY,QACP,oCAAmC;AAE1C;;;;;;;CAOC,GAED;;;;CAIC,GACD,SAASC,IAAIC,KAAqB,EAAEC,KAAa;IAC/C,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,yBAAyB;QAC7CQ,QAAQ;QACRC,UAAU;QACVL;QACAM,UAAU;YAAC;YAAY;YAAU;YAAS;SAAQ;QAClDC,UAAU;YACRZ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;YAC3DJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,aAAa,aAAa;YAC7DJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,WAAW,WAAW;YACzDJ,oBAAoBI,OAAO,QAAQ,UAAU,UAAU;YACvDJ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;SAC5D;QACDS,MAAMT,UAAU,QAAQ,6DAA6D;QACrFU,SAASZ,aAAaE,UAAU,QAAQ,iBAAiB,iBAAiB;IAC5E;AACF;AAEA,OAAO,MAAMW,kBAAoD;IAC/D;QAAEC,MAAM;QAAaC,OAAO;QAAad,KAAKA,IAAI,OAAO;IAAI;IAC7D;QAAEa,MAAM;QAAQC,OAAO;QAAad,KAAKA,IAAI,QAAQ;IAAI;CAC1D,CAAA;AAED,mFAAmF,GACnF,SAASe,QAAQd,KAAqB,EAAEC,KAAa;IACnD,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,qBAAqB;QACzCQ,QAAQ;QACRU,QAAQ;YAAEC,KAAK;QAAI;QACnBV,UAAU;QACVW,YAAY;QACZC,aAAa;QACbjB;QACAS,SAASZ,aAAaE,UAAU,QAAQ,qBAAqB,qBAAqB;IACpF;AACF;AAEA,OAAO,MAAMmB,oBAA4D;IACvE;QAAEnB,OAAO;QAAaoB,MAAMN,QAAQ,OAAO;IAAI;IAC/C;QAAEd,OAAO;QAAQoB,MAAMN,QAAQ,QAAQ;IAAI;CAC5C,CAAA"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/native-nav.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type NativeNavContribution,\n type NativeNavCreateContribution,\n type NativeNavRow,\n nativeNavHubSection,\n nativeNavIcon,\n nativeNavRef,\n} from '@aglyn/aglyn/app-utils/native-nav'\n\n/**\n * The CRM in the native apps' navigation (AGL-3943): the rows this plugin\n * adds to the manifest the shells draw, placed into the core's lists by\n * `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the\n * console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.\n *\n * Imports only the core's plain nav module, so the native contracts read it.\n */\n\n/**\n * The row at either scope. The workspace's opens the console's org page\n * (`orgNavItems`, `/[orgSlug]/crm`), the site's the site's hub; both list\n * the same sections under the same ids.\n */\nfunction row(scope: 'org' | 'host', order: number): NativeNavRow {\n return {\n id: 'crm',\n label: 'CRM',\n icon: nativeNavIcon('person.text.rectangle', 'contact_mail'),\n screen: 'crm.contacts',\n pluginId: 'crm',\n order,\n keywords: ['contacts', 'people', 'deals', 'tasks'],\n sections: [\n nativeNavHubSection(scope, '/crm', 'contacts', 'Contacts', 'crm.contacts'),\n nativeNavHubSection(scope, '/crm', 'leads', 'Leads', 'crm.leads'),\n nativeNavHubSection(scope, '/crm', 'companies', 'Companies', 'crm.companies'),\n nativeNavHubSection(scope, '/crm', 'deals', 'Deals / Opportunities', 'crm.deals'),\n nativeNavHubSection(scope, '/crm', 'tasks', 'Tasks', 'crm.tasks'),\n nativeNavHubSection(scope, '/crm', 'reports', 'Reports', 'crm.reports'),\n nativeNavHubSection(scope, '/crm', 'fields', 'Fields', 'crm.fields'),\n nativeNavHubSection(scope, '/crm', 'settings', 'Settings', 'crm.settings'),\n ],\n docs: scope === 'org' ? '/content-and-data/crm/overview#at-the-organization-level' : '/content-and-data/crm/overview',\n console: nativeNavRef(scope === 'org' ? 'orgPluginTab' : 'hostPluginTab', '/crm'),\n }\n}\n\nexport const NATIVE_NAV_ROWS: readonly NativeNavContribution[] = [\n { list: 'workspace', group: 'workspace', row: row('org', 20) },\n { list: 'site', group: 'customers', row: row('host', 20) },\n]\n\n/** A contact, at either scope: the console's \"New contact\" on the CRM's Contacts. */\nfunction contact(scope: 'org' | 'host', order: number): NativeNavCreateContribution['item'] {\n return {\n id: 'contact',\n label: 'Contact',\n icon: nativeNavIcon('person.badge.plus', 'person_add'),\n screen: 'crm.contacts',\n params: { new: '1' },\n pluginId: 'crm',\n permission: 'data.manage',\n planFeature: 'crm',\n order,\n console: nativeNavRef(scope === 'org' ? 'orgPluginSection' : 'hostPluginSection', '/crm/contacts'),\n }\n}\n\nexport const NATIVE_NAV_CREATE: readonly NativeNavCreateContribution[] = [\n { scope: 'workspace', item: contact('org', 20) },\n { scope: 'site', item: contact('host', 70) },\n]\n"],"names":["nativeNavHubSection","nativeNavIcon","nativeNavRef","row","scope","order","id","label","icon","screen","pluginId","keywords","sections","docs","console","NATIVE_NAV_ROWS","list","group","contact","params","new","permission","planFeature","NATIVE_NAV_CREATE","item"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAIEA,mBAAmB,EACnBC,aAAa,EACbC,YAAY,QACP,oCAAmC;AAE1C;;;;;;;CAOC,GAED;;;;CAIC,GACD,SAASC,IAAIC,KAAqB,EAAEC,KAAa;IAC/C,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,yBAAyB;QAC7CQ,QAAQ;QACRC,UAAU;QACVL;QACAM,UAAU;YAAC;YAAY;YAAU;YAAS;SAAQ;QAClDC,UAAU;YACRZ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;YAC3DJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,aAAa,aAAa;YAC7DJ,oBAAoBI,OAAO,QAAQ,SAAS,yBAAyB;YACrEJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,WAAW,WAAW;YACzDJ,oBAAoBI,OAAO,QAAQ,UAAU,UAAU;YACvDJ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;SAC5D;QACDS,MAAMT,UAAU,QAAQ,6DAA6D;QACrFU,SAASZ,aAAaE,UAAU,QAAQ,iBAAiB,iBAAiB;IAC5E;AACF;AAEA,OAAO,MAAMW,kBAAoD;IAC/D;QAAEC,MAAM;QAAaC,OAAO;QAAad,KAAKA,IAAI,OAAO;IAAI;IAC7D;QAAEa,MAAM;QAAQC,OAAO;QAAad,KAAKA,IAAI,QAAQ;IAAI;CAC1D,CAAA;AAED,mFAAmF,GACnF,SAASe,QAAQd,KAAqB,EAAEC,KAAa;IACnD,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,qBAAqB;QACzCQ,QAAQ;QACRU,QAAQ;YAAEC,KAAK;QAAI;QACnBV,UAAU;QACVW,YAAY;QACZC,aAAa;QACbjB;QACAS,SAASZ,aAAaE,UAAU,QAAQ,qBAAqB,qBAAqB;IACpF;AACF;AAEA,OAAO,MAAMmB,oBAA4D;IACvE;QAAEnB,OAAO;QAAaoB,MAAMN,QAAQ,OAAO;IAAI;IAC/C;QAAEd,OAAO;QAAQoB,MAAMN,QAAQ,QAAQ;IAAI;CAC5C,CAAA"}
@@ -0,0 +1,84 @@
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 ContactProfileInput } from '@aglyn/tenant-data-admin/server/upsert-contact';
18
+ /**
19
+ * A PERSON THE TEAM ADDS WHO DID NOT COME THROUGH A SITE'S CAPTURE (AGL-4122).
20
+ *
21
+ * `upsertHostContact` is the door for somebody a site met, and it needs what a
22
+ * site's capture always has: a site and an address. A person typed in by the
23
+ * team may have neither. They may be a name and a phone number, added at the
24
+ * organization level, because the team is working a deal with someone whose
25
+ * email comes later. That is this path, and only that: a site AND an address
26
+ * still go through the capture door, which is why the route asks.
27
+ *
28
+ * ## Who the person already is
29
+ *
30
+ * The address when there is one, through the same index the capture reads. With
31
+ * none, the phone number. With neither, nobody: two blank emails are not the
32
+ * same person, and a name is not an identity.
33
+ *
34
+ * The phone lookup is the one narrowing the capture's address lookup refuses,
35
+ * deliberately. An address names one human, so the capture recognizes them
36
+ * across every site. A phone number can be a shared office line, so it only
37
+ * matches somebody this caller's view already holds, and an agency's two
38
+ * clients who share a switchboard stay two records.
39
+ *
40
+ * ## The facet, and what names no site
41
+ *
42
+ * With a Source site the profile lives in that site's group's facet, as a
43
+ * capture's would. With none it lives in `ORG_CONTACT_HOLDER_ID`, and the row
44
+ * has no capturing site: `hostId` is `null`, `capturedByHostIds` is empty, and
45
+ * the record says so instead of guessing a brand that never met the person.
46
+ *
47
+ * ## Consent
48
+ *
49
+ * Only with an address, since a basis for mail needs somewhere to send it. With
50
+ * a site, for that site or the group it disclosed. With none, the person told
51
+ * the organization yes, so the grant is written on every site it has. One entry
52
+ * per site, each a group of one: `byHost[hostId]` stays a lookup, and a site
53
+ * added next month inherits nothing.
54
+ *
55
+ * ## What is not announced
56
+ *
57
+ * A created person with no site or no address fires no `contactCreated`: the
58
+ * event belongs to a site, and what listens for it is mostly mail. The team
59
+ * added them on purpose, so owner rules and domain matching have nothing to add
60
+ * either.
61
+ */
62
+ export interface AddContactByHandInput {
63
+ orgId: string;
64
+ org: Record<string, unknown> | null | undefined;
65
+ /** The site they came from, or `null` for none. */
66
+ sourceHostId: string | null;
67
+ /** Normalized, or `null` for a person with no address yet. */
68
+ email: string | null;
69
+ name?: string;
70
+ facet?: ContactProfileInput;
71
+ tags?: readonly string[];
72
+ /** An opt-in the person gave. Ignored with no address. */
73
+ marketingConsent?: boolean;
74
+ /** The consent-group sentence shown beside the box, with a Source site. */
75
+ disclosedConsentGroup?: string;
76
+ }
77
+ export type AddContactByHandVerdict = {
78
+ contactId: string;
79
+ created: boolean;
80
+ } | {
81
+ refused: 'band' | 'erased' | 'error';
82
+ };
83
+ /** Adds the person, or merges into who they already are. Never rejects. */
84
+ export declare function addContactByHand(input: AddContactByHandInput): Promise<AddContactByHandVerdict>;
@@ -0,0 +1,212 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { CAPTURED_BY_HOST_FIELD, checkCrmRecordsQuota, composeContactName, consentGroupForGrant, consentGroupForHost, consentGroupScope, CONTACT_COMPANY_IDS_FIELD, CONTACT_FACETS_FIELD, contactFacetPath, crmDefaultScopeOf, crmNewRecordListFields, crmReadTokens, createResourceUid, MARKETING_CONSENT_BY_HOST_FIELD, marketingConsentFieldsForGroup, marketingConsentFieldsForHost, nameSearchFields, ORG_CONTACT_HOLDER_ID, ORG_SCOPE_TOKEN, planContactCompanyLink, readContactCompanyLink } from "@aglyn/aglyn/server";
19
+ import { contactCompanyLinkFields, firebaseAdmin, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
20
+ import { scheduleCapturedEmailCheck } from "@aglyn/tenant-data-admin/server/capture-email-check";
21
+ import { settleCompanyContactsCounts } from "@aglyn/tenant-data-admin/server/contact-company-link";
22
+ import { emailIndexBeside, findContactByEmail, writeContactEmailIndex } from "@aglyn/tenant-data-admin/server/contact-email-index";
23
+ import { countCrmRecords } from "@aglyn/tenant-data-admin/server/crm-records";
24
+ import { hostRefusesCaptureForErasure } from "@aglyn/tenant-data-admin/server/email-suppression";
25
+ import { storableProfile } from "@aglyn/tenant-data-admin/server/upsert-contact";
26
+ import { FieldValue } from "firebase-admin/firestore";
27
+ import { ORG_HOLDER_GROUP, contactPrimaryGroup } from "../model/contact-holder.js";
28
+ import { orgHostIds } from "./org-caller.js";
29
+ /** The Consent entries for the opt-in, keyed `byHost[hostId]`. */ function consentEntries(input, group, hostIds) {
30
+ if (!input.marketingConsent || !input.email) return {};
31
+ const now = Date.now();
32
+ const fields = input.sourceHostId ? marketingConsentFieldsForGroup(consentGroupForGrant(group, input.disclosedConsentGroup), now) : Object.assign({}, ...hostIds.map((hostId)=>marketingConsentFieldsForHost(hostId, now)[MARKETING_CONSENT_BY_HOST_FIELD]));
33
+ return input.sourceHostId ? fields[MARKETING_CONSENT_BY_HOST_FIELD] : fields;
34
+ }
35
+ /** Who a person with this number already is, among those the caller can see. */ async function findVisibleContactByPhone(contacts, phone, readable) {
36
+ const matches = await contacts.where('phone', '==', phone).limit(10).get();
37
+ const visible = matches.docs.find((doc)=>{
38
+ if (!readable) return true;
39
+ const tokens = doc.get('visibleTo');
40
+ return Array.isArray(tokens) && tokens.some((token)=>readable.includes(String(token)));
41
+ });
42
+ return visible != null ? visible : null;
43
+ }
44
+ /** The profile written into a holder's existing facet, one dotted path per field. */ function facetUpdate(groupId, profile, name, tags) {
45
+ const { custom } = profile, fields = _object_without_properties_loose(profile, [
46
+ "custom"
47
+ ]);
48
+ const update = {};
49
+ for (const [field, value] of Object.entries(fields)){
50
+ if (value !== undefined) update[contactFacetPath(groupId, field)] = value;
51
+ }
52
+ for (const [key, value] of Object.entries(custom != null ? custom : {})){
53
+ update[contactFacetPath(groupId, `custom.${key}`)] = value;
54
+ }
55
+ if (name) update[contactFacetPath(groupId, 'name')] = name;
56
+ if (tags.length) update[contactFacetPath(groupId, 'tags')] = FieldValue.arrayUnion(...tags);
57
+ return update;
58
+ }
59
+ /** Adds the person, or merges into who they already are. Never rejects. */ export async function addContactByHand(input) {
60
+ try {
61
+ var _input_tags;
62
+ const { orgId, org, sourceHostId, email } = input;
63
+ const firestore = firebaseAdmin.app().firestore();
64
+ const orgRef = firestore.collection('orgs').doc(orgId);
65
+ const contacts = orgRef.collection('contacts');
66
+ const group = sourceHostId ? consentGroupForHost(org, sourceHostId) : ORG_HOLDER_GROUP;
67
+ const hostIds = sourceHostId ? [
68
+ sourceHostId
69
+ ] : await orgHostIds(firestore, orgId);
70
+ const tags = (_input_tags = input.tags) != null ? _input_tags : [];
71
+ const profile = storableProfile(input.facet);
72
+ if (profile.address === null) delete profile.address;
73
+ if (profile.otherAddress === null) delete profile.otherAddress;
74
+ const composedName = composeContactName(profile.firstName, profile.lastName);
75
+ const canonicalName = (input.name || composedName || '').slice(0, 120);
76
+ const holderName = (composedName || input.name || '').slice(0, 120);
77
+ // The one number the record can be found by; a person who gave only a
78
+ // mobile is found by that.
79
+ const phone = profile.phone || profile.mobilePhone || '';
80
+ const scope = crmDefaultScopeOf(org) === 'org' ? [
81
+ ORG_SCOPE_TOKEN
82
+ ] : consentGroupScope(group);
83
+ const existing = email ? await findContactByEmail(contacts, email) : phone ? await findVisibleContactByPhone(contacts, phone, sourceHostId && crmDefaultScopeOf(org) !== 'org' ? crmReadTokens(group) : null) : null;
84
+ if (existing) {
85
+ var _existing_data;
86
+ const data = (_existing_data = existing.data()) != null ? _existing_data : {};
87
+ const groupId = sourceHostId ? group.groupId : contactPrimaryGroup(data, org).groupId || ORG_CONTACT_HOLDER_ID;
88
+ const { companyId } = profile, withoutCompany = _object_without_properties_loose(profile, [
89
+ "companyId"
90
+ ]);
91
+ const link = companyId ? planContactCompanyLink(readContactCompanyLink(data, groupId), companyId) : null;
92
+ const consent = consentEntries(input, group, hostIds);
93
+ await existing.ref.update(_extends({}, facetUpdate(groupId, withoutCompany, holderName, tags), link ? contactCompanyLinkFields(link, groupId) : {}, phone && !data['phone'] ? {
94
+ phone
95
+ } : {}, Object.fromEntries(Object.entries(consent).map(([hostId, entry])=>[
96
+ `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}`,
97
+ entry
98
+ ])), sourceHostId ? {
99
+ [CAPTURED_BY_HOST_FIELD]: FieldValue.arrayUnion(sourceHostId),
100
+ visibleTo: FieldValue.arrayUnion(...scope)
101
+ } : {}, {
102
+ updatedAt: FieldValue.serverTimestamp()
103
+ }));
104
+ if (link) await settleCompanyContactsCounts(orgRef.collection('companies'), link);
105
+ await restampCrmListFieldsAt(existing.ref, 'contacts');
106
+ return {
107
+ contactId: existing.id,
108
+ created: false
109
+ };
110
+ }
111
+ // An erased person stays erased, whichever door they come back through.
112
+ if (email) {
113
+ for (const hostId of hostIds){
114
+ if (await hostRefusesCaptureForErasure(hostId, email, firestore)) {
115
+ return {
116
+ refused: 'erased'
117
+ };
118
+ }
119
+ }
120
+ }
121
+ const { crmRecordsCount } = await countCrmRecords(orgRef, contacts);
122
+ if (!checkCrmRecordsQuota(org != null ? org : null, crmRecordsCount).allowed) {
123
+ return {
124
+ refused: 'band'
125
+ };
126
+ }
127
+ const facet = _extends({
128
+ sources: {
129
+ manual: true
130
+ },
131
+ interactions: [
132
+ _extends({
133
+ type: 'manual',
134
+ atMs: Date.now()
135
+ }, sourceHostId ? {
136
+ hostId: sourceHostId
137
+ } : {}, {
138
+ summary: 'Added by hand'
139
+ })
140
+ ],
141
+ tags: [
142
+ ...tags
143
+ ]
144
+ }, holderName ? {
145
+ name: holderName
146
+ } : {}, profile);
147
+ const consent = consentEntries(input, group, hostIds);
148
+ const record = _extends({
149
+ hostId: sourceHostId,
150
+ [CAPTURED_BY_HOST_FIELD]: sourceHostId ? [
151
+ sourceHostId
152
+ ] : [],
153
+ visibleTo: scope,
154
+ /*
155
+ * BLANK RATHER THAN ABSENT. The Contacts table sorts by every column, and
156
+ * a Firestore `orderBy` drops a document that lacks the field, so a person
157
+ * with no address would vanish from the list the moment someone sorted it
158
+ * by email. Blank sorts first and reads as nothing, and no reader treats
159
+ * it as an address: `normalizeContactEmail` answers `null`.
160
+ */ email: email != null ? email : ''
161
+ }, canonicalName ? nameSearchFields(canonicalName) : {}, phone ? {
162
+ phone
163
+ } : {}, profile.companyName ? {
164
+ companyName: profile.companyName
165
+ } : {}, profile.companyId ? {
166
+ [CONTACT_COMPANY_IDS_FIELD]: [
167
+ profile.companyId
168
+ ]
169
+ } : {}, {
170
+ [CONTACT_FACETS_FIELD]: {
171
+ [group.groupId]: facet
172
+ }
173
+ }, Object.keys(consent).length ? {
174
+ [MARKETING_CONSENT_BY_HOST_FIELD]: consent
175
+ } : {}, {
176
+ createdAt: FieldValue.serverTimestamp(),
177
+ updatedAt: FieldValue.serverTimestamp()
178
+ });
179
+ const created = contacts.doc(createResourceUid());
180
+ await created.set(_extends({}, record, crmNewRecordListFields('contacts', record)));
181
+ if (email) {
182
+ await writeContactEmailIndex(emailIndexBeside(contacts), created.id, [
183
+ email
184
+ ]);
185
+ scheduleCapturedEmailCheck(_extends({
186
+ orgId
187
+ }, sourceHostId ? {
188
+ hostId: sourceHostId
189
+ } : {}, {
190
+ email
191
+ }));
192
+ }
193
+ if (profile.companyId) {
194
+ await settleCompanyContactsCounts(orgRef.collection('companies'), planContactCompanyLink({
195
+ companyId: null,
196
+ companyIds: [],
197
+ heldElsewhere: []
198
+ }, profile.companyId));
199
+ }
200
+ return {
201
+ contactId: created.id,
202
+ created: true
203
+ };
204
+ } catch (error) {
205
+ console.error('[crm] addContactByHand failed', error);
206
+ return {
207
+ refused: 'error'
208
+ };
209
+ }
210
+ }
211
+
212
+ //# sourceMappingURL=add-contact-by-hand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/add-contact-by-hand.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 CAPTURED_BY_HOST_FIELD,\n checkCrmRecordsQuota,\n composeContactName,\n type ConsentGroup,\n consentGroupForGrant,\n consentGroupForHost,\n consentGroupScope,\n CONTACT_COMPANY_IDS_FIELD,\n CONTACT_FACETS_FIELD,\n type ContactFacet,\n contactFacetPath,\n crmDefaultScopeOf,\n crmNewRecordListFields,\n crmReadTokens,\n createResourceUid,\n MARKETING_CONSENT_BY_HOST_FIELD,\n marketingConsentFieldsForGroup,\n marketingConsentFieldsForHost,\n nameSearchFields,\n ORG_CONTACT_HOLDER_ID,\n ORG_SCOPE_TOKEN,\n planContactCompanyLink,\n readContactCompanyLink,\n} from '@aglyn/aglyn/server'\nimport {\n contactCompanyLinkFields,\n firebaseAdmin,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { scheduleCapturedEmailCheck } from '@aglyn/tenant-data-admin/server/capture-email-check'\nimport { settleCompanyContactsCounts } from '@aglyn/tenant-data-admin/server/contact-company-link'\nimport {\n emailIndexBeside,\n findContactByEmail,\n writeContactEmailIndex,\n} from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { countCrmRecords } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { hostRefusesCaptureForErasure } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n type ContactProfileInput,\n storableProfile,\n} from '@aglyn/tenant-data-admin/server/upsert-contact'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { ORG_HOLDER_GROUP, contactPrimaryGroup } from '../model/contact-holder'\nimport { orgHostIds } from './org-caller'\n\n/**\n * A PERSON THE TEAM ADDS WHO DID NOT COME THROUGH A SITE'S CAPTURE (AGL-4122).\n *\n * `upsertHostContact` is the door for somebody a site met, and it needs what a\n * site's capture always has: a site and an address. A person typed in by the\n * team may have neither. They may be a name and a phone number, added at the\n * organization level, because the team is working a deal with someone whose\n * email comes later. That is this path, and only that: a site AND an address\n * still go through the capture door, which is why the route asks.\n *\n * ## Who the person already is\n *\n * The address when there is one, through the same index the capture reads. With\n * none, the phone number. With neither, nobody: two blank emails are not the\n * same person, and a name is not an identity.\n *\n * The phone lookup is the one narrowing the capture's address lookup refuses,\n * deliberately. An address names one human, so the capture recognizes them\n * across every site. A phone number can be a shared office line, so it only\n * matches somebody this caller's view already holds, and an agency's two\n * clients who share a switchboard stay two records.\n *\n * ## The facet, and what names no site\n *\n * With a Source site the profile lives in that site's group's facet, as a\n * capture's would. With none it lives in `ORG_CONTACT_HOLDER_ID`, and the row\n * has no capturing site: `hostId` is `null`, `capturedByHostIds` is empty, and\n * the record says so instead of guessing a brand that never met the person.\n *\n * ## Consent\n *\n * Only with an address, since a basis for mail needs somewhere to send it. With\n * a site, for that site or the group it disclosed. With none, the person told\n * the organization yes, so the grant is written on every site it has. One entry\n * per site, each a group of one: `byHost[hostId]` stays a lookup, and a site\n * added next month inherits nothing.\n *\n * ## What is not announced\n *\n * A created person with no site or no address fires no `contactCreated`: the\n * event belongs to a site, and what listens for it is mostly mail. The team\n * added them on purpose, so owner rules and domain matching have nothing to add\n * either.\n */\n\nexport interface AddContactByHandInput {\n orgId: string\n org: Record<string, unknown> | null | undefined\n /** The site they came from, or `null` for none. */\n sourceHostId: string | null\n /** Normalized, or `null` for a person with no address yet. */\n email: string | null\n name?: string\n facet?: ContactProfileInput\n tags?: readonly string[]\n /** An opt-in the person gave. Ignored with no address. */\n marketingConsent?: boolean\n /** The consent-group sentence shown beside the box, with a Source site. */\n disclosedConsentGroup?: string\n}\n\nexport type AddContactByHandVerdict =\n | { contactId: string; created: boolean }\n | { refused: 'band' | 'erased' | 'error' }\n\n/** The Consent entries for the opt-in, keyed `byHost[hostId]`. */\nfunction consentEntries(\n input: AddContactByHandInput,\n group: ConsentGroup,\n hostIds: readonly string[],\n): Record<string, unknown> {\n if (!input.marketingConsent || !input.email) return {}\n const now = Date.now()\n const fields = input.sourceHostId\n ? marketingConsentFieldsForGroup(\n consentGroupForGrant(group, input.disclosedConsentGroup),\n now,\n )\n : Object.assign(\n {},\n ...hostIds.map(\n (hostId) =>\n marketingConsentFieldsForHost(hostId, now)[MARKETING_CONSENT_BY_HOST_FIELD],\n ),\n )\n return input.sourceHostId\n ? (fields[MARKETING_CONSENT_BY_HOST_FIELD] as Record<string, unknown>)\n : fields\n}\n\n/** Who a person with this number already is, among those the caller can see. */\nasync function findVisibleContactByPhone(\n contacts: FirebaseFirestore.CollectionReference,\n phone: string,\n readable: readonly string[] | null,\n): Promise<FirebaseFirestore.DocumentSnapshot | null> {\n const matches = await contacts.where('phone', '==', phone).limit(10).get()\n const visible = matches.docs.find((doc) => {\n if (!readable) return true\n const tokens = doc.get('visibleTo')\n return Array.isArray(tokens) && tokens.some((token) => readable.includes(String(token)))\n })\n return visible ?? null\n}\n\n/** The profile written into a holder's existing facet, one dotted path per field. */\nfunction facetUpdate(\n groupId: string,\n profile: ReturnType<typeof storableProfile>,\n name: string,\n tags: readonly string[],\n): Record<string, unknown> {\n const { custom, ...fields } = profile\n const update: Record<string, unknown> = {}\n for (const [field, value] of Object.entries(fields)) {\n if (value !== undefined) update[contactFacetPath(groupId, field)] = value\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[contactFacetPath(groupId, `custom.${key}`)] = value\n }\n if (name) update[contactFacetPath(groupId, 'name')] = name\n if (tags.length) update[contactFacetPath(groupId, 'tags')] = FieldValue.arrayUnion(...tags)\n return update\n}\n\n/** Adds the person, or merges into who they already are. Never rejects. */\nexport async function addContactByHand(\n input: AddContactByHandInput,\n): Promise<AddContactByHandVerdict> {\n try {\n const { orgId, org, sourceHostId, email } = input\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contacts = orgRef.collection('contacts')\n const group = sourceHostId ? consentGroupForHost(org, sourceHostId) : ORG_HOLDER_GROUP\n const hostIds = sourceHostId ? [sourceHostId] : await orgHostIds(firestore, orgId)\n const tags = input.tags ?? []\n\n const profile = storableProfile(input.facet)\n if (profile.address === null) delete profile.address\n if (profile.otherAddress === null) delete profile.otherAddress\n const composedName = composeContactName(profile.firstName, profile.lastName)\n const canonicalName = (input.name || composedName || '').slice(0, 120)\n const holderName = (composedName || input.name || '').slice(0, 120)\n // The one number the record can be found by; a person who gave only a\n // mobile is found by that.\n const phone = profile.phone || profile.mobilePhone || ''\n const scope = crmDefaultScopeOf(org) === 'org' ? [ORG_SCOPE_TOKEN] : consentGroupScope(group)\n\n const existing = email\n ? await findContactByEmail(contacts, email)\n : phone\n ? await findVisibleContactByPhone(\n contacts,\n phone,\n sourceHostId && crmDefaultScopeOf(org) !== 'org' ? crmReadTokens(group) : null,\n )\n : null\n\n if (existing) {\n const data = (existing.data() ?? {}) as Record<string, unknown>\n const groupId = sourceHostId\n ? group.groupId\n : contactPrimaryGroup(data, org).groupId || ORG_CONTACT_HOLDER_ID\n const { companyId, ...withoutCompany } = profile\n const link = companyId\n ? planContactCompanyLink(readContactCompanyLink(data, groupId), companyId)\n : null\n const consent = consentEntries(input, group, hostIds)\n await existing.ref.update({\n ...facetUpdate(groupId, withoutCompany, holderName, tags),\n ...(link ? contactCompanyLinkFields(link, groupId) : {}),\n ...(phone && !data['phone'] ? { phone } : {}),\n ...Object.fromEntries(\n Object.entries(consent).map(([hostId, entry]) => [\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}`,\n entry,\n ]),\n ),\n ...(sourceHostId\n ? {\n [CAPTURED_BY_HOST_FIELD]: FieldValue.arrayUnion(sourceHostId),\n visibleTo: FieldValue.arrayUnion(...scope),\n }\n : {}),\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (link) await settleCompanyContactsCounts(orgRef.collection('companies'), link)\n await restampCrmListFieldsAt(existing.ref, 'contacts')\n return { contactId: existing.id, created: false }\n }\n\n // An erased person stays erased, whichever door they come back through.\n if (email) {\n for (const hostId of hostIds) {\n if (await hostRefusesCaptureForErasure(hostId, email, firestore)) {\n return { refused: 'erased' }\n }\n }\n }\n\n const { crmRecordsCount } = await countCrmRecords(orgRef, contacts)\n if (!checkCrmRecordsQuota((org as never) ?? null, crmRecordsCount).allowed) {\n return { refused: 'band' }\n }\n\n const facet: Partial<ContactFacet> & Record<string, unknown> = {\n sources: { manual: true },\n interactions: [\n {\n type: 'manual',\n atMs: Date.now(),\n ...(sourceHostId ? { hostId: sourceHostId } : {}),\n summary: 'Added by hand',\n },\n ],\n tags: [...tags],\n ...(holderName ? { name: holderName } : {}),\n ...profile,\n }\n const consent = consentEntries(input, group, hostIds)\n const record: Record<string, unknown> = {\n hostId: sourceHostId,\n [CAPTURED_BY_HOST_FIELD]: sourceHostId ? [sourceHostId] : [],\n visibleTo: scope,\n /*\n * BLANK RATHER THAN ABSENT. The Contacts table sorts by every column, and\n * a Firestore `orderBy` drops a document that lacks the field, so a person\n * with no address would vanish from the list the moment someone sorted it\n * by email. Blank sorts first and reads as nothing, and no reader treats\n * it as an address: `normalizeContactEmail` answers `null`.\n */\n email: email ?? '',\n ...(canonicalName ? nameSearchFields(canonicalName) : {}),\n ...(phone ? { phone } : {}),\n ...(profile.companyName ? { companyName: profile.companyName } : {}),\n ...(profile.companyId ? { [CONTACT_COMPANY_IDS_FIELD]: [profile.companyId] } : {}),\n [CONTACT_FACETS_FIELD]: { [group.groupId]: facet },\n ...(Object.keys(consent).length ? { [MARKETING_CONSENT_BY_HOST_FIELD]: consent } : {}),\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const created = contacts.doc(createResourceUid())\n await created.set({\n ...record,\n ...crmNewRecordListFields('contacts', record),\n })\n if (email) {\n await writeContactEmailIndex(emailIndexBeside(contacts), created.id, [email])\n scheduleCapturedEmailCheck({ orgId, ...(sourceHostId ? { hostId: sourceHostId } : {}), email })\n }\n if (profile.companyId) {\n await settleCompanyContactsCounts(\n orgRef.collection('companies'),\n planContactCompanyLink(\n { companyId: null, companyIds: [], heldElsewhere: [] },\n profile.companyId,\n ),\n )\n }\n return { contactId: created.id, created: true }\n } catch (error) {\n console.error('[crm] addContactByHand failed', error)\n return { refused: 'error' }\n }\n}\n"],"names":["CAPTURED_BY_HOST_FIELD","checkCrmRecordsQuota","composeContactName","consentGroupForGrant","consentGroupForHost","consentGroupScope","CONTACT_COMPANY_IDS_FIELD","CONTACT_FACETS_FIELD","contactFacetPath","crmDefaultScopeOf","crmNewRecordListFields","crmReadTokens","createResourceUid","MARKETING_CONSENT_BY_HOST_FIELD","marketingConsentFieldsForGroup","marketingConsentFieldsForHost","nameSearchFields","ORG_CONTACT_HOLDER_ID","ORG_SCOPE_TOKEN","planContactCompanyLink","readContactCompanyLink","contactCompanyLinkFields","firebaseAdmin","restampCrmListFieldsAt","scheduleCapturedEmailCheck","settleCompanyContactsCounts","emailIndexBeside","findContactByEmail","writeContactEmailIndex","countCrmRecords","hostRefusesCaptureForErasure","storableProfile","FieldValue","ORG_HOLDER_GROUP","contactPrimaryGroup","orgHostIds","consentEntries","input","group","hostIds","marketingConsent","email","now","Date","fields","sourceHostId","disclosedConsentGroup","Object","assign","map","hostId","findVisibleContactByPhone","contacts","phone","readable","matches","where","limit","get","visible","docs","find","doc","tokens","Array","isArray","some","token","includes","String","facetUpdate","groupId","profile","name","tags","custom","update","field","value","entries","undefined","key","length","arrayUnion","addContactByHand","orgId","org","firestore","app","orgRef","collection","facet","address","otherAddress","composedName","firstName","lastName","canonicalName","slice","holderName","mobilePhone","scope","existing","data","companyId","withoutCompany","link","consent","ref","fromEntries","entry","visibleTo","updatedAt","serverTimestamp","contactId","id","created","refused","crmRecordsCount","allowed","sources","manual","interactions","type","atMs","summary","record","companyName","keys","createdAt","set","companyIds","heldElsewhere","error","console"],"mappings":"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"}