@aglyn/plugins-crm 1.1.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.
- package/package.json +14 -14
- package/src/lib/components/company-contact-roles-card.js +5 -3
- package/src/lib/components/company-contact-roles-card.js.map +1 -1
- package/src/lib/components/contact-known-by-card.d.ts +2 -1
- package/src/lib/components/contact-known-by-card.js +16 -3
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/hooks/use-crm-record-names.d.ts +3 -2
- package/src/lib/hooks/use-crm-record-names.js +4 -4
- package/src/lib/hooks/use-crm-record-names.js.map +1 -1
- package/src/lib/model/deal-contact-roles.d.ts +12 -2
- package/src/lib/model/deal-contact-roles.js +17 -5
- package/src/lib/model/deal-contact-roles.js.map +1 -1
- package/src/lib/model/native-vocabulary.d.ts +9 -0
- package/src/lib/model/native-vocabulary.js +13 -0
- package/src/lib/model/native-vocabulary.js.map +1 -1
- package/src/lib/server/deal-contact-roles.d.ts +2 -0
- package/src/lib/server/deal-contact-roles.js +38 -1
- package/src/lib/server/deal-contact-roles.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.
|
|
34
|
-
"@aglyn/shared-util-email": "1.
|
|
35
|
-
"@aglyn/shared-util-http": "1.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.
|
|
28
|
+
"@aglyn/aglyn": "1.2.0",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.2.0",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.2.0",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.2.0",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.2.0",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.2.0",
|
|
34
|
+
"@aglyn/shared-util-email": "1.2.0",
|
|
35
|
+
"@aglyn/shared-util-http": "1.2.0",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.2.0",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.2.0",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.2.0",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.2.0",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.2.0",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { CRM_COLLECTIONS,
|
|
18
|
+
import { CRM_COLLECTIONS, pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
19
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
20
20
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
21
21
|
import { doc, updateDoc } from "firebase/firestore";
|
|
@@ -23,7 +23,7 @@ import { useCallback, useMemo, useState } from "react";
|
|
|
23
23
|
import { useCrmPicklist } from "../hooks/use-crm-picklist.js";
|
|
24
24
|
import { useCrmRecordNames } from "../hooks/use-crm-record-names.js";
|
|
25
25
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
26
|
-
import { companyContactRolesSave } from "../model/deal-contact-roles.js";
|
|
26
|
+
import { companyContactRolesOf, companyContactRolesSave } from "../model/deal-contact-roles.js";
|
|
27
27
|
import { ContactRolesEditor } from "./contact-roles-editor.js";
|
|
28
28
|
/**
|
|
29
29
|
* Salesforce's Account Contact Roles (AGL-4122): who at a company plays which
|
|
@@ -41,7 +41,7 @@ import { ContactRolesEditor } from "./contact-roles-editor.js";
|
|
|
41
41
|
org
|
|
42
42
|
});
|
|
43
43
|
const groupId = (_ref = (_scope_consentGroup = scope.consentGroup) == null ? void 0 : _scope_consentGroup.groupId) != null ? _ref : null;
|
|
44
|
-
const roles = useMemo(()=>
|
|
44
|
+
const roles = useMemo(()=>companyContactRolesOf(company), [
|
|
45
45
|
company
|
|
46
46
|
]);
|
|
47
47
|
const roleList = useCrmPicklist('opportunityContactRole', scope.orgId);
|
|
@@ -75,6 +75,8 @@ import { ContactRolesEditor } from "./contact-roles-editor.js";
|
|
|
75
75
|
}, async ()=>{
|
|
76
76
|
await updateDoc(doc(firestore, 'orgs', scope.orgId, CRM_COLLECTIONS.companies, companyId), {
|
|
77
77
|
contactRoles: verdict.contactRoles,
|
|
78
|
+
contactRoleContactIds: verdict.contactRoleContactIds,
|
|
79
|
+
contactRoleKeys: verdict.contactRoleKeys,
|
|
78
80
|
updatedAt: new Date()
|
|
79
81
|
});
|
|
80
82
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-contact-roles-card.tsx"],"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'use client'\n\nimport {\n CRM_COLLECTIONS,\n type CrmDealContactRole,\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-contact-roles-card.tsx"],"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'use client'\n\nimport {\n CRM_COLLECTIONS,\n type CrmDealContactRole,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport { useCrmRecordNames } from '../hooks/use-crm-record-names'\nimport { type CrmOrgDoc, useCrmScope } from '../hooks/use-crm-scope'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport { companyContactRolesOf, companyContactRolesSave } from '../model/deal-contact-roles'\nimport { ContactRolesEditor } from './contact-roles-editor'\n\nexport interface CompanyContactRolesCardProps {\n companyId: string\n company: { contactRoles?: unknown }\n /** The site the page is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrgDoc\n routes: CrmRoutes\n /** The listener's verdict on the company, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * Salesforce's Account Contact Roles (AGL-4122): who at a company plays which\n * part, and who the Primary is. Separate from the roles on a deal, as there: a\n * person's part in the company is a fact about the account, and their part in\n * one deal can differ. The same list of roles serves both.\n */\nexport function CompanyContactRolesCard(props: CompanyContactRolesCardProps) {\n const { companyId, company, hostId, org, routes, fromCache, unreadable } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const scope = useCrmScope({ hostId, org })\n const groupId = scope.consentGroup?.groupId ?? null\n const roles = useMemo(() => companyContactRolesOf(company), [company])\n const roleList = useCrmPicklist('opportunityContactRole', scope.orgId)\n const nameOf = useCrmRecordNames({\n orgId: scope.orgId,\n groupId,\n org: (org ?? null) as Record<string, unknown> | null,\n records: roles.map((row) => ({ kind: 'contact' as const, id: row.contactId })),\n })\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n const save = useCallback(\n async (next: CrmDealContactRole[], done: string) => {\n if (!scope.orgId) return false\n const verdict = companyContactRolesSave(company, next, roleList.picklist)\n if (verdict.ok === false) {\n enqueueSnackbar(verdict.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const written = await writeGuardedBySeed(\n { subject: 'company', unreadable, fromCache },\n async () => {\n await updateDoc(\n doc(firestore, 'orgs', scope.orgId as string, CRM_COLLECTIONS.companies, companyId),\n {\n contactRoles: verdict.contactRoles,\n contactRoleContactIds: verdict.contactRoleContactIds,\n contactRoleKeys: verdict.contactRoleKeys,\n updatedAt: new Date(),\n },\n )\n },\n )\n if (!written.ok) {\n enqueueSnackbar(written.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [scope.orgId, company, roleList.picklist, unreadable, fromCache, firestore, companyId, enqueueSnackbar],\n )\n\n return (\n <ContactRolesEditor\n noun=\"company\"\n roles={roles}\n busy={busy}\n adding={adding}\n onAddingChange={setAdding}\n ready={Boolean(scope.orgId)}\n hostId={hostId}\n org={org}\n routes={routes}\n picklist={roleList.picklist}\n nameOf={nameOf}\n help={pluginDocsHelp('companies', { anchor: '#contact-roles' })}\n emptyDescription=\"Name who at the company plays which part, one of them Primary.\"\n save={save}\n />\n )\n}\nCompanyContactRolesCard.displayName = 'CompanyContactRolesCard'\n\nexport default CompanyContactRolesCard\n"],"names":["CRM_COLLECTIONS","pluginDocsHelp","useSnackbar","useFirestore","writeGuardedBySeed","doc","updateDoc","useCallback","useMemo","useState","useCrmPicklist","useCrmRecordNames","useCrmScope","companyContactRolesOf","companyContactRolesSave","ContactRolesEditor","CompanyContactRolesCard","props","scope","companyId","company","hostId","org","routes","fromCache","unreadable","firestore","enqueueSnackbar","groupId","consentGroup","roles","roleList","orgId","nameOf","records","map","row","kind","id","contactId","busy","setBusy","adding","setAdding","save","next","done","verdict","picklist","ok","error","variant","persist","written","subject","companies","contactRoles","contactRoleContactIds","contactRoleKeys","updatedAt","Date","message","console","allowDuplicate","noun","onAddingChange","ready","Boolean","help","anchor","emptyDescription","displayName"],"mappings":"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"}
|
|
@@ -20,7 +20,8 @@ export interface ContactKnownByCardProps {
|
|
|
20
20
|
* the person's marketing consent FOR EACH of those sites — one verdict per
|
|
21
21
|
* capturing site, each naming the controller it was read for, because a
|
|
22
22
|
* basis is per (person, controller) and "consented" with nobody attached
|
|
23
|
-
* would be a different and false claim.
|
|
23
|
+
* would be a different and false claim. A person no site captured shows the
|
|
24
|
+
* same verdicts, one per site their consent is recorded on.
|
|
24
25
|
*
|
|
25
26
|
* Notes, tags, activity and commercial figures are each holder's own
|
|
26
27
|
* records and are not projected here: `orgContactRow` is an allow-list for
|
|
@@ -51,7 +51,8 @@ ConsentChip.displayName = 'ConsentChip';
|
|
|
51
51
|
* the person's marketing consent FOR EACH of those sites — one verdict per
|
|
52
52
|
* capturing site, each naming the controller it was read for, because a
|
|
53
53
|
* basis is per (person, controller) and "consented" with nobody attached
|
|
54
|
-
* would be a different and false claim.
|
|
54
|
+
* would be a different and false claim. A person no site captured shows the
|
|
55
|
+
* same verdicts, one per site their consent is recorded on.
|
|
55
56
|
*
|
|
56
57
|
* Notes, tags, activity and commercial figures are each holder's own
|
|
57
58
|
* records and are not projected here: `orgContactRow` is an allow-list for
|
|
@@ -128,11 +129,23 @@ ConsentChip.displayName = 'ConsentChip';
|
|
|
128
129
|
color: "text.secondary",
|
|
129
130
|
children: 'No site recorded'
|
|
130
131
|
}),
|
|
131
|
-
/*#__PURE__*/ _jsx(
|
|
132
|
+
!capturedByHostIds.length && consent.length ? /*#__PURE__*/ _jsx(Stack, {
|
|
133
|
+
useFlexGap: true,
|
|
134
|
+
direction: "row",
|
|
135
|
+
spacing: 1,
|
|
136
|
+
sx: {
|
|
137
|
+
flexWrap: 'wrap'
|
|
138
|
+
},
|
|
139
|
+
children: consent.map((entry)=>/*#__PURE__*/ _jsx(ConsentChip, {
|
|
140
|
+
entry: entry,
|
|
141
|
+
siteName: mount.siteName(entry.hostId)
|
|
142
|
+
}, entry.hostId))
|
|
143
|
+
}) : null,
|
|
144
|
+
capturedByHostIds.length ? /*#__PURE__*/ _jsx(Typography, {
|
|
132
145
|
variant: "caption",
|
|
133
146
|
color: "text.secondary",
|
|
134
147
|
children: 'The profile, notes, tags and timeline on this page are the first ' + "capturing site's. Open the person in another site to see what " + 'it holds.'
|
|
135
|
-
})
|
|
148
|
+
}) : null
|
|
136
149
|
]
|
|
137
150
|
})
|
|
138
151
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-known-by-card.tsx"],"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'use client'\n\nimport {\n orgContactConsentLabel,\n orgContactRow,\n pluginDocsHelp,\n type OrgContactHostConsent,\n} from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useMemo } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\n\nexport interface ContactKnownByCardProps {\n /** The document as read — the projection below is an allow-list over it. */\n row: Record<string, unknown>\n contactId: string\n /** The org document, the only input that can resolve a declared consent group. */\n org: CrmOrgDoc\n}\n\n/**\n * One consent verdict, rendered.\n *\n * The label always names the controller — `orgContactConsentLabel` is what\n * guarantees that — and the color follows the basis rather than replacing\n * it: a green chip with no name on it would be a bare \"consented\", which is\n * the claim the per-brand model exists to stop anybody making.\n */\nfunction ConsentChip(props: { entry: OrgContactHostConsent; siteName: string }) {\n const { entry, siteName } = props\n return (\n <Chip\n size=\"small\"\n color={\n entry.basis === 'granted'\n ? 'success'\n : entry.basis === 'declined'\n ? 'error'\n : 'default'\n }\n variant={entry.basis === 'unrecorded' ? 'outlined' : 'filled'}\n label={orgContactConsentLabel(entry, siteName)}\n />\n )\n}\nConsentChip.displayName = 'ConsentChip'\n\n/**\n * KNOWN BY — the cross-site fact, on the organization-level record\n * (AGL-2630).\n *\n * A contact document is shared by every site that has captured the person:\n * one human who touched two of an org's sites is ONE row, which is the\n * dedupe the shared address book exists for and what makes the billing unit\n * — unique people per org — mean anything. Under a site that fact is\n * invisible on purpose: a site's hub is one holder's view. This card is the\n * one place it is shown, and it shows exactly what the old org-level\n * address book showed and nothing more: which sites know the person, and\n * the person's marketing consent FOR EACH of those sites — one verdict per\n * capturing site, each naming the controller it was read for, because a\n * basis is per (person, controller) and \"consented\" with nobody attached\n * would be a different and false claim.\n *\n * Notes, tags, activity and commercial figures are each holder's own\n * records and are not projected here: `orgContactRow` is an allow-list for\n * that reason, and the card renders nothing it did not return. The rest of\n * the record page reads through the person's PRIMARY holder; this card is\n * how a reader learns which other sites hold a profile, and the link on\n * each site is how they open it there.\n *\n * Rendered only beneath the org mount — under a site the hook answers\n * `null` and so does the card.\n */\nexport function ContactKnownByCard(props: ContactKnownByCardProps) {\n const { row, contactId, org } = props\n const mount = useCrmOrgMount()\n const projected = useMemo(\n () => orgContactRow(row, contactId, (org ?? null) as Record<string, unknown> | null),\n [row, contactId, org],\n )\n if (!mount) return null\n const { capturedByHostIds, consent } = projected\n return (\n <CardDisplay\n header={'Known by'}\n help={pluginDocsHelp('contacts', { anchor: '#known-by' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {capturedByHostIds.length ? (\n <Stack spacing={1.5}>\n {capturedByHostIds.map((hostId) => {\n const entry = consent.find((verdict) => verdict.hostId === hostId)\n const href = mount.siteHubHref(hostId)\n const name = mount.siteName(hostId)\n return (\n <Stack\n useFlexGap\n key={hostId}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n {/*\n A site whose subdomain has not resolved must still be\n NAMED — the relationship is real, and hiding it would\n understate who knows this person — but not linked,\n because the route would not exist; a plain span, not an\n anchor to nowhere. Two elements are safe here: the card\n mounts only once the record's listener has answered, so\n there is no server render for a client render to disagree\n with.\n */}\n {href ? (\n <AppLink\n href={`${href}/contacts/${encodeURIComponent(contactId)}`}\n underline=\"hover\"\n >\n {name}\n </AppLink>\n ) : (\n <Typography component=\"span\" variant=\"body2\">\n {name}\n </Typography>\n )}\n {entry ? <ConsentChip entry={entry} siteName={name} /> : null}\n {entry?.declared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Consent given to ${entry.groupName ?? entry.groupId}, which ${name} is part of.`}\n </Typography>\n ) : null}\n </Stack>\n )\n })}\n </Stack>\n ) : (\n /*\n * Unattributed, not unknown. A row written before the attribution\n * existed names no site, and reading that as \"every site\" would\n * put a person in front of businesses that never met them.\n */\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The profile, notes, tags and timeline on this page are the first ' +\n \"capturing site's. Open the person in another site to see what \" +\n 'it holds.'}\n </Typography>\n </Stack>\n </CardDisplay>\n )\n}\nContactKnownByCard.displayName = 'ContactKnownByCard'\n\nexport default ContactKnownByCard\n"],"names":["orgContactConsentLabel","orgContactRow","pluginDocsHelp","AppLink","CardDisplay","Chip","Stack","Typography","useMemo","useCrmOrgMount","ConsentChip","props","entry","siteName","size","color","basis","variant","label","displayName","ContactKnownByCard","row","contactId","org","mount","projected","capturedByHostIds","consent","header","help","anchor","contentGutterX","contentGutterY","spacing","length","map","hostId","find","verdict","href","siteHubHref","name","useFlexGap","direction","sx","alignItems","flexWrap","encodeURIComponent","underline","component","declared","groupName","groupId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,sBAAsB,EACtBC,aAAa,EACbC,cAAc,QAET,eAAc;AACrB,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACvD,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,cAAc,QAAQ,gCAA4B;AAW3D;;;;;;;CAOC,GACD,SAASC,YAAYC,KAAyD;IAC5E,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGF;IAC5B,qBACE,KAACN;QACCS,MAAK;QACLC,OACEH,MAAMI,KAAK,KAAK,YACZ,YACAJ,MAAMI,KAAK,KAAK,aACd,UACA;QAERC,SAASL,MAAMI,KAAK,KAAK,eAAe,aAAa;QACrDE,OAAOlB,uBAAuBY,OAAOC;;AAG3C;AACAH,YAAYS,WAAW,GAAG;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,SAASC,mBAAmBT,KAA8B;IAC/D,MAAM,EAAEU,GAAG,EAAEC,SAAS,EAAEC,GAAG,EAAE,GAAGZ;IAChC,MAAMa,QAAQf;IACd,MAAMgB,YAAYjB,QAChB,IAAMP,cAAcoB,KAAKC,WAAYC,cAAAA,MAAO,OAC5C;QAACF;QAAKC;QAAWC;KAAI;IAEvB,IAAI,CAACC,OAAO,OAAO;IACnB,MAAM,EAAEE,iBAAiB,EAAEC,OAAO,EAAE,GAAGF;IACvC,qBACE,KAACrB;QACCwB,QAAQ;QACRC,MAAM3B,eAAe,YAAY;YAAE4B,QAAQ;QAAY;QACvDC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC1B;YAAM2B,SAAS;;gBACbP,kBAAkBQ,MAAM,iBACvB,KAAC5B;oBAAM2B,SAAS;8BACbP,kBAAkBS,GAAG,CAAC,CAACC;4BAqCOxB;wBApC7B,MAAMA,QAAQe,QAAQU,IAAI,CAAC,CAACC,UAAYA,QAAQF,MAAM,KAAKA;wBAC3D,MAAMG,OAAOf,MAAMgB,WAAW,CAACJ;wBAC/B,MAAMK,OAAOjB,MAAMX,QAAQ,CAACuB;wBAC5B,qBACE,MAAC9B;4BACCoC,UAAU;4BAEVC,WAAU;4BACVV,SAAS;4BACTW,IAAI;gCAAEC,YAAY;gCAAUC,UAAU;4BAAO;;gCAY5CP,qBACC,KAACpC;oCACCoC,MAAM,GAAGA,KAAK,UAAU,EAAEQ,mBAAmBzB,YAAY;oCACzD0B,WAAU;8CAETP;mDAGH,KAAClC;oCAAW0C,WAAU;oCAAOhC,SAAQ;8CAClCwB;;gCAGJ7B,sBAAQ,KAACF;oCAAYE,OAAOA;oCAAOC,UAAU4B;qCAAW;gCACxD7B,CAAAA,yBAAAA,MAAOsC,QAAQ,kBACd,KAAC3C;oCAAWU,SAAQ;oCAAUF,OAAM;8CACjC,CAAC,iBAAiB,GAAEH,mBAAAA,MAAMuC,SAAS,YAAfvC,mBAAmBA,MAAMwC,OAAO,CAAC,QAAQ,EAAEX,KAAK,YAAY,CAAC;qCAElF;;2BAhCCL;oBAmCX;qBAGF;;;;WAIC,iBACD,KAAC7B;oBAAWU,SAAQ;oBAAQF,OAAM;8BAC/B;;8BAGL,KAACR;oBAAWU,SAAQ;oBAAUF,OAAM;8BACjC,sEACC,mEACA;;;;;AAKZ;AACAK,mBAAmBD,WAAW,GAAG;AAEjC,eAAeC,mBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-known-by-card.tsx"],"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'use client'\n\nimport {\n orgContactConsentLabel,\n orgContactRow,\n pluginDocsHelp,\n type OrgContactHostConsent,\n} from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useMemo } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\n\nexport interface ContactKnownByCardProps {\n /** The document as read — the projection below is an allow-list over it. */\n row: Record<string, unknown>\n contactId: string\n /** The org document, the only input that can resolve a declared consent group. */\n org: CrmOrgDoc\n}\n\n/**\n * One consent verdict, rendered.\n *\n * The label always names the controller — `orgContactConsentLabel` is what\n * guarantees that — and the color follows the basis rather than replacing\n * it: a green chip with no name on it would be a bare \"consented\", which is\n * the claim the per-brand model exists to stop anybody making.\n */\nfunction ConsentChip(props: { entry: OrgContactHostConsent; siteName: string }) {\n const { entry, siteName } = props\n return (\n <Chip\n size=\"small\"\n color={\n entry.basis === 'granted'\n ? 'success'\n : entry.basis === 'declined'\n ? 'error'\n : 'default'\n }\n variant={entry.basis === 'unrecorded' ? 'outlined' : 'filled'}\n label={orgContactConsentLabel(entry, siteName)}\n />\n )\n}\nConsentChip.displayName = 'ConsentChip'\n\n/**\n * KNOWN BY — the cross-site fact, on the organization-level record\n * (AGL-2630).\n *\n * A contact document is shared by every site that has captured the person:\n * one human who touched two of an org's sites is ONE row, which is the\n * dedupe the shared address book exists for and what makes the billing unit\n * — unique people per org — mean anything. Under a site that fact is\n * invisible on purpose: a site's hub is one holder's view. This card is the\n * one place it is shown, and it shows exactly what the old org-level\n * address book showed and nothing more: which sites know the person, and\n * the person's marketing consent FOR EACH of those sites — one verdict per\n * capturing site, each naming the controller it was read for, because a\n * basis is per (person, controller) and \"consented\" with nobody attached\n * would be a different and false claim. A person no site captured shows the\n * same verdicts, one per site their consent is recorded on.\n *\n * Notes, tags, activity and commercial figures are each holder's own\n * records and are not projected here: `orgContactRow` is an allow-list for\n * that reason, and the card renders nothing it did not return. The rest of\n * the record page reads through the person's PRIMARY holder; this card is\n * how a reader learns which other sites hold a profile, and the link on\n * each site is how they open it there.\n *\n * Rendered only beneath the org mount — under a site the hook answers\n * `null` and so does the card.\n */\nexport function ContactKnownByCard(props: ContactKnownByCardProps) {\n const { row, contactId, org } = props\n const mount = useCrmOrgMount()\n const projected = useMemo(\n () => orgContactRow(row, contactId, (org ?? null) as Record<string, unknown> | null),\n [row, contactId, org],\n )\n if (!mount) return null\n const { capturedByHostIds, consent } = projected\n return (\n <CardDisplay\n header={'Known by'}\n help={pluginDocsHelp('contacts', { anchor: '#known-by' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {capturedByHostIds.length ? (\n <Stack spacing={1.5}>\n {capturedByHostIds.map((hostId) => {\n const entry = consent.find((verdict) => verdict.hostId === hostId)\n const href = mount.siteHubHref(hostId)\n const name = mount.siteName(hostId)\n return (\n <Stack\n useFlexGap\n key={hostId}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n {/*\n A site whose subdomain has not resolved must still be\n NAMED — the relationship is real, and hiding it would\n understate who knows this person — but not linked,\n because the route would not exist; a plain span, not an\n anchor to nowhere. Two elements are safe here: the card\n mounts only once the record's listener has answered, so\n there is no server render for a client render to disagree\n with.\n */}\n {href ? (\n <AppLink\n href={`${href}/contacts/${encodeURIComponent(contactId)}`}\n underline=\"hover\"\n >\n {name}\n </AppLink>\n ) : (\n <Typography component=\"span\" variant=\"body2\">\n {name}\n </Typography>\n )}\n {entry ? <ConsentChip entry={entry} siteName={name} /> : null}\n {entry?.declared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Consent given to ${entry.groupName ?? entry.groupId}, which ${name} is part of.`}\n </Typography>\n ) : null}\n </Stack>\n )\n })}\n </Stack>\n ) : (\n /*\n * Unattributed, not unknown. A row written before the attribution\n * existed names no site, and reading that as \"every site\" would\n * put a person in front of businesses that never met them.\n */\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n )}\n {!capturedByHostIds.length && consent.length ? (\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap' }}>\n {consent.map((entry) => (\n <ConsentChip\n key={entry.hostId}\n entry={entry}\n siteName={mount.siteName(entry.hostId)}\n />\n ))}\n </Stack>\n ) : null}\n {capturedByHostIds.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The profile, notes, tags and timeline on this page are the first ' +\n \"capturing site's. Open the person in another site to see what \" +\n 'it holds.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nContactKnownByCard.displayName = 'ContactKnownByCard'\n\nexport default ContactKnownByCard\n"],"names":["orgContactConsentLabel","orgContactRow","pluginDocsHelp","AppLink","CardDisplay","Chip","Stack","Typography","useMemo","useCrmOrgMount","ConsentChip","props","entry","siteName","size","color","basis","variant","label","displayName","ContactKnownByCard","row","contactId","org","mount","projected","capturedByHostIds","consent","header","help","anchor","contentGutterX","contentGutterY","spacing","length","map","hostId","find","verdict","href","siteHubHref","name","useFlexGap","direction","sx","alignItems","flexWrap","encodeURIComponent","underline","component","declared","groupName","groupId"],"mappings":"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"}
|
|
@@ -23,8 +23,9 @@ export declare const CRM_RECORD_COLLECTIONS: Record<CrmRecordKind, string>;
|
|
|
23
23
|
*
|
|
24
24
|
* A contact's name is read through the viewing group's facet, the way the
|
|
25
25
|
* people list reads it, so one business renaming a person does not change
|
|
26
|
-
* what an unrelated business's task list shows; the email
|
|
27
|
-
* contact nobody has named. A company has a name and
|
|
26
|
+
* what an unrelated business's task list shows; the email, then the phone
|
|
27
|
+
* number, stands in for a contact nobody has named. A company has a name and
|
|
28
|
+
* a deal a title.
|
|
28
29
|
*/
|
|
29
30
|
export declare function crmRecordName(kind: CrmRecordKind, data: Record<string, unknown> | undefined, groupId: string): string;
|
|
30
31
|
/** Test seam: forget everything, so a spec's fixtures are what get read. */
|
|
@@ -28,14 +28,14 @@ import { contactPrimaryGroup } from "../model/contact-record.js";
|
|
|
28
28
|
*
|
|
29
29
|
* A contact's name is read through the viewing group's facet, the way the
|
|
30
30
|
* people list reads it, so one business renaming a person does not change
|
|
31
|
-
* what an unrelated business's task list shows; the email
|
|
32
|
-
* contact nobody has named. A company has a name and
|
|
31
|
+
* what an unrelated business's task list shows; the email, then the phone
|
|
32
|
+
* number, stands in for a contact nobody has named. A company has a name and
|
|
33
|
+
* a deal a title.
|
|
33
34
|
*/ export function crmRecordName(kind, data, groupId) {
|
|
34
35
|
if (!data) return '';
|
|
35
36
|
switch(kind){
|
|
36
37
|
case 'contact':
|
|
37
|
-
|
|
38
|
-
return contactDisplayName(data, groupId) || String((_data_email = data['email']) != null ? _data_email : '');
|
|
38
|
+
return contactDisplayName(data, groupId) || String(data['email'] || data['phone'] || '');
|
|
39
39
|
case 'company':
|
|
40
40
|
var _data_name;
|
|
41
41
|
return String((_data_name = data['name']) != null ? _data_name : '');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-record-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 { contactDisplayName, CRM_COLLECTIONS } from '@aglyn/aglyn'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { doc, type Firestore, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { contactPrimaryGroup } from '../model/contact-record'\n\nexport type CrmRecordKind = 'contact' | 'company' | 'deal'\n\n/** The collection each kind of record lives in, under `orgs/{orgId}/`. */\nexport const CRM_RECORD_COLLECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n}\n\n/**\n * What a record is CALLED, from its document.\n *\n * A contact's name is read through the viewing group's facet, the way the\n * people list reads it, so one business renaming a person does not change\n * what an unrelated business's task list shows; the email stands in for a\n * contact nobody has named. A company has a name and a deal a title.\n */\nexport function crmRecordName(\n kind: CrmRecordKind,\n data: Record<string, unknown> | undefined,\n groupId: string,\n): string {\n if (!data) return ''\n switch (kind) {\n case 'contact':\n return contactDisplayName(data, groupId) || String(data['email'] ?? '')\n case 'company':\n return String(data['name'] ?? '')\n case 'deal':\n return String(data['title'] ?? '')\n }\n}\n\n/**\n * Names already read, keyed by `orgId/collection/id`, shared by every\n * mount for the life of the page.\n *\n * A task list names the same handful of records over and over — five tasks\n * about one deal are five rows and one name — and a person moves between the\n * tasks section, a contact's page and the dashboard without the records\n * changing underneath them. `null` records a document that does not exist,\n * so a task pointing at a deleted record costs one read rather than one per\n * render. A rename is picked up on the next page load, which is the right\n * staleness for a label beside a link to the record itself.\n */\nconst nameCache = new Map<string, string | null>()\nconst pending = new Map<string, Promise<void>>()\n\nconst cacheKey = (orgId: string, kind: CrmRecordKind, id: string) =>\n `${orgId}/${CRM_RECORD_COLLECTIONS[kind]}/${id}`\n\n/** Test seam: forget everything, so a spec's fixtures are what get read. */\nexport function resetCrmRecordNameCache(): void {\n nameCache.clear()\n pending.clear()\n}\n\n/**\n * The reads that fill the cache for `records` not already in it — each\n * record read once however many callers ask at the same time.\n */\nfunction startNameReads(\n firestore: Firestore,\n orgId: string,\n groupId: string | null,\n org: Record<string, unknown> | null | undefined,\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>,\n): Promise<void>[] {\n const reads: Promise<void>[] = []\n for (const { kind, id } of records) {\n if (!id) continue\n const key = cacheKey(orgId, kind, id)\n if (nameCache.has(key)) continue\n let read = pending.get(key)\n if (!read) {\n read = getDoc(doc(firestore, 'orgs', orgId, CRM_RECORD_COLLECTIONS[kind], id))\n .then((snapshot) => {\n const data = snapshot.exists() ? (snapshot.data() as Record<string, unknown>) : undefined\n nameCache.set(\n key,\n data ? crmRecordName(kind, data, groupId ?? contactPrimaryGroup(data, org).groupId) : null,\n )\n })\n .catch(() => {\n // A refused or failed read is not cached: the cell keeps showing\n // the id, and the next mount tries again.\n })\n .finally(() => {\n pending.delete(key)\n })\n pending.set(key, read)\n }\n reads.push(read)\n }\n return reads\n}\n\n/**\n * The names of `records`, read now — for a one-off like a file export,\n * which needs every name at once rather than a listener's re-render\n * (AGL-3521). Shares {@link useCrmRecordNames}' cache, and answers the same\n * lookup: `undefined` for a record that could not be named.\n */\nexport async function readCrmRecordNames(\n firestore: Firestore,\n options: {\n orgId: string\n groupId: string | null\n org?: Record<string, unknown> | null\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>\n },\n): Promise<(kind: CrmRecordKind, id: string) => string | undefined> {\n const { orgId, groupId, org, records } = options\n await Promise.all(startNameReads(firestore, orgId, groupId, org, records))\n return (kind, id) => nameCache.get(cacheKey(orgId, kind, id)) ?? undefined\n}\n\n/**\n * The names of the records a list of tasks points at, resolved once each.\n *\n * Returns a lookup rather than a map so the caller does not have to know the\n * cache's key shape. An unresolved record answers `undefined` — the cell\n * renders the link with the id until the name lands, which is one render\n * later on a warm cache and one round trip on a cold one.\n */\nexport function useCrmRecordNames(options: {\n orgId: string | null | undefined\n /**\n * The viewing group a contact is named through, or `null` at the\n * organization level (AGL-2630), where each contact is named through its\n * own primary holder, resolved from {@link org}.\n */\n groupId: string | null\n org?: Record<string, unknown> | null\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>\n}): (kind: CrmRecordKind, id: string) => string | undefined {\n const { orgId, groupId, org, records } = options\n const firestore = useFirestore()\n // A counter rather than a copy of the cache: the cache is the source of\n // truth and this only exists to re-render when it grows.\n const [, setResolved] = useState(0)\n\n // The pairs as one string, so the effect runs when the SET changes and not\n // when the caller builds a new array of the same pairs on every render.\n const wanted = records\n .map((record) => `${record.kind}:${record.id}`)\n .filter((entry, index, all) => all.indexOf(entry) === index)\n .sort()\n .join('\\n')\n\n useEffect(() => {\n if (!orgId || !wanted) return\n let active = true\n const reads = startNameReads(\n firestore,\n orgId,\n groupId,\n org,\n wanted.split('\\n').map((entry) => {\n const [kind, id] = entry.split(':') as [CrmRecordKind, string]\n return { kind, id }\n }),\n )\n if (!reads.length) return\n void Promise.all(reads).then(() => {\n if (active) setResolved((count) => count + 1)\n })\n return () => {\n active = false\n }\n }, [firestore, orgId, groupId, wanted])\n\n return useCallback(\n (kind: CrmRecordKind, id: string) => {\n if (!orgId) return undefined\n const hit = nameCache.get(cacheKey(orgId, kind, id))\n return hit === null ? undefined : hit\n },\n [orgId],\n )\n}\n"],"names":["contactDisplayName","CRM_COLLECTIONS","useFirestore","doc","getDoc","useCallback","useEffect","useState","contactPrimaryGroup","CRM_RECORD_COLLECTIONS","contact","company","companies","deal","deals","crmRecordName","kind","data","groupId","String","nameCache","Map","pending","cacheKey","orgId","id","resetCrmRecordNameCache","clear","startNameReads","firestore","org","records","reads","key","has","read","get","then","snapshot","exists","undefined","set","catch","finally","delete","push","readCrmRecordNames","options","Promise","all","useCrmRecordNames","setResolved","wanted","map","record","filter","entry","index","indexOf","sort","join","active","split","length","count","hit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-record-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 { contactDisplayName, CRM_COLLECTIONS } from '@aglyn/aglyn'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { doc, type Firestore, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { contactPrimaryGroup } from '../model/contact-record'\n\nexport type CrmRecordKind = 'contact' | 'company' | 'deal'\n\n/** The collection each kind of record lives in, under `orgs/{orgId}/`. */\nexport const CRM_RECORD_COLLECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n}\n\n/**\n * What a record is CALLED, from its document.\n *\n * A contact's name is read through the viewing group's facet, the way the\n * people list reads it, so one business renaming a person does not change\n * what an unrelated business's task list shows; the email, then the phone\n * number, stands in for a contact nobody has named. A company has a name and\n * a deal a title.\n */\nexport function crmRecordName(\n kind: CrmRecordKind,\n data: Record<string, unknown> | undefined,\n groupId: string,\n): string {\n if (!data) return ''\n switch (kind) {\n case 'contact':\n return (\n contactDisplayName(data, groupId) || String(data['email'] || data['phone'] || '')\n )\n case 'company':\n return String(data['name'] ?? '')\n case 'deal':\n return String(data['title'] ?? '')\n }\n}\n\n/**\n * Names already read, keyed by `orgId/collection/id`, shared by every\n * mount for the life of the page.\n *\n * A task list names the same handful of records over and over — five tasks\n * about one deal are five rows and one name — and a person moves between the\n * tasks section, a contact's page and the dashboard without the records\n * changing underneath them. `null` records a document that does not exist,\n * so a task pointing at a deleted record costs one read rather than one per\n * render. A rename is picked up on the next page load, which is the right\n * staleness for a label beside a link to the record itself.\n */\nconst nameCache = new Map<string, string | null>()\nconst pending = new Map<string, Promise<void>>()\n\nconst cacheKey = (orgId: string, kind: CrmRecordKind, id: string) =>\n `${orgId}/${CRM_RECORD_COLLECTIONS[kind]}/${id}`\n\n/** Test seam: forget everything, so a spec's fixtures are what get read. */\nexport function resetCrmRecordNameCache(): void {\n nameCache.clear()\n pending.clear()\n}\n\n/**\n * The reads that fill the cache for `records` not already in it — each\n * record read once however many callers ask at the same time.\n */\nfunction startNameReads(\n firestore: Firestore,\n orgId: string,\n groupId: string | null,\n org: Record<string, unknown> | null | undefined,\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>,\n): Promise<void>[] {\n const reads: Promise<void>[] = []\n for (const { kind, id } of records) {\n if (!id) continue\n const key = cacheKey(orgId, kind, id)\n if (nameCache.has(key)) continue\n let read = pending.get(key)\n if (!read) {\n read = getDoc(doc(firestore, 'orgs', orgId, CRM_RECORD_COLLECTIONS[kind], id))\n .then((snapshot) => {\n const data = snapshot.exists() ? (snapshot.data() as Record<string, unknown>) : undefined\n nameCache.set(\n key,\n data ? crmRecordName(kind, data, groupId ?? contactPrimaryGroup(data, org).groupId) : null,\n )\n })\n .catch(() => {\n // A refused or failed read is not cached: the cell keeps showing\n // the id, and the next mount tries again.\n })\n .finally(() => {\n pending.delete(key)\n })\n pending.set(key, read)\n }\n reads.push(read)\n }\n return reads\n}\n\n/**\n * The names of `records`, read now — for a one-off like a file export,\n * which needs every name at once rather than a listener's re-render\n * (AGL-3521). Shares {@link useCrmRecordNames}' cache, and answers the same\n * lookup: `undefined` for a record that could not be named.\n */\nexport async function readCrmRecordNames(\n firestore: Firestore,\n options: {\n orgId: string\n groupId: string | null\n org?: Record<string, unknown> | null\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>\n },\n): Promise<(kind: CrmRecordKind, id: string) => string | undefined> {\n const { orgId, groupId, org, records } = options\n await Promise.all(startNameReads(firestore, orgId, groupId, org, records))\n return (kind, id) => nameCache.get(cacheKey(orgId, kind, id)) ?? undefined\n}\n\n/**\n * The names of the records a list of tasks points at, resolved once each.\n *\n * Returns a lookup rather than a map so the caller does not have to know the\n * cache's key shape. An unresolved record answers `undefined` — the cell\n * renders the link with the id until the name lands, which is one render\n * later on a warm cache and one round trip on a cold one.\n */\nexport function useCrmRecordNames(options: {\n orgId: string | null | undefined\n /**\n * The viewing group a contact is named through, or `null` at the\n * organization level (AGL-2630), where each contact is named through its\n * own primary holder, resolved from {@link org}.\n */\n groupId: string | null\n org?: Record<string, unknown> | null\n records: ReadonlyArray<{ kind: CrmRecordKind; id: string }>\n}): (kind: CrmRecordKind, id: string) => string | undefined {\n const { orgId, groupId, org, records } = options\n const firestore = useFirestore()\n // A counter rather than a copy of the cache: the cache is the source of\n // truth and this only exists to re-render when it grows.\n const [, setResolved] = useState(0)\n\n // The pairs as one string, so the effect runs when the SET changes and not\n // when the caller builds a new array of the same pairs on every render.\n const wanted = records\n .map((record) => `${record.kind}:${record.id}`)\n .filter((entry, index, all) => all.indexOf(entry) === index)\n .sort()\n .join('\\n')\n\n useEffect(() => {\n if (!orgId || !wanted) return\n let active = true\n const reads = startNameReads(\n firestore,\n orgId,\n groupId,\n org,\n wanted.split('\\n').map((entry) => {\n const [kind, id] = entry.split(':') as [CrmRecordKind, string]\n return { kind, id }\n }),\n )\n if (!reads.length) return\n void Promise.all(reads).then(() => {\n if (active) setResolved((count) => count + 1)\n })\n return () => {\n active = false\n }\n }, [firestore, orgId, groupId, wanted])\n\n return useCallback(\n (kind: CrmRecordKind, id: string) => {\n if (!orgId) return undefined\n const hit = nameCache.get(cacheKey(orgId, kind, id))\n return hit === null ? undefined : hit\n },\n [orgId],\n )\n}\n"],"names":["contactDisplayName","CRM_COLLECTIONS","useFirestore","doc","getDoc","useCallback","useEffect","useState","contactPrimaryGroup","CRM_RECORD_COLLECTIONS","contact","company","companies","deal","deals","crmRecordName","kind","data","groupId","String","nameCache","Map","pending","cacheKey","orgId","id","resetCrmRecordNameCache","clear","startNameReads","firestore","org","records","reads","key","has","read","get","then","snapshot","exists","undefined","set","catch","finally","delete","push","readCrmRecordNames","options","Promise","all","useCrmRecordNames","setResolved","wanted","map","record","filter","entry","index","indexOf","sort","join","active","split","length","count","hit"],"mappings":"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"}
|
|
@@ -43,16 +43,26 @@ export declare function dealContactRolesSave(deal: {
|
|
|
43
43
|
ok: false;
|
|
44
44
|
error: string;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The roles a company holds, as stored. A deal's reader takes the Primary from
|
|
48
|
+
* the deal's `contactId`; a company has none, so its Primary is on the row.
|
|
49
|
+
*/
|
|
50
|
+
export declare function companyContactRolesOf(company: {
|
|
51
|
+
contactRoles?: unknown;
|
|
52
|
+
}): CrmDealContactRole[];
|
|
46
53
|
/**
|
|
47
54
|
* What a save of a company's card writes: the roles judged against the org's
|
|
48
|
-
* list
|
|
49
|
-
*
|
|
55
|
+
* list, the ids a merge or an erasure finds the company by, and the keys a
|
|
56
|
+
* rename of a role does. A company has no `contactId` beside them, so nothing
|
|
57
|
+
* else moves with the Primary.
|
|
50
58
|
*/
|
|
51
59
|
export declare function companyContactRolesSave(company: {
|
|
52
60
|
contactRoles?: unknown;
|
|
53
61
|
}, next: readonly CrmDealContactRole[], picklist: CrmPicklist): {
|
|
54
62
|
ok: true;
|
|
55
63
|
contactRoles: CrmDealContactRole[];
|
|
64
|
+
contactRoleContactIds: string[];
|
|
65
|
+
contactRoleKeys: string[];
|
|
56
66
|
} | {
|
|
57
67
|
ok: false;
|
|
58
68
|
error: string;
|
|
@@ -14,7 +14,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { DEAL_CONTACT_ROLES_MAX, dealContactRoleFields, dealContactRolesOf, judgeDealContactRoles } from "@aglyn/aglyn";
|
|
17
|
+
*/ import { crmDealContactRoleListFields, DEAL_CONTACT_ROLES_MAX, dealContactRoleFields, dealContactRolesOf, judgeDealContactRoles, readDealContactRoles } from "@aglyn/aglyn";
|
|
18
18
|
/*
|
|
19
19
|
* THE DEAL PAGE'S CONTACT ROLES CARD (AGL-3521), as pure functions: what
|
|
20
20
|
* each edit makes of the list, and the fields one save writes.
|
|
@@ -71,16 +71,28 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
71
71
|
primaryChanged: fields.contactId !== before
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The roles a company holds, as stored. A deal's reader takes the Primary from
|
|
76
|
+
* the deal's `contactId`; a company has none, so its Primary is on the row.
|
|
77
|
+
*/ export function companyContactRolesOf(company) {
|
|
78
|
+
return readDealContactRoles(company.contactRoles);
|
|
79
|
+
}
|
|
74
80
|
/**
|
|
75
81
|
* What a save of a company's card writes: the roles judged against the org's
|
|
76
|
-
* list
|
|
77
|
-
*
|
|
82
|
+
* list, the ids a merge or an erasure finds the company by, and the keys a
|
|
83
|
+
* rename of a role does. A company has no `contactId` beside them, so nothing
|
|
84
|
+
* else moves with the Primary.
|
|
78
85
|
*/ export function companyContactRolesSave(company, next, picklist) {
|
|
79
|
-
const judged = judgeDealContactRoles(picklist, next,
|
|
86
|
+
const judged = judgeDealContactRoles(picklist, next, companyContactRolesOf(company));
|
|
80
87
|
if (judged.ok === false) return judged;
|
|
88
|
+
const { contactRoleContactIds, contactRoleKeys } = crmDealContactRoleListFields({
|
|
89
|
+
contactRoles: judged.roles
|
|
90
|
+
});
|
|
81
91
|
return {
|
|
82
92
|
ok: true,
|
|
83
|
-
contactRoles: judged.roles
|
|
93
|
+
contactRoles: judged.roles,
|
|
94
|
+
contactRoleContactIds,
|
|
95
|
+
contactRoleKeys
|
|
84
96
|
};
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -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 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 * What a save of a company's card writes: the roles judged against the org's\n * list. A company has no `contactId` beside them, so nothing else moves with
|
|
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"}
|
|
@@ -23,5 +23,7 @@
|
|
|
23
23
|
* there, which is how a deal named a deleted contact before roles existed.
|
|
24
24
|
*/
|
|
25
25
|
export declare function sweepDealContactRoles(firestore: FirebaseFirestore.Firestore, deals: FirebaseFirestore.CollectionReference, contactId: string, to: string | null, label: string): Promise<number>;
|
|
26
|
+
/** One company's update once `contactId` is moved to `to`, or taken off for `null`. */
|
|
27
|
+
export declare function companyContactRolesUpdate(company: Record<string, unknown>, contactId: string, to: string | null): Record<string, unknown>;
|
|
26
28
|
/** One deal's update once `contactId` is moved to `to`, or taken off for `null`. */
|
|
27
29
|
export declare function dealContactRolesUpdate(deal: Record<string, unknown>, contactId: string, to: string | null): Record<string, unknown>;
|
|
@@ -14,7 +14,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { crmListFieldsPatch, dealContactRoleFields, dealContactRolesOf, dealContactRolesRepointed, dealContactRolesWithout } from "@aglyn/aglyn/app-utils/crm";
|
|
17
|
+
*/ import { CRM_COLLECTIONS, crmDealContactRoleListFields, crmListFieldsPatch, dealContactRoleFields, dealContactRolesOf, dealContactRolesRepointed, dealContactRolesWithout, readDealContactRoles } from "@aglyn/aglyn/app-utils/crm";
|
|
18
18
|
import { FieldValue } from "firebase-admin/firestore";
|
|
19
19
|
/**
|
|
20
20
|
* A CONTACT LEAVING THE DEALS IT IS ON (AGL-3521): Salesforce's Opportunity
|
|
@@ -29,6 +29,9 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
29
29
|
* (`dealContactRolesWithout` / `dealContactRolesRepointed`), its Primary and
|
|
30
30
|
* `contactId` written together and its list fields restamped, so a written
|
|
31
31
|
* deal falls out of both queries and the next pass finds only what is left.
|
|
32
|
+
*
|
|
33
|
+
* A company's Account Contact Roles (AGL-4122) follow the same person, so the
|
|
34
|
+
* sweep takes them too and no caller has to remember them.
|
|
32
35
|
*/ /** How many deals one pass reads, and one batch writes. */ const PAGE = 400;
|
|
33
36
|
/** A ceiling on passes, so a query that keeps answering cannot loop forever. */ const PASSES = 25;
|
|
34
37
|
/**
|
|
@@ -39,7 +42,9 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
39
42
|
* stops: a deal still naming the contact reads it as a person who is not
|
|
40
43
|
* there, which is how a deal named a deleted contact before roles existed.
|
|
41
44
|
*/ export async function sweepDealContactRoles(firestore, deals, contactId, to, label) {
|
|
45
|
+
var _deals_parent;
|
|
42
46
|
if (!contactId || contactId === to) return 0;
|
|
47
|
+
await sweepCompanyContactRoles(firestore, (_deals_parent = deals.parent) == null ? void 0 : _deals_parent.collection(CRM_COLLECTIONS.companies), contactId, to, label);
|
|
43
48
|
let written = 0;
|
|
44
49
|
// Each query on its own: a deal the first could not reach may still name
|
|
45
50
|
// the contact as its `contactId`.
|
|
@@ -67,6 +72,38 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
67
72
|
}
|
|
68
73
|
return written;
|
|
69
74
|
}
|
|
75
|
+
/** The companies naming `contactId`, moved to `to` or taken off for `null`. */ async function sweepCompanyContactRoles(firestore, companies, contactId, to, label) {
|
|
76
|
+
if (!companies) return;
|
|
77
|
+
try {
|
|
78
|
+
const query = companies.where('contactRoleContactIds', 'array-contains', contactId);
|
|
79
|
+
for(let pass = 0; pass < PASSES; pass += 1){
|
|
80
|
+
const page = await query.limit(PAGE).get();
|
|
81
|
+
if (page.empty) break;
|
|
82
|
+
const batch = firestore.batch();
|
|
83
|
+
for (const company of page.docs){
|
|
84
|
+
var _company_data;
|
|
85
|
+
batch.update(company.ref, companyContactRolesUpdate((_company_data = company.data()) != null ? _company_data : {}, contactId, to));
|
|
86
|
+
}
|
|
87
|
+
await batch.commit();
|
|
88
|
+
if (page.size < PAGE) break;
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`${label}: company contact roles sweep failed`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** One company's update once `contactId` is moved to `to`, or taken off for `null`. */ export function companyContactRolesUpdate(company, contactId, to) {
|
|
95
|
+
const roles = readDealContactRoles(company['contactRoles']);
|
|
96
|
+
const contactRoles = to === null ? dealContactRolesWithout(roles, contactId) : dealContactRolesRepointed(roles, contactId, to);
|
|
97
|
+
const { contactRoleContactIds, contactRoleKeys } = crmDealContactRoleListFields({
|
|
98
|
+
contactRoles
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
contactRoles,
|
|
102
|
+
contactRoleContactIds,
|
|
103
|
+
contactRoleKeys,
|
|
104
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
70
107
|
/** One deal's update once `contactId` is moved to `to`, or taken off for `null`. */ export function dealContactRolesUpdate(deal, contactId, to) {
|
|
71
108
|
var _fields_contactId, _fields_contactId1;
|
|
72
109
|
const roles = dealContactRolesOf(deal);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/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 crmListFieldsPatch,\n dealContactRoleFields,\n dealContactRolesOf,\n dealContactRolesRepointed,\n dealContactRolesWithout,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * A CONTACT LEAVING THE DEALS IT IS ON (AGL-3521): Salesforce's Opportunity\n * Contact Roles, kept in step with a contact that is merged away, deleted or\n * erased.\n *\n * A deal names its contacts twice over — every one in `contactRoles`, and\n * the Primary as `contactId` — so a deal is found by either:\n * `contactRoleContactIds` (the list field every writer stamps from the\n * roles) and `contactId` (a deal written before roles, which holds that\n * alone). Each match is rewritten through the one pure rule\n * (`dealContactRolesWithout` / `dealContactRolesRepointed`), its Primary and\n * `contactId` written together and its list fields restamped, so a written\n * deal falls out of both queries and the next pass finds only what is left.\n */\n\n/** How many deals one pass reads, and one batch writes. */\nconst PAGE = 400\n/** A ceiling on passes, so a query that keeps answering cannot loop forever. */\nconst PASSES = 25\n\n/**\n * The deals naming `contactId`, each with that contact moved to `to` —\n * a merge's survivor — or, with `to` null, taken off the deal. A Primary\n * taken off leaves the deal with none, and `contactId` with it. Answers how\n * many deals were written. A failure is logged and that query's sweep\n * stops: a deal still naming the contact reads it as a person who is not\n * there, which is how a deal named a deleted contact before roles existed.\n */\nexport async function sweepDealContactRoles(\n firestore: FirebaseFirestore.Firestore,\n deals: FirebaseFirestore.CollectionReference,\n contactId: string,\n to: string | null,\n label: string,\n): Promise<number> {\n if (!contactId || contactId === to) return 0\n let written = 0\n // Each query on its own: a deal the first could not reach may still name\n // the contact as its `contactId`.\n for (const field of ['contactRoleContactIds', 'contactId'] as const) {\n try {\n const query = deals.where(field, field === 'contactId' ? '==' : 'array-contains', contactId)\n for (let pass = 0; pass < PASSES; pass += 1) {\n const page = await query.limit(PAGE).get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const deal of page.docs) {\n batch.update(deal.ref, dealContactRolesUpdate(deal.data() ?? {}, contactId, to))\n }\n await batch.commit()\n written += page.size\n if (page.size < PAGE) break\n }\n } catch (error) {\n console.error(`${label}: deal contact roles sweep failed on ${field}`, error)\n }\n }\n return written\n}\n\n/** One deal's update once `contactId` is moved to `to`, or taken off for `null`. */\nexport function dealContactRolesUpdate(\n deal: Record<string, unknown>,\n contactId: string,\n to: string | null,\n): Record<string, unknown> {\n const roles = dealContactRolesOf(deal)\n const next: CrmDealContactRole[] =\n to === null ? dealContactRolesWithout(roles, contactId) : dealContactRolesRepointed(roles, contactId, to)\n const fields = dealContactRoleFields(next)\n const after = { ...deal, contactRoles: fields.contactRoles, contactId: fields.contactId ?? undefined }\n return {\n contactRoles: fields.contactRoles,\n contactId: fields.contactId ?? FieldValue.delete(),\n // The name the drawer copied beside the Primary goes when the Primary does.\n ...(fields.contactId !== deal['contactId'] ? { contactName: FieldValue.delete() } : {}),\n ...crmListFieldsPatch('deals', after),\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n"],"names":["crmListFieldsPatch","dealContactRoleFields","dealContactRolesOf","dealContactRolesRepointed","dealContactRolesWithout","FieldValue","PAGE","PASSES","sweepDealContactRoles","firestore","deals","contactId","to","label","written","field","query","where","pass","page","limit","get","empty","batch","deal","docs","update","ref","dealContactRolesUpdate","data","commit","size","error","console","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/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 CRM_COLLECTIONS,\n type CrmDealContactRole,\n crmDealContactRoleListFields,\n crmListFieldsPatch,\n dealContactRoleFields,\n dealContactRolesOf,\n dealContactRolesRepointed,\n dealContactRolesWithout,\n readDealContactRoles,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * A CONTACT LEAVING THE DEALS IT IS ON (AGL-3521): Salesforce's Opportunity\n * Contact Roles, kept in step with a contact that is merged away, deleted or\n * erased.\n *\n * A deal names its contacts twice over — every one in `contactRoles`, and\n * the Primary as `contactId` — so a deal is found by either:\n * `contactRoleContactIds` (the list field every writer stamps from the\n * roles) and `contactId` (a deal written before roles, which holds that\n * alone). Each match is rewritten through the one pure rule\n * (`dealContactRolesWithout` / `dealContactRolesRepointed`), its Primary and\n * `contactId` written together and its list fields restamped, so a written\n * deal falls out of both queries and the next pass finds only what is left.\n *\n * A company's Account Contact Roles (AGL-4122) follow the same person, so the\n * sweep takes them too and no caller has to remember them.\n */\n\n/** How many deals one pass reads, and one batch writes. */\nconst PAGE = 400\n/** A ceiling on passes, so a query that keeps answering cannot loop forever. */\nconst PASSES = 25\n\n/**\n * The deals naming `contactId`, each with that contact moved to `to` —\n * a merge's survivor — or, with `to` null, taken off the deal. A Primary\n * taken off leaves the deal with none, and `contactId` with it. Answers how\n * many deals were written. A failure is logged and that query's sweep\n * stops: a deal still naming the contact reads it as a person who is not\n * there, which is how a deal named a deleted contact before roles existed.\n */\nexport async function sweepDealContactRoles(\n firestore: FirebaseFirestore.Firestore,\n deals: FirebaseFirestore.CollectionReference,\n contactId: string,\n to: string | null,\n label: string,\n): Promise<number> {\n if (!contactId || contactId === to) return 0\n await sweepCompanyContactRoles(\n firestore,\n deals.parent?.collection(CRM_COLLECTIONS.companies),\n contactId,\n to,\n label,\n )\n let written = 0\n // Each query on its own: a deal the first could not reach may still name\n // the contact as its `contactId`.\n for (const field of ['contactRoleContactIds', 'contactId'] as const) {\n try {\n const query = deals.where(field, field === 'contactId' ? '==' : 'array-contains', contactId)\n for (let pass = 0; pass < PASSES; pass += 1) {\n const page = await query.limit(PAGE).get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const deal of page.docs) {\n batch.update(deal.ref, dealContactRolesUpdate(deal.data() ?? {}, contactId, to))\n }\n await batch.commit()\n written += page.size\n if (page.size < PAGE) break\n }\n } catch (error) {\n console.error(`${label}: deal contact roles sweep failed on ${field}`, error)\n }\n }\n return written\n}\n\n/** The companies naming `contactId`, moved to `to` or taken off for `null`. */\nasync function sweepCompanyContactRoles(\n firestore: FirebaseFirestore.Firestore,\n companies: FirebaseFirestore.CollectionReference | undefined,\n contactId: string,\n to: string | null,\n label: string,\n): Promise<void> {\n if (!companies) return\n try {\n const query = companies.where('contactRoleContactIds', 'array-contains', contactId)\n for (let pass = 0; pass < PASSES; pass += 1) {\n const page = await query.limit(PAGE).get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const company of page.docs) {\n batch.update(company.ref, companyContactRolesUpdate(company.data() ?? {}, contactId, to))\n }\n await batch.commit()\n if (page.size < PAGE) break\n }\n } catch (error) {\n console.error(`${label}: company contact roles sweep failed`, error)\n }\n}\n\n/** One company's update once `contactId` is moved to `to`, or taken off for `null`. */\nexport function companyContactRolesUpdate(\n company: Record<string, unknown>,\n contactId: string,\n to: string | null,\n): Record<string, unknown> {\n const roles = readDealContactRoles(company['contactRoles'])\n const contactRoles =\n to === null ? dealContactRolesWithout(roles, contactId) : dealContactRolesRepointed(roles, contactId, to)\n const { contactRoleContactIds, contactRoleKeys } = crmDealContactRoleListFields({ contactRoles })\n return {\n contactRoles,\n contactRoleContactIds,\n contactRoleKeys,\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/** One deal's update once `contactId` is moved to `to`, or taken off for `null`. */\nexport function dealContactRolesUpdate(\n deal: Record<string, unknown>,\n contactId: string,\n to: string | null,\n): Record<string, unknown> {\n const roles = dealContactRolesOf(deal)\n const next: CrmDealContactRole[] =\n to === null ? dealContactRolesWithout(roles, contactId) : dealContactRolesRepointed(roles, contactId, to)\n const fields = dealContactRoleFields(next)\n const after = { ...deal, contactRoles: fields.contactRoles, contactId: fields.contactId ?? undefined }\n return {\n contactRoles: fields.contactRoles,\n contactId: fields.contactId ?? FieldValue.delete(),\n // The name the drawer copied beside the Primary goes when the Primary does.\n ...(fields.contactId !== deal['contactId'] ? { contactName: FieldValue.delete() } : {}),\n ...crmListFieldsPatch('deals', after),\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n"],"names":["CRM_COLLECTIONS","crmDealContactRoleListFields","crmListFieldsPatch","dealContactRoleFields","dealContactRolesOf","dealContactRolesRepointed","dealContactRolesWithout","readDealContactRoles","FieldValue","PAGE","PASSES","sweepDealContactRoles","firestore","deals","contactId","to","label","sweepCompanyContactRoles","parent","collection","companies","written","field","query","where","pass","page","limit","get","empty","batch","deal","docs","update","ref","dealContactRolesUpdate","data","commit","size","error","console","company","companyContactRolesUpdate","roles","contactRoles","contactRoleContactIds","contactRoleKeys","updatedAt","serverTimestamp","fields","next","after","undefined","delete","contactName"],"mappings":"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"}
|