@aglyn/plugins-crm 1.0.0-beta.284 → 1.0.0-beta.287
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/contact-associations-card.js +5 -7
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-list-columns.js +5 -7
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/erase-person-action.d.ts +6 -4
- package/src/lib/components/erase-person-action.js +10 -8
- package/src/lib/components/erase-person-action.js.map +1 -1
- package/src/lib/components/reports/contacts-mix-card.js +4 -6
- package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +2 -4
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/use-can-erase-people.d.ts +32 -0
- package/src/lib/components/use-can-erase-people.js +56 -0
- package/src/lib/components/use-can-erase-people.js.map +1 -0
- package/src/lib/model/record-facts-disclosed.js +4 -6
- package/src/lib/model/record-facts-disclosed.js.map +1 -1
- package/src/lib/model/record-facts.js +3 -5
- package/src/lib/model/record-facts.js.map +1 -1
- package/src/lib/server/capture-contact.d.ts +5 -5
- package/src/lib/server/capture-contact.js +7 -6
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/erase-person.d.ts +10 -7
- package/src/lib/server/erase-person.js +18 -13
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/org-caller.d.ts +16 -4
- package/src/lib/server/org-caller.js +20 -4
- package/src/lib/server/org-caller.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/erase-person-action.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 PERSON_ERASURE_NOT_REACHED,\n PERSON_ERASURE_REMOVES,\n PERSON_ERASURE_RETAINS,\n personErasureConfirmationMatches,\n} from '@aglyn/aglyn'\nimport { mdiAccountOffOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n List,\n ListItem,\n ListItemText,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { type ReactNode, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCanManageCrmSettings } from './settings-section'\nimport { useCrmApi } from './use-crm-api'\n\n/** The record the action is offered on: which kind, its id, and its address. */\nexport type ErasePersonSubject =\n | { kind: 'contact'; id: string; email: string }\n | { kind: 'lead'; id: string; email: string }\n\nexport interface UseErasePersonActionProps {\n /**\n * The site the request is filed from — the route logs it on that site's\n * feed. At the organization level the record's own capturing site\n * (AGL-2630), or `null` for a record no site has captured: beneath the\n * org hub's mount the route's org variant files it anyway (AGL-2634), and\n * only a surface mounted nowhere holds the item.\n */\n hostId: string | null\n orgId: string | null | undefined\n /** Null while the record has not loaded; the item is then disabled. */\n subject: ErasePersonSubject | null\n /** The record's marker, when a request is already waiting. */\n requestedAtMs: number | null\n}\n\nexport interface ErasePersonAction {\n /** Zero or one item, to spread into the record header's overflow menu. */\n menuItems: RowActionsMenuItem[]\n /** The pending banner, or null. Render it where the record's cards begin. */\n banner: ReactNode\n /** The dialog. Render it once, anywhere in the page. */\n dialog: ReactNode\n /**\n * When the waiting request was filed, or null while none waits: the\n * record's marker, or the route's answer until the listener carries the\n * marker. What a page gates its other acts on — Convert, on a lead — so\n * they close the moment the item does.\n */\n pendingSinceMs: number | null\n}\n\n/**\n * **Erase this person** on a record page (AGL-2623).\n *\n * Owned by a hook rather than a component because the overflow menu takes\n * items as data: the page spreads `menuItems` into its own list, renders\n * `banner` under the header and `dialog` at the end, and everything the\n * action knows — who may, whether a request is already waiting, what the\n * dialog says, what the route answers — lives here.\n *\n * Offered to workspace admins and owners only, and the item is present but\n * disabled for everyone else with the reason as its tooltip: an absent item\n * and an inapplicable one look alike, and a site editor who cannot find\n * the action should learn who can rather than conclude it does not exist.\n *\n * Takes the site from the page and the workspace from `useOrgDataScope`,\n * never a route, so an org-level mount can offer the same item.\n */\nexport function useErasePersonAction(props: UseErasePersonActionProps): ErasePersonAction {\n const { hostId, orgId, subject, requestedAtMs } = props\n const { canManage, ready } = useCanManageCrmSettings(orgId ?? undefined)\n const orgMount = useCrmOrgMount()\n // A site to file from, or the org to file as (AGL-2634).\n const canFile = Boolean(hostId) || Boolean(orgMount)\n const [open, setOpen] = useState(false)\n // The marker arrives on the record's own document, so a request filed on\n // this page shows as pending the moment the listener catches up — and,\n // until it does, the moment the route answered.\n const [filedAtMs, setFiledAtMs] = useState<number | null>(null)\n const pendingSince = requestedAtMs ?? filedAtMs\n\n const menuItems = useMemo<RowActionsMenuItem[]>(() => {\n const disabledReason = !subject\n ? 'The record has not loaded'\n : !canFile\n ? 'No site has captured this person to file from'\n : pendingSince\n ? 'An erasure is already pending for this person'\n : !ready\n ? 'Checking your workspace role'\n : !canManage\n ? 'Only a workspace admin can erase a person'\n : undefined\n return [\n {\n key: 'erase-person',\n label: 'Erase this person',\n icon: <MdiIcon path={mdiAccountOffOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(disabledReason),\n disabledReason,\n onClick: () => setOpen(true),\n },\n ]\n }, [canManage, canFile, pendingSince, ready, subject])\n\n const banner = pendingSince ? <ErasurePendingBanner requestedAtMs={pendingSince} /> : null\n const dialog = subject && canFile ? (\n <ErasePersonDialog\n open={open}\n onClose={() => setOpen(false)}\n hostId={hostId}\n subject={subject}\n onFiled={(atMs) => setFiledAtMs(atMs)}\n />\n ) : null\n\n return { menuItems, banner, dialog, pendingSinceMs: pendingSince }\n}\n\nexport interface ErasurePendingBannerProps {\n requestedAtMs: number\n}\n\n/**\n * The state a record wears between the request and the run. Says when it\n * was asked and what will happen, so a reader arriving cold neither files\n * it again nor keeps working the record.\n */\nexport function ErasurePendingBanner(props: ErasurePendingBannerProps) {\n const { requestedAtMs } = props\n return (\n <Alert severity=\"warning\" data-testid=\"erasure-pending-banner\">\n <strong>{'Erasure pending. '}</strong>\n {`Requested ${new Date(requestedAtMs).toLocaleString()}. The nightly erasure ` +\n 'job removes this person from every site in the workspace; until then the ' +\n 'record is kept only so the request can be seen. Nothing new should be added ' +\n 'to it.'}\n </Alert>\n )\n}\nErasurePendingBanner.displayName = 'ErasurePendingBanner'\n\nexport interface ErasePersonDialogProps {\n open: boolean\n onClose: () => void\n /** The site the request is filed from, or `null` beneath the org hub's mount. */\n hostId: string | null\n subject: ErasePersonSubject\n /** Called with the request's queue time once the route has filed it. */\n onFiled: (pendingSinceMs: number) => void\n}\n\n/**\n * The confirmation: everything the request removes, everything it keeps\n * and how, what it does not reach, and the two facts that follow the click\n * — the address is closed to capture at once, and the sweep runs with the\n * nightly job. The admin types the address back; the route checks the same\n * match before it files anything.\n */\nexport function ErasePersonDialog(props: ErasePersonDialogProps) {\n const { open, onClose, hostId, subject, onFiled } = props\n const call = useCrmApi(hostId)\n const { enqueueSnackbar } = useSnackbar()\n const [typed, setTyped] = useState('')\n const [busy, setBusy] = useState(false)\n useEffect(() => {\n if (open) setTyped('')\n }, [open])\n const confirmed = personErasureConfirmationMatches(typed, subject.email)\n\n const submit = async () => {\n if (!confirmed || busy) return\n setBusy(true)\n try {\n const { response, payload } = await call('erase-person', {\n ...(subject.kind === 'contact' ? { contactId: subject.id } : { leadId: subject.id }),\n email: typed.trim(),\n })\n if (!response.ok) {\n throw new Error(String(payload['error'] ?? 'The erasure could not be filed'))\n }\n onFiled(Number(payload['pendingSinceMs'] ?? Date.now()))\n enqueueSnackbar(\n payload['alreadyPending']\n ? 'An erasure was already pending for this person'\n : 'Erasure requested — it runs with the nightly job',\n { variant: 'success', persist: false },\n )\n onClose()\n } catch (error) {\n enqueueSnackbar(error instanceof Error ? error.message : 'The erasure could not be filed', {\n variant: 'error',\n })\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Erase ${subject.email} from this workspace?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'This is a privacy erasure, not a delete: the person is removed from every ' +\n 'site in the workspace, whoever captured them, and it cannot be undone. ' +\n 'The request is filed now and runs with the nightly erasure job; from this ' +\n 'moment the address is closed, so a later form fill or order cannot ' +\n 'recreate the record, and no campaign is sent to it.'}\n </DialogContentText>\n <Stack spacing={2}>\n <ListBlock heading=\"Removed across the workspace\" lines={PERSON_ERASURE_REMOVES} />\n <ListBlock heading=\"Kept, with the person taken off\" lines={PERSON_ERASURE_RETAINS} />\n <ListBlock heading=\"Not reached — finish these by hand\" lines={PERSON_ERASURE_NOT_REACHED} />\n <TextField\n label=\"Type the email address to confirm\"\n value={typed}\n onChange={(event) => setTyped(event.target.value)}\n fullWidth\n autoFocus\n autoComplete=\"off\"\n slotProps={{ htmlInput: { 'aria-label': 'Type the email address to confirm' } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"error\"\n onClick={() => void submit()}\n disabled={!confirmed || busy}\n >\n {busy ? 'Filing…' : 'Erase permanently'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nErasePersonDialog.displayName = 'ErasePersonDialog'\n\nfunction ListBlock(props: { heading: string; lines: readonly string[] }) {\n return (\n <div>\n <Typography variant=\"subtitle2\">{props.heading}</Typography>\n <List dense disablePadding>\n {props.lines.map((line) => (\n <ListItem key={line} disableGutters sx={{ py: 0 }}>\n <ListItemText\n primary={line}\n slotProps={{ primary: { variant: 'body2', color: 'text.secondary' } }}\n />\n </ListItem>\n ))}\n </List>\n </div>\n )\n}\n"],"names":["PERSON_ERASURE_NOT_REACHED","PERSON_ERASURE_REMOVES","PERSON_ERASURE_RETAINS","personErasureConfirmationMatches","mdiAccountOffOutline","MdiIcon","useSnackbar","Alert","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","List","ListItem","ListItemText","Stack","TextField","Typography","useEffect","useMemo","useState","useCrmOrgMount","useCanManageCrmSettings","useCrmApi","useErasePersonAction","props","hostId","orgId","subject","requestedAtMs","canManage","ready","undefined","orgMount","canFile","Boolean","open","setOpen","filedAtMs","setFiledAtMs","pendingSince","menuItems","disabledReason","key","label","icon","path","size","destructive","disabled","onClick","banner","ErasurePendingBanner","dialog","ErasePersonDialog","onClose","onFiled","atMs","pendingSinceMs","severity","data-testid","strong","Date","toLocaleString","displayName","call","enqueueSnackbar","typed","setTyped","busy","setBusy","confirmed","email","submit","payload","response","kind","contactId","id","leadId","trim","ok","Error","String","Number","now","variant","persist","error","message","maxWidth","fullWidth","sx","mb","spacing","ListBlock","heading","lines","value","onChange","event","target","autoFocus","autoComplete","slotProps","htmlInput","color","div","dense","disablePadding","map","line","disableGutters","py","primary"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/erase-person-action.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 PERSON_ERASURE_NOT_REACHED,\n PERSON_ERASURE_REMOVES,\n PERSON_ERASURE_RETAINS,\n personErasureConfirmationMatches,\n} from '@aglyn/aglyn'\nimport { mdiAccountOffOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n List,\n ListItem,\n ListItemText,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { type ReactNode, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCanErasePeople } from './use-can-erase-people'\nimport { useCrmApi } from './use-crm-api'\n\n/** The record the action is offered on: which kind, its id, and its address. */\nexport type ErasePersonSubject =\n | { kind: 'contact'; id: string; email: string }\n | { kind: 'lead'; id: string; email: string }\n\nexport interface UseErasePersonActionProps {\n /**\n * The site the request is filed from — the route logs it on that site's\n * feed. At the organization level the record's own capturing site\n * (AGL-2630), or `null` for a record no site has captured: beneath the\n * org hub's mount the route's org variant files it anyway (AGL-2634), and\n * only a surface mounted nowhere holds the item.\n */\n hostId: string | null\n orgId: string | null | undefined\n /** Null while the record has not loaded; the item is then disabled. */\n subject: ErasePersonSubject | null\n /** The record's marker, when a request is already waiting. */\n requestedAtMs: number | null\n}\n\nexport interface ErasePersonAction {\n /** Zero or one item, to spread into the record header's overflow menu. */\n menuItems: RowActionsMenuItem[]\n /** The pending banner, or null. Render it where the record's cards begin. */\n banner: ReactNode\n /** The dialog. Render it once, anywhere in the page. */\n dialog: ReactNode\n /**\n * When the waiting request was filed, or null while none waits: the\n * record's marker, or the route's answer until the listener carries the\n * marker. What a page gates its other acts on — Convert, on a lead — so\n * they close the moment the item does.\n */\n pendingSinceMs: number | null\n}\n\n/**\n * **Erase this person** on a record page (AGL-2623).\n *\n * Owned by a hook rather than a component because the overflow menu takes\n * items as data: the page spreads `menuItems` into its own list, renders\n * `banner` under the header and `dialog` at the end, and everything the\n * action knows — who may, whether a request is already waiting, what the\n * dialog says, what the route answers — lives here.\n *\n * Offered to whoever holds \"Privacy requests\" (`org.privacy`, AGL-4047) over\n * the whole workspace — owners and admins by default — and the item is\n * present but disabled for everyone else with the reason as its tooltip: an\n * absent item and an inapplicable one look alike, and a site editor who\n * cannot find the action should learn who can rather than conclude it does\n * not exist.\n *\n * Takes the site from the page and the workspace from `useOrgDataScope`,\n * never a route, so an org-level mount can offer the same item.\n */\nexport function useErasePersonAction(props: UseErasePersonActionProps): ErasePersonAction {\n const { hostId, orgId, subject, requestedAtMs } = props\n const { canErase, ready } = useCanErasePeople(orgId ?? undefined)\n const orgMount = useCrmOrgMount()\n // A site to file from, or the org to file as (AGL-2634).\n const canFile = Boolean(hostId) || Boolean(orgMount)\n const [open, setOpen] = useState(false)\n // The marker arrives on the record's own document, so a request filed on\n // this page shows as pending the moment the listener catches up — and,\n // until it does, the moment the route answered.\n const [filedAtMs, setFiledAtMs] = useState<number | null>(null)\n const pendingSince = requestedAtMs ?? filedAtMs\n\n const menuItems = useMemo<RowActionsMenuItem[]>(() => {\n const disabledReason = !subject\n ? 'The record has not loaded'\n : !canFile\n ? 'No site has captured this person to file from'\n : pendingSince\n ? 'An erasure is already pending for this person'\n : !ready\n ? 'Checking your workspace role'\n : !canErase\n ? 'Your role does not include \"Privacy requests\" — ask an organization admin'\n : undefined\n return [\n {\n key: 'erase-person',\n label: 'Erase this person',\n icon: <MdiIcon path={mdiAccountOffOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(disabledReason),\n disabledReason,\n onClick: () => setOpen(true),\n },\n ]\n }, [canErase, canFile, pendingSince, ready, subject])\n\n const banner = pendingSince ? <ErasurePendingBanner requestedAtMs={pendingSince} /> : null\n const dialog = subject && canFile ? (\n <ErasePersonDialog\n open={open}\n onClose={() => setOpen(false)}\n hostId={hostId}\n subject={subject}\n onFiled={(atMs) => setFiledAtMs(atMs)}\n />\n ) : null\n\n return { menuItems, banner, dialog, pendingSinceMs: pendingSince }\n}\n\nexport interface ErasurePendingBannerProps {\n requestedAtMs: number\n}\n\n/**\n * The state a record wears between the request and the run. Says when it\n * was asked and what will happen, so a reader arriving cold neither files\n * it again nor keeps working the record.\n */\nexport function ErasurePendingBanner(props: ErasurePendingBannerProps) {\n const { requestedAtMs } = props\n return (\n <Alert severity=\"warning\" data-testid=\"erasure-pending-banner\">\n <strong>{'Erasure pending. '}</strong>\n {`Requested ${new Date(requestedAtMs).toLocaleString()}. The nightly erasure ` +\n 'job removes this person from every site in the workspace; until then the ' +\n 'record is kept only so the request can be seen. Nothing new should be added ' +\n 'to it.'}\n </Alert>\n )\n}\nErasurePendingBanner.displayName = 'ErasurePendingBanner'\n\nexport interface ErasePersonDialogProps {\n open: boolean\n onClose: () => void\n /** The site the request is filed from, or `null` beneath the org hub's mount. */\n hostId: string | null\n subject: ErasePersonSubject\n /** Called with the request's queue time once the route has filed it. */\n onFiled: (pendingSinceMs: number) => void\n}\n\n/**\n * The confirmation: everything the request removes, everything it keeps\n * and how, what it does not reach, and the two facts that follow the click\n * — the address is closed to capture at once, and the sweep runs with the\n * nightly job. The admin types the address back; the route checks the same\n * match before it files anything.\n */\nexport function ErasePersonDialog(props: ErasePersonDialogProps) {\n const { open, onClose, hostId, subject, onFiled } = props\n const call = useCrmApi(hostId)\n const { enqueueSnackbar } = useSnackbar()\n const [typed, setTyped] = useState('')\n const [busy, setBusy] = useState(false)\n useEffect(() => {\n if (open) setTyped('')\n }, [open])\n const confirmed = personErasureConfirmationMatches(typed, subject.email)\n\n const submit = async () => {\n if (!confirmed || busy) return\n setBusy(true)\n try {\n const { response, payload } = await call('erase-person', {\n ...(subject.kind === 'contact' ? { contactId: subject.id } : { leadId: subject.id }),\n email: typed.trim(),\n })\n if (!response.ok) {\n throw new Error(String(payload['error'] ?? 'The erasure could not be filed'))\n }\n onFiled(Number(payload['pendingSinceMs'] ?? Date.now()))\n enqueueSnackbar(\n payload['alreadyPending']\n ? 'An erasure was already pending for this person'\n : 'Erasure requested — it runs with the nightly job',\n { variant: 'success', persist: false },\n )\n onClose()\n } catch (error) {\n enqueueSnackbar(error instanceof Error ? error.message : 'The erasure could not be filed', {\n variant: 'error',\n })\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Erase ${subject.email} from this workspace?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'This is a privacy erasure, not a delete: the person is removed from every ' +\n 'site in the workspace, whoever captured them, and it cannot be undone. ' +\n 'The request is filed now and runs with the nightly erasure job; from this ' +\n 'moment the address is closed, so a later form fill or order cannot ' +\n 'recreate the record, and no campaign is sent to it.'}\n </DialogContentText>\n <Stack spacing={2}>\n <ListBlock heading=\"Removed across the workspace\" lines={PERSON_ERASURE_REMOVES} />\n <ListBlock heading=\"Kept, with the person taken off\" lines={PERSON_ERASURE_RETAINS} />\n <ListBlock heading=\"Not reached — finish these by hand\" lines={PERSON_ERASURE_NOT_REACHED} />\n <TextField\n label=\"Type the email address to confirm\"\n value={typed}\n onChange={(event) => setTyped(event.target.value)}\n fullWidth\n autoFocus\n autoComplete=\"off\"\n slotProps={{ htmlInput: { 'aria-label': 'Type the email address to confirm' } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"error\"\n onClick={() => void submit()}\n disabled={!confirmed || busy}\n >\n {busy ? 'Filing…' : 'Erase permanently'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nErasePersonDialog.displayName = 'ErasePersonDialog'\n\nfunction ListBlock(props: { heading: string; lines: readonly string[] }) {\n return (\n <div>\n <Typography variant=\"subtitle2\">{props.heading}</Typography>\n <List dense disablePadding>\n {props.lines.map((line) => (\n <ListItem key={line} disableGutters sx={{ py: 0 }}>\n <ListItemText\n primary={line}\n slotProps={{ primary: { variant: 'body2', color: 'text.secondary' } }}\n />\n </ListItem>\n ))}\n </List>\n </div>\n )\n}\n"],"names":["PERSON_ERASURE_NOT_REACHED","PERSON_ERASURE_REMOVES","PERSON_ERASURE_RETAINS","personErasureConfirmationMatches","mdiAccountOffOutline","MdiIcon","useSnackbar","Alert","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","List","ListItem","ListItemText","Stack","TextField","Typography","useEffect","useMemo","useState","useCrmOrgMount","useCanErasePeople","useCrmApi","useErasePersonAction","props","hostId","orgId","subject","requestedAtMs","canErase","ready","undefined","orgMount","canFile","Boolean","open","setOpen","filedAtMs","setFiledAtMs","pendingSince","menuItems","disabledReason","key","label","icon","path","size","destructive","disabled","onClick","banner","ErasurePendingBanner","dialog","ErasePersonDialog","onClose","onFiled","atMs","pendingSinceMs","severity","data-testid","strong","Date","toLocaleString","displayName","call","enqueueSnackbar","typed","setTyped","busy","setBusy","confirmed","email","submit","payload","response","kind","contactId","id","leadId","trim","ok","Error","String","Number","now","variant","persist","error","message","maxWidth","fullWidth","sx","mb","spacing","ListBlock","heading","lines","value","onChange","event","target","autoFocus","autoComplete","slotProps","htmlInput","color","div","dense","disablePadding","map","line","disableGutters","py","primary"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
18
|
import * as Aglyn from "@aglyn/aglyn";
|
|
19
|
+
import { labelContactSource } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
|
|
19
20
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
20
21
|
import { percent, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
21
22
|
import { Alert, Stack, Typography } from "@mui/material";
|
|
@@ -116,14 +117,11 @@ import { funnelFromStages, tally } from "../../model/crm-reports.js";
|
|
|
116
117
|
title: 'By source',
|
|
117
118
|
children: [
|
|
118
119
|
/*#__PURE__*/ _jsx(ReportBreakdown, {
|
|
119
|
-
rows: mix.sources.map((row)=>{
|
|
120
|
-
var _Aglyn_CONTACT_SOURCE_LABELS_row_key;
|
|
121
|
-
return {
|
|
120
|
+
rows: mix.sources.map((row)=>({
|
|
122
121
|
key: row.key,
|
|
123
|
-
label: (
|
|
122
|
+
label: labelContactSource(row.key),
|
|
124
123
|
value: row.count
|
|
125
|
-
}
|
|
126
|
-
}),
|
|
124
|
+
})),
|
|
127
125
|
emptyText: status === 'loading' ? 'Reading…' : 'No contacts to count yet.'
|
|
128
126
|
}),
|
|
129
127
|
mix.sources.length ? /*#__PURE__*/ _jsx(Typography, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-mix-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 * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { limit, orderBy, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { contactPrimaryGroup } from '../../model/contact-record'\nimport { ReportBreakdown } from './report-breakdown'\nimport { plural } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { type AggregateRead, useWindowRead } from './use-aggregate-read'\nimport { funnelFromStages, tally } from '../../model/crm-reports'\n\n/**\n * How many contacts the source mix and the funnel are read from.\n *\n * These two charts need a FIELD off each contact — which facet sources it\n * carries, which lifecycle stage — and Firestore aggregates cannot group by\n * a map key, so the only way to draw them is to read documents. A thousand\n * is the same bound the contacts list carries, and past it the card SAYS it\n * is a sample rather than quietly drawing a thousand as if it were the org.\n */\nconst CONTACT_SAMPLE_CEILING = 1000\n\nexport interface ContactsMixCardProps {\n report: CrmReportScope\n /** The section's count of every contact this reader may see — what decides whether the read is a sample. */\n totalContacts: AggregateRead<number>\n}\n\n/**\n * Contacts by source, and the lifecycle funnel, from the newest thousand\n * contacts (AGL-2604).\n *\n * Both are read through THIS group's facet, the way every other field on\n * the contacts page is: a source is which capture surface this holder saw\n * the person through, and a stage is where this business's sales team put\n * them. Reading either off the top of the document would chart another\n * holder's records — the disclosure the facet shape exists to end.\n *\n * Newest by `createdAt` rather than by `updatedAt`, so that the sample is\n * \"the last thousand people captured\" — a population a reader can reason\n * about — and not \"the thousand most recently edited\", which is whichever\n * ones a bulk tag touched last.\n */\nexport function ContactsMixCard(props: ContactsMixCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, groupId, org, nowMs } = report\n const firestore = useFirestore()\n\n const sample = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_SAMPLE_CEILING + 1),\n ),\n CONTACT_SAMPLE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'contacts:sample') },\n )\n const status = sample.status\n\n const mix = useMemo(() => {\n // Through the viewing group's facet under a site; at the organization\n // level (AGL-2630) through each person's own primary holder, so the mix\n // is of the profiles the capturing sites keep.\n const facets = sample.rows.map((row) =>\n Aglyn.readContactFacet(\n row,\n groupId ?? contactPrimaryGroup(row, org).groupId,\n ),\n )\n // A person captured two ways counts under both sources: the chart asks\n // \"how many people came through each door\", and a person who came\n // through two did.\n const sources = tally(\n facets.flatMap((facet) =>\n (Object.keys(facet.sources) as Aglyn.ContactSource[]).filter(\n (source) => facet.sources[source],\n ),\n ),\n (source) => source,\n )\n const stageCounts: Partial<Record<Aglyn.ContactLifecycleStage, number>> = {}\n let unstaged = 0\n for (const facet of facets) {\n const stage = facet.lifecycleStage\n if (Aglyn.isContactLifecycleStage(stage)) {\n stageCounts[stage] = (stageCounts[stage] ?? 0) + 1\n } else {\n unstaged += 1\n }\n }\n return { sources, funnel: funnelFromStages(stageCounts), unstaged }\n }, [sample, groupId, org])\n\n // A sample only once the read has settled: while the window is empty\n // every org \"exceeds\" zero rows.\n const sampled =\n status === 'success' &&\n (sample.truncated ||\n (totalContacts.value !== null && totalContacts.value > sample.rows.length))\n\n return (\n <CardDisplay\n header={'Sources and lifecycle'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#sources-and-lifecycle',\n excerpt:\n 'Which capture surfaces your contacts came through, and how many ' +\n 'have reached each lifecycle stage. Read from the newest thousand ' +\n 'contacts, through this site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {sampled ? (\n <Alert severity=\"info\">\n {`Read from the newest ${sample.rows.length.toLocaleString()} contacts${\n totalContacts.value !== null\n ? ` of ${totalContacts.value.toLocaleString()}`\n : ''\n } — a sample, not the whole list.`}\n </Alert>\n ) : null}\n <Section title={'By source'}>\n <ReportBreakdown\n rows={mix.sources.map((row) => ({\n key: row.key,\n label:\n Aglyn.CONTACT_SOURCE_LABELS[row.key as Aglyn.ContactSource] ??\n row.key,\n value: row.count,\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No contacts to count yet.'\n }\n />\n {mix.sources.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Lifecycle funnel'}>\n <ReportBreakdown\n rows={mix.funnel.steps.map((step) => ({\n key: step.stage,\n label: step.label,\n value: step.reached,\n note:\n `${plural(step.count, 'person', 'people')} here now` +\n (step.conversion === null\n ? ''\n : ` · ${percent(step.conversion)} of the step before`),\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No contacts to place yet.'\n }\n />\n {sample.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Each step counts everyone who reached it or went further. ${plural(\n mix.unstaged,\n 'contact',\n )} with no stage yet · ${mix.funnel.other.toLocaleString()} marked other.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsMixCard.displayName = 'ContactsMixCard'\n\nexport default ContactsMixCard\n"],"names":["Aglyn","CardDisplay","percent","Section","Alert","Stack","Typography","limit","orderBy","query","useMemo","useFirestore","contactPrimaryGroup","ReportBreakdown","plural","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","funnelFromStages","tally","CONTACT_SAMPLE_CEILING","ContactsMixCard","props","report","totalContacts","scope","tokens","groupId","org","nowMs","firestore","sample","cacheKey","status","mix","facets","rows","map","row","readContactFacet","sources","flatMap","facet","Object","keys","filter","source","stageCounts","unstaged","stage","lifecycleStage","isContactLifecycleStage","funnel","sampled","truncated","value","length","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","severity","toLocaleString","title","key","label","CONTACT_SOURCE_LABELS","count","emptyText","variant","color","steps","step","reached","note","conversion","other","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SACEC,OAAO,EACPC,OAAO,QACF,6DAA4D;AACnE,SAASC,KAAK,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACxD,SAASC,KAAK,EAAEC,OAAO,EAAEC,KAAK,QAAQ,qBAAoB;AAC1D,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,mBAAmB,QAAQ,gCAA4B;AAChE,SAASC,eAAe,QAAQ,wBAAoB;AACpD,SAASC,MAAM,QAAQ,qBAAiB;AACxC,SAEEC,cAAc,EACdC,gBAAgB,EAChBC,eAAe,QACV,oBAAgB;AACvB,SAA6BC,aAAa,QAAQ,0BAAsB;AACxE,SAASC,gBAAgB,EAAEC,KAAK,QAAQ,6BAAyB;AAEjE;;;;;;;;CAQC,GACD,MAAMC,yBAAyB;AAQ/B;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAE,GAAGF;IAClC,MAAM,EAAEG,KAAK,EAAEC,MAAM,EAAEC,OAAO,EAAEC,GAAG,EAAEC,KAAK,EAAE,GAAGN;IAC/C,MAAMO,YAAYpB;IAElB,MAAMqB,SAASd,cACb,IACET,MACEO,iBAAiBe,WAAWL,OAAO,gBAChCT,gBAAgBU,SACnBnB,QAAQ,aAAa,SACrBD,MAAMc,yBAAyB,KAEnCA,wBACA;QAACU;QAAWL;QAAOC;QAAQG;KAAM,EACjC;QAAEG,UAAUlB,eAAeS,QAAQ;IAAmB;IAExD,MAAMU,SAASF,OAAOE,MAAM;IAE5B,MAAMC,MAAMzB,QAAQ;QAClB,sEAAsE;QACtE,wEAAwE;QACxE,+CAA+C;QAC/C,MAAM0B,SAASJ,OAAOK,IAAI,CAACC,GAAG,CAAC,CAACC,MAC9BvC,MAAMwC,gBAAgB,CACpBD,KACAX,kBAAAA,UAAWhB,oBAAoB2B,KAAKV,KAAKD,OAAO;QAGpD,uEAAuE;QACvE,kEAAkE;QAClE,mBAAmB;QACnB,MAAMa,UAAUrB,MACdgB,OAAOM,OAAO,CAAC,CAACC,QACd,AAACC,OAAOC,IAAI,CAACF,MAAMF,OAAO,EAA4BK,MAAM,CAC1D,CAACC,SAAWJ,MAAMF,OAAO,CAACM,OAAO,IAGrC,CAACA,SAAWA;QAEd,MAAMC,cAAoE,CAAC;QAC3E,IAAIC,WAAW;QACf,KAAK,MAAMN,SAASP,OAAQ;YAC1B,MAAMc,QAAQP,MAAMQ,cAAc;YAClC,IAAInD,MAAMoD,uBAAuB,CAACF,QAAQ;oBAClBF;gBAAtBA,WAAW,CAACE,MAAM,GAAG,EAACF,qBAAAA,WAAW,CAACE,MAAM,YAAlBF,qBAAsB,KAAK;YACnD,OAAO;gBACLC,YAAY;YACd;QACF;QACA,OAAO;YAAER;YAASY,QAAQlC,iBAAiB6B;YAAcC;QAAS;IACpE,GAAG;QAACjB;QAAQJ;QAASC;KAAI;IAEzB,qEAAqE;IACrE,iCAAiC;IACjC,MAAMyB,UACJpB,WAAW,aACVF,CAAAA,OAAOuB,SAAS,IACd9B,cAAc+B,KAAK,KAAK,QAAQ/B,cAAc+B,KAAK,GAAGxB,OAAOK,IAAI,CAACoB,MAAM;IAE7E,qBACE,KAACxD;QACCyD,QAAQ;QACRC,MAAM3D,MAAM4D,cAAc,CAAC,cAAc;YACvCC,QAAQ;YACRC,SACE,qEACA,sEACA;QACJ;QACAC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC3D;YAAM4D,SAAS;;gBACbX,wBACC,KAAClD;oBAAM8D,UAAS;8BACb,CAAC,qBAAqB,EAAElC,OAAOK,IAAI,CAACoB,MAAM,CAACU,cAAc,GAAG,SAAS,EACpE1C,cAAc+B,KAAK,KAAK,OACpB,CAAC,IAAI,EAAE/B,cAAc+B,KAAK,CAACW,cAAc,IAAI,GAC7C,GACL,gCAAgC,CAAC;qBAElC;8BACJ,MAAChE;oBAAQiE,OAAO;;sCACd,KAACvD;4BACCwB,MAAMF,IAAIM,OAAO,CAACH,GAAG,CAAC,CAACC;oCAGnBvC;uCAH4B;oCAC9BqE,KAAK9B,IAAI8B,GAAG;oCACZC,KAAK,GACHtE,uCAAAA,MAAMuE,qBAAqB,CAAChC,IAAI8B,GAAG,CAAwB,YAA3DrE,uCACAuC,IAAI8B,GAAG;oCACTb,OAAOjB,IAAIiC,KAAK;gCAClB;;4BACAC,WACEvC,WAAW,YAAY,aAAa;;wBAGvCC,IAAIM,OAAO,CAACgB,MAAM,iBACjB,KAACnD;4BAAWoE,SAAQ;4BAAUC,OAAM;sCACjC;6BAED;;;8BAEN,MAACxE;oBAAQiE,OAAO;;sCACd,KAACvD;4BACCwB,MAAMF,IAAIkB,MAAM,CAACuB,KAAK,CAACtC,GAAG,CAAC,CAACuC,OAAU,CAAA;oCACpCR,KAAKQ,KAAK3B,KAAK;oCACfoB,OAAOO,KAAKP,KAAK;oCACjBd,OAAOqB,KAAKC,OAAO;oCACnBC,MACE,GAAGjE,OAAO+D,KAAKL,KAAK,EAAE,UAAU,UAAU,SAAS,CAAC,GACnDK,CAAAA,KAAKG,UAAU,KAAK,OACjB,KACA,CAAC,GAAG,EAAE9E,QAAQ2E,KAAKG,UAAU,EAAE,mBAAmB,CAAC,AAAD;gCAC1D,CAAA;4BACAP,WACEvC,WAAW,YAAY,aAAa;;wBAGvCF,OAAOK,IAAI,CAACoB,MAAM,iBACjB,KAACnD;4BAAWoE,SAAQ;4BAAUC,OAAM;sCACjC,CAAC,0DAA0D,EAAE7D,OAC5DqB,IAAIc,QAAQ,EACZ,WACA,qBAAqB,EAAEd,IAAIkB,MAAM,CAAC4B,KAAK,CAACd,cAAc,GAAG,cAAc,CAAC;6BAE1E;;;;;;AAKd;AACA7C,gBAAgB4D,WAAW,GAAG;AAE9B,eAAe5D,gBAAe"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-mix-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 * as Aglyn from '@aglyn/aglyn'\nimport { labelContactSource } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { limit, orderBy, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { contactPrimaryGroup } from '../../model/contact-record'\nimport { ReportBreakdown } from './report-breakdown'\nimport { plural } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { type AggregateRead, useWindowRead } from './use-aggregate-read'\nimport { funnelFromStages, tally } from '../../model/crm-reports'\n\n/**\n * How many contacts the source mix and the funnel are read from.\n *\n * These two charts need a FIELD off each contact — which facet sources it\n * carries, which lifecycle stage — and Firestore aggregates cannot group by\n * a map key, so the only way to draw them is to read documents. A thousand\n * is the same bound the contacts list carries, and past it the card SAYS it\n * is a sample rather than quietly drawing a thousand as if it were the org.\n */\nconst CONTACT_SAMPLE_CEILING = 1000\n\nexport interface ContactsMixCardProps {\n report: CrmReportScope\n /** The section's count of every contact this reader may see — what decides whether the read is a sample. */\n totalContacts: AggregateRead<number>\n}\n\n/**\n * Contacts by source, and the lifecycle funnel, from the newest thousand\n * contacts (AGL-2604).\n *\n * Both are read through THIS group's facet, the way every other field on\n * the contacts page is: a source is which capture surface this holder saw\n * the person through, and a stage is where this business's sales team put\n * them. Reading either off the top of the document would chart another\n * holder's records — the disclosure the facet shape exists to end.\n *\n * Newest by `createdAt` rather than by `updatedAt`, so that the sample is\n * \"the last thousand people captured\" — a population a reader can reason\n * about — and not \"the thousand most recently edited\", which is whichever\n * ones a bulk tag touched last.\n */\nexport function ContactsMixCard(props: ContactsMixCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, groupId, org, nowMs } = report\n const firestore = useFirestore()\n\n const sample = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_SAMPLE_CEILING + 1),\n ),\n CONTACT_SAMPLE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'contacts:sample') },\n )\n const status = sample.status\n\n const mix = useMemo(() => {\n // Through the viewing group's facet under a site; at the organization\n // level (AGL-2630) through each person's own primary holder, so the mix\n // is of the profiles the capturing sites keep.\n const facets = sample.rows.map((row) =>\n Aglyn.readContactFacet(\n row,\n groupId ?? contactPrimaryGroup(row, org).groupId,\n ),\n )\n // A person captured two ways counts under both sources: the chart asks\n // \"how many people came through each door\", and a person who came\n // through two did.\n const sources = tally(\n facets.flatMap((facet) =>\n (Object.keys(facet.sources) as Aglyn.ContactSource[]).filter(\n (source) => facet.sources[source],\n ),\n ),\n (source) => source,\n )\n const stageCounts: Partial<Record<Aglyn.ContactLifecycleStage, number>> = {}\n let unstaged = 0\n for (const facet of facets) {\n const stage = facet.lifecycleStage\n if (Aglyn.isContactLifecycleStage(stage)) {\n stageCounts[stage] = (stageCounts[stage] ?? 0) + 1\n } else {\n unstaged += 1\n }\n }\n return { sources, funnel: funnelFromStages(stageCounts), unstaged }\n }, [sample, groupId, org])\n\n // A sample only once the read has settled: while the window is empty\n // every org \"exceeds\" zero rows.\n const sampled =\n status === 'success' &&\n (sample.truncated ||\n (totalContacts.value !== null && totalContacts.value > sample.rows.length))\n\n return (\n <CardDisplay\n header={'Sources and lifecycle'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#sources-and-lifecycle',\n excerpt:\n 'Which capture surfaces your contacts came through, and how many ' +\n 'have reached each lifecycle stage. Read from the newest thousand ' +\n 'contacts, through this site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {sampled ? (\n <Alert severity=\"info\">\n {`Read from the newest ${sample.rows.length.toLocaleString()} contacts${\n totalContacts.value !== null\n ? ` of ${totalContacts.value.toLocaleString()}`\n : ''\n } — a sample, not the whole list.`}\n </Alert>\n ) : null}\n <Section title={'By source'}>\n <ReportBreakdown\n rows={mix.sources.map((row) => ({\n key: row.key,\n label: labelContactSource(row.key),\n value: row.count,\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No contacts to count yet.'\n }\n />\n {mix.sources.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Lifecycle funnel'}>\n <ReportBreakdown\n rows={mix.funnel.steps.map((step) => ({\n key: step.stage,\n label: step.label,\n value: step.reached,\n note:\n `${plural(step.count, 'person', 'people')} here now` +\n (step.conversion === null\n ? ''\n : ` · ${percent(step.conversion)} of the step before`),\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No contacts to place yet.'\n }\n />\n {sample.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Each step counts everyone who reached it or went further. ${plural(\n mix.unstaged,\n 'contact',\n )} with no stage yet · ${mix.funnel.other.toLocaleString()} marked other.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsMixCard.displayName = 'ContactsMixCard'\n\nexport default ContactsMixCard\n"],"names":["Aglyn","labelContactSource","CardDisplay","percent","Section","Alert","Stack","Typography","limit","orderBy","query","useMemo","useFirestore","contactPrimaryGroup","ReportBreakdown","plural","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","funnelFromStages","tally","CONTACT_SAMPLE_CEILING","ContactsMixCard","props","report","totalContacts","scope","tokens","groupId","org","nowMs","firestore","sample","cacheKey","status","mix","facets","rows","map","row","readContactFacet","sources","flatMap","facet","Object","keys","filter","source","stageCounts","unstaged","stage","lifecycleStage","isContactLifecycleStage","funnel","sampled","truncated","value","length","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","severity","toLocaleString","title","key","label","count","emptyText","variant","color","steps","step","reached","note","conversion","other","displayName"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
18
|
import * as Aglyn from "@aglyn/aglyn";
|
|
19
|
+
import { labelContactSource } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
|
|
19
20
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
20
21
|
import { measuredRate, percent, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
21
22
|
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
@@ -90,10 +91,7 @@ import { conversionBySource } from "../../model/crm-reports.js";
|
|
|
90
91
|
]);
|
|
91
92
|
const read = status === 'success';
|
|
92
93
|
const rate = read ? measuredRate(conversion.customers, conversion.total, 'captured') : null;
|
|
93
|
-
const labelOf = (source)=>
|
|
94
|
-
var _Aglyn_CONTACT_SOURCE_LABELS_source;
|
|
95
|
-
return (_Aglyn_CONTACT_SOURCE_LABELS_source = Aglyn.CONTACT_SOURCE_LABELS[source]) != null ? _Aglyn_CONTACT_SOURCE_LABELS_source : source;
|
|
96
|
-
};
|
|
94
|
+
const labelOf = (source)=>labelContactSource(source);
|
|
97
95
|
const sampled = read && (window.truncated || captured.value !== null && captured.value > window.rows.length);
|
|
98
96
|
const caption = sampled ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` + (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') + ' in the period; the captured tile is counted on the server.' : undefined;
|
|
99
97
|
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-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 * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { conversionBySource } from '../../model/crm-reports'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","conversionBySource","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","useFlexGap","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-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 * as Aglyn from '@aglyn/aglyn'\nimport { labelContactSource } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { conversionBySource } from '../../model/crm-reports'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n labelContactSource(source)\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","labelContactSource","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","conversionBySource","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","useFlexGap","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Whether the signed-in member may file a privacy erasure, and whether that
|
|
19
|
+
* is known yet (AGL-4047): "Privacy requests" (`org.privacy`) over the whole
|
|
20
|
+
* workspace, the key `/api/crm/erase-person` asks. On for owners and admins
|
|
21
|
+
* by default; a custom role or an override can hand it to another org-wide
|
|
22
|
+
* member or take it from an admin. A site collaborator never files one.
|
|
23
|
+
*
|
|
24
|
+
* Read off the member's own membership document and their custom role when
|
|
25
|
+
* they hold one, both of which the rules let a member read, the way the
|
|
26
|
+
* consent-group cards answer their own key. `ready` separates "no" from "not
|
|
27
|
+
* yet", so the item disables with a reason instead of vanishing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useCanErasePeople(orgId: string | undefined): {
|
|
30
|
+
canErase: boolean;
|
|
31
|
+
ready: boolean;
|
|
32
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { isOrgWideMember, resolveOrgPermissions } from "@aglyn/aglyn";
|
|
18
|
+
import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
|
|
19
|
+
import { doc } from "firebase/firestore";
|
|
20
|
+
/**
|
|
21
|
+
* Whether the signed-in member may file a privacy erasure, and whether that
|
|
22
|
+
* is known yet (AGL-4047): "Privacy requests" (`org.privacy`) over the whole
|
|
23
|
+
* workspace, the key `/api/crm/erase-person` asks. On for owners and admins
|
|
24
|
+
* by default; a custom role or an override can hand it to another org-wide
|
|
25
|
+
* member or take it from an admin. A site collaborator never files one.
|
|
26
|
+
*
|
|
27
|
+
* Read off the member's own membership document and their custom role when
|
|
28
|
+
* they hold one, both of which the rules let a member read, the way the
|
|
29
|
+
* consent-group cards answer their own key. `ready` separates "no" from "not
|
|
30
|
+
* yet", so the item disables with a reason instead of vanishing.
|
|
31
|
+
*/ export function useCanErasePeople(orgId) {
|
|
32
|
+
var _ref, _roleRead_data;
|
|
33
|
+
const firestore = useFirestore();
|
|
34
|
+
const { data: user } = useUser();
|
|
35
|
+
const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : '';
|
|
36
|
+
const memberRead = useFirestoreDoc(()=>orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null, [
|
|
37
|
+
firestore,
|
|
38
|
+
orgId,
|
|
39
|
+
uid
|
|
40
|
+
]);
|
|
41
|
+
const member = memberRead.status === 'success' ? memberRead.data : undefined;
|
|
42
|
+
const roleId = typeof (member == null ? void 0 : member.roleId) === 'string' ? member.roleId : '';
|
|
43
|
+
const roleRead = useFirestoreDoc(()=>orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null, [
|
|
44
|
+
firestore,
|
|
45
|
+
orgId,
|
|
46
|
+
roleId
|
|
47
|
+
]);
|
|
48
|
+
const ready = Boolean(orgId && uid) && memberRead.status !== 'loading' && (!roleId || roleRead.status !== 'loading');
|
|
49
|
+
const granted = resolveOrgPermissions(member != null ? member : null, roleId && roleRead.status === 'success' ? (_roleRead_data = roleRead.data) != null ? _roleRead_data : null : null);
|
|
50
|
+
return {
|
|
51
|
+
canErase: Boolean(member) && isOrgWideMember(member) && granted['org.privacy'] === true,
|
|
52
|
+
ready
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=use-can-erase-people.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/use-can-erase-people.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'use client'\n\nimport { type AglynOrgCustomRole, isOrgWideMember, resolveOrgPermissions } from '@aglyn/aglyn'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport { doc } from 'firebase/firestore'\n\n/** A membership document, as the permission resolver reads one. */\ntype MemberRecord = NonNullable<Parameters<typeof resolveOrgPermissions>[0]>\n\n/**\n * Whether the signed-in member may file a privacy erasure, and whether that\n * is known yet (AGL-4047): \"Privacy requests\" (`org.privacy`) over the whole\n * workspace, the key `/api/crm/erase-person` asks. On for owners and admins\n * by default; a custom role or an override can hand it to another org-wide\n * member or take it from an admin. A site collaborator never files one.\n *\n * Read off the member's own membership document and their custom role when\n * they hold one, both of which the rules let a member read, the way the\n * consent-group cards answer their own key. `ready` separates \"no\" from \"not\n * yet\", so the item disables with a reason instead of vanishing.\n */\nexport function useCanErasePeople(orgId: string | undefined): { canErase: boolean; ready: boolean } {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const memberRead = useFirestoreDoc<MemberRecord>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n const member = memberRead.status === 'success' ? memberRead.data : undefined\n const roleId = typeof member?.roleId === 'string' ? member.roleId : ''\n const roleRead = useFirestoreDoc<AglynOrgCustomRole>(\n () => (orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null),\n [firestore, orgId, roleId],\n )\n const ready =\n Boolean(orgId && uid) && memberRead.status !== 'loading' && (!roleId || roleRead.status !== 'loading')\n const granted = resolveOrgPermissions(\n member ?? null,\n roleId && roleRead.status === 'success' ? (roleRead.data ?? null) : null,\n )\n return { canErase: Boolean(member) && isOrgWideMember(member) && granted['org.privacy'] === true, ready }\n}\n"],"names":["isOrgWideMember","resolveOrgPermissions","useFirestore","useFirestoreDoc","useUser","doc","useCanErasePeople","orgId","roleRead","firestore","data","user","uid","memberRead","member","status","undefined","roleId","ready","Boolean","granted","canErase"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;AAEA,SAAkCA,eAAe,EAAEC,qBAAqB,QAAQ,eAAc;AAC9F,SAASC,YAAY,EAAEC,eAAe,EAAEC,OAAO,QAAQ,iCAAgC;AACvF,SAASC,GAAG,QAAQ,qBAAoB;AAKxC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAkBC,KAAyB;cAkBZC;IAjB7C,MAAMC,YAAYP;IAClB,MAAM,EAAEQ,MAAMC,IAAI,EAAE,GAAGP;IACvB,MAAMQ,cAAMD,wBAAAA,KAAMC,GAAG,mBAAI;IACzB,MAAMC,aAAaV,gBACjB,IAAOI,SAASK,MAAMP,IAAII,WAAW,QAAQF,OAAO,WAAWK,OAAO,MACtE;QAACH;QAAWF;QAAOK;KAAI;IAEzB,MAAME,SAASD,WAAWE,MAAM,KAAK,YAAYF,WAAWH,IAAI,GAAGM;IACnE,MAAMC,SAAS,QAAOH,0BAAAA,OAAQG,MAAM,MAAK,WAAWH,OAAOG,MAAM,GAAG;IACpE,MAAMT,WAAWL,gBACf,IAAOI,SAASU,SAASZ,IAAII,WAAW,QAAQF,OAAO,SAASU,UAAU,MAC1E;QAACR;QAAWF;QAAOU;KAAO;IAE5B,MAAMC,QACJC,QAAQZ,SAASK,QAAQC,WAAWE,MAAM,KAAK,aAAc,CAAA,CAACE,UAAUT,SAASO,MAAM,KAAK,SAAQ;IACtG,MAAMK,UAAUnB,sBACda,iBAAAA,SAAU,MACVG,UAAUT,SAASO,MAAM,KAAK,aAAaP,iBAAAA,SAASE,IAAI,YAAbF,iBAAiB,OAAQ;IAEtE,OAAO;QAAEa,UAAUF,QAAQL,WAAWd,gBAAgBc,WAAWM,OAAO,CAAC,cAAc,KAAK;QAAMF;IAAM;AAC1G"}
|
|
@@ -14,7 +14,8 @@ 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 { CONTACT_LIFECYCLE_STAGE_LABELS,
|
|
17
|
+
*/ import { CONTACT_LIFECYCLE_STAGE_LABELS, CRM_ACTIVITY_KIND_LABELS, CRM_EMAIL_DELIVERY_STATE_LABELS, CRM_FORECAST_CATEGORY_LABELS, CRM_TASK_KIND_LABELS, contactDisplayName, crmActivityDirection, crmLeadStatusLabel, DEAL_NEXT_STEP_MAX, dealContactRolesOf, dealForecastCategory, dealProbability, dealStageById, dealStageForecastCategory, interactionsForGroup, isContactLifecycleStage, isCrmEmailDeliveryState, readContactFacet } from "@aglyn/aglyn/server";
|
|
18
|
+
import { labelContactSource } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
|
|
18
19
|
import { CRM_FACTS_CONTACT_ROLES_MAX, CRM_FACTS_DEALS_MAX, CRM_FACTS_LABEL_MAX, CRM_FACTS_NOTES_MAX, CRM_FACTS_TAGS_MAX, CRM_FACTS_TASKS_MAX, CRM_FACTS_TEXT_MAX, crmFactDay, crmFactMillis, crmFactMoney, crmFactText, crmTimelineFacts, leadSourceFact } from "./record-facts.js";
|
|
19
20
|
/** An email address written inside free text. */ const EMAIL_IN_PROSE = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g;
|
|
20
21
|
/** A run of digits and the separators a phone number is written with. */ const DIGIT_RUN_IN_PROSE = /\+?\(?\d[\d\s().-]{7,}\d/g;
|
|
@@ -62,7 +63,7 @@ function tagsOf(value) {
|
|
|
62
63
|
/** A capture the platform recorded on a contact, as a timeline entry. */ export function crmDisclosedInteractionFact(interaction) {
|
|
63
64
|
const on = crmFactDay(interaction.atMs);
|
|
64
65
|
if (!on) return null;
|
|
65
|
-
const kind = interaction.type
|
|
66
|
+
const kind = labelContactSource(interaction.type, 'Capture');
|
|
66
67
|
const text = crmFactProse(interaction.summary, CRM_FACTS_TEXT_MAX);
|
|
67
68
|
return text ? {
|
|
68
69
|
on,
|
|
@@ -121,10 +122,7 @@ export function disclosedContactFacts(input) {
|
|
|
121
122
|
var _facet_sources;
|
|
122
123
|
const { row, group } = input;
|
|
123
124
|
const facet = readContactFacet(row, group.groupId);
|
|
124
|
-
const sources = Object.entries((_facet_sources = facet.sources) != null ? _facet_sources : {}).filter(([, on])=>on === true).map(([source])=>
|
|
125
|
-
var _CONTACT_SOURCE_LABELS_source;
|
|
126
|
-
return (_CONTACT_SOURCE_LABELS_source = CONTACT_SOURCE_LABELS[source]) != null ? _CONTACT_SOURCE_LABELS_source : '';
|
|
127
|
-
}).filter(Boolean);
|
|
125
|
+
const sources = Object.entries((_facet_sources = facet.sources) != null ? _facet_sources : {}).filter(([, on])=>on === true).map(([source])=>labelContactSource(source, '')).filter(Boolean);
|
|
128
126
|
const interactions = interactionsForGroup(facet.interactions, group.hostIds);
|
|
129
127
|
return {
|
|
130
128
|
record: 'contact',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/record-facts-disclosed.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n CRM_ACTIVITY_KIND_LABELS,\n CRM_EMAIL_DELIVERY_STATE_LABELS,\n CRM_FORECAST_CATEGORY_LABELS,\n CRM_TASK_KIND_LABELS,\n contactDisplayName,\n crmActivityDirection,\n crmLeadStatusLabel,\n DEAL_NEXT_STEP_MAX,\n dealContactRolesOf,\n dealForecastCategory,\n dealProbability,\n dealStageById,\n dealStageForecastCategory,\n interactionsForGroup,\n isContactLifecycleStage,\n isCrmEmailDeliveryState,\n readContactFacet,\n type ContactInteraction,\n type CrmActivity,\n type CrmDeal,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n CRM_FACTS_CONTACT_ROLES_MAX,\n CRM_FACTS_DEALS_MAX,\n CRM_FACTS_LABEL_MAX,\n CRM_FACTS_NOTES_MAX,\n CRM_FACTS_TAGS_MAX,\n CRM_FACTS_TASKS_MAX,\n CRM_FACTS_TEXT_MAX,\n crmFactDay,\n crmFactMillis,\n crmFactMoney,\n crmFactText,\n crmTimelineFacts,\n leadSourceFact,\n type CompanyFactsInput,\n type ContactFactsInput,\n type CrmCompanyFacts,\n type CrmContactFacts,\n type CrmContactRoleFact,\n type CrmDealFact,\n type CrmDealFacts,\n type CrmLeadFacts,\n type CrmTaskFact,\n type CrmTimelineFact,\n type DealFactsInput,\n type LeadFactsInput,\n} from './record-facts'\n\n/**\n * THE DISCLOSED FACTS: what CRM assistance sends while\n * `release_crm_assist_whole_record` is off.\n *\n * The published Privacy Policy (section 2) and the Anthropic row on\n * `/legal/subprocessors` promise that CRM assistance sends a record's name\n * and, by kind, its job title, company, lifecycle stage, tags, capture\n * history and counts, domain, industry, headcount, pipeline stages, status,\n * amount, dates, lost reason, parties and lead status, with its notes,\n * newest timeline entries and open tasks and deals — and never an email,\n * phone or postal field, marketing consent, custom field value, team member\n * or record id. These builders report exactly that, so production keeps the\n * promise until those pages are republished and the flag is turned on;\n * `record-facts.ts` builds the whole record for the flag's on side.\n *\n * ## What never leaves\n *\n * No email address, phone number or postal address of anyone; no birthdate;\n * no assistant and no reports-to; no marketing consent; no custom field\n * VALUE; no team member's name or id; no document id of any record; no\n * file; no order or payment detail beyond a count. A logged email carries its\n * subject and a cut of its body, never the address it went to or came from.\n *\n * Every text a person wrote into a record — a name, a job title, a tag, notes,\n * a logged activity, a capture's summary, a task's or a deal's title, a\n * reason — is reported as written, except that an email address or a phone\n * number inside it is replaced by a placeholder first ({@link crmFactProse}):\n * an import that put an address in the name column leaves no address behind.\n * What the workspace configured — pipeline, stage and field names — and a\n * company's domain are reported as they are. A postal address typed into a\n * note is not recognized and leaves as typed.\n *\n * The bytes are as stable as the whole record's: one key order, UTC days,\n * money as a code and a fixed-point amount.\n */\n\nexport type CrmDisclosedContactFacts = Pick<\n CrmContactFacts,\n | 'record'\n | 'name'\n | 'salutation'\n | 'jobTitle'\n | 'department'\n | 'doNotCall'\n | 'company'\n | 'lifecycleStage'\n | 'tags'\n | 'sources'\n | 'orders'\n | 'lastPurchase'\n | 'since'\n | 'lastEmailEngagement'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n | 'deals'\n>\n\nexport type CrmDisclosedCompanyFacts = Pick<\n CrmCompanyFacts,\n | 'record'\n | 'name'\n | 'domain'\n | 'industry'\n | 'type'\n | 'rating'\n | 'ownership'\n | 'accountSource'\n | 'employees'\n | 'annualRevenue'\n | 'tags'\n | 'people'\n | 'since'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n | 'deals'\n>\n\nexport type CrmDisclosedDealFacts = Pick<\n CrmDealFacts,\n | 'record'\n | 'title'\n | 'pipeline'\n | 'stages'\n | 'stageId'\n | 'stage'\n | 'status'\n | 'amount'\n | 'expectedClose'\n | 'inStageSince'\n | 'lostReason'\n | 'type'\n | 'leadSource'\n | 'nextStep'\n | 'probability'\n | 'forecastCategory'\n | 'contact'\n | 'contactRoles'\n | 'company'\n | 'since'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n> & {\n /** How many products the deal holds — a count, never their names or prices. */\n products: number\n}\n\nexport type CrmDisclosedLeadFacts = Pick<\n CrmLeadFacts,\n | 'record'\n | 'name'\n | 'salutation'\n | 'company'\n | 'jobTitle'\n | 'leadSource'\n | 'industry'\n | 'rating'\n | 'employees'\n | 'annualRevenue'\n | 'doNotCall'\n | 'status'\n | 'sources'\n | 'captures'\n | 'firstSeen'\n | 'lastSeen'\n | 'assigned'\n | 'converted'\n | 'unqualifiedReason'\n | 'notes'\n | 'timeline'\n>\n\n/** An email address written inside free text. */\nconst EMAIL_IN_PROSE = /[\\w.+-]+@[\\w-]+(?:\\.[\\w-]+)+/g\n/** A run of digits and the separators a phone number is written with. */\nconst DIGIT_RUN_IN_PROSE = /\\+?\\(?\\d[\\d\\s().-]{7,}\\d/g\n/** Calendar days, which a run of digits may be and a phone number is not. */\nconst DAYS_ONLY = /^\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{4}-\\d{2}-\\d{2})*$/\n\nexport const CRM_FACTS_EMAIL_PLACEHOLDER = '[email address]'\nexport const CRM_FACTS_PHONE_PLACEHOLDER = '[phone number]'\n\nconst DAY_MS = 86_400_000\n\n/**\n * Free text the team wrote, as {@link crmFactText} reports it, with every\n * email address, and every run of ten digits or more that is not a list of\n * days, replaced by a placeholder. The fields that hold an address or a number\n * are never reported, and this keeps one typed into a note from leaving\n * either. Replaced before the cut, so a cut never halves an address into\n * something unrecognized.\n */\nexport function crmFactProse(value: unknown, max: number): string {\n if (typeof value !== 'string') return ''\n const scrubbed = value\n .replace(EMAIL_IN_PROSE, CRM_FACTS_EMAIL_PLACEHOLDER)\n .replace(DIGIT_RUN_IN_PROSE, (run) =>\n DAYS_ONLY.test(run.trim()) || run.replace(/\\D/g, '').length < 10 ? run : CRM_FACTS_PHONE_PLACEHOLDER,\n )\n return crmFactText(scrubbed, max)\n}\n\nfunction tagsOf(value: unknown): string[] {\n return Array.isArray(value)\n ? value\n .map((tag) => crmFactProse(tag, 40))\n .filter(Boolean)\n .slice(0, CRM_FACTS_TAGS_MAX)\n : []\n}\n\n/** A logged activity as a timeline entry: never the address it went to or came from. */\nexport function crmDisclosedActivityFact(activity: Partial<CrmActivity>): CrmTimelineFact | null {\n const on = crmFactDay(activity.atMs)\n if (!on) return null\n const kind = activity.kind && CRM_ACTIVITY_KIND_LABELS[activity.kind] ? activity.kind : 'other'\n const fact: CrmTimelineFact = { on, kind: CRM_ACTIVITY_KIND_LABELS[kind] }\n const direction = crmActivityDirection(kind, activity.direction)\n if (direction) fact.direction = direction\n const subject = crmFactProse(activity.subject ?? activity.threadSubject, CRM_FACTS_LABEL_MAX)\n if (subject) fact.subject = subject\n const text = crmFactProse(activity.body, CRM_FACTS_TEXT_MAX)\n if (text) fact.text = text\n const outcome = crmFactProse(activity.outcome, CRM_FACTS_LABEL_MAX)\n if (outcome) fact.outcome = outcome\n if (kind === 'email' && isCrmEmailDeliveryState(activity.deliveryState)) {\n fact.delivery = CRM_EMAIL_DELIVERY_STATE_LABELS[activity.deliveryState]\n }\n return fact\n}\n\n/** A capture the platform recorded on a contact, as a timeline entry. */\nexport function crmDisclosedInteractionFact(interaction: Partial<ContactInteraction>): CrmTimelineFact | null {\n const on = crmFactDay(interaction.atMs)\n if (!on) return null\n const kind =\n interaction.type && CONTACT_SOURCE_LABELS[interaction.type] ? CONTACT_SOURCE_LABELS[interaction.type] : 'Capture'\n const text = crmFactProse(interaction.summary, CRM_FACTS_TEXT_MAX)\n return text ? { on, kind, text } : { on, kind }\n}\n\n/** The open tasks among `tasks`, soonest due first, undated last: never who holds one, nor its notes. */\nexport function crmDisclosedOpenTaskFacts(tasks: ReadonlyArray<Partial<CrmTask>>, nowMs: number): CrmTaskFact[] {\n return tasks\n .filter((task) => task.status !== 'done' && crmFactText(task.title, CRM_FACTS_LABEL_MAX))\n .map((task, index) => ({ task, index, due: crmFactMillis(task.dueAtMs) }))\n .sort((a, b) => (a.due ?? Number.POSITIVE_INFINITY) - (b.due ?? Number.POSITIVE_INFINITY) || a.index - b.index)\n .slice(0, CRM_FACTS_TASKS_MAX)\n .map(({ task, due }) => ({\n title: crmFactProse(task.title, CRM_FACTS_LABEL_MAX),\n kind:\n crmFactProse(task.typeLabel, CRM_FACTS_LABEL_MAX) ||\n (task.kind && CRM_TASK_KIND_LABELS[task.kind] ? CRM_TASK_KIND_LABELS[task.kind] : 'To-do'),\n priority: task.priority === 'high' || task.priority === 'low' ? task.priority : 'normal',\n ...(crmFactProse(task.statusLabel, CRM_FACTS_LABEL_MAX)\n ? { status: crmFactProse(task.statusLabel, CRM_FACTS_LABEL_MAX) }\n : {}),\n due: due === null ? null : crmFactDay(due),\n // Overdue by the UTC day: a task due today is not overdue until tomorrow.\n overdue: due !== null && Math.floor(due / DAY_MS) < Math.floor(nowMs / DAY_MS),\n }))\n}\n\n/** One deal, with its stage named by the pipeline it is in. */\nfunction disclosedDealSummary(deal: Partial<CrmDeal>, pipeline: CrmPipeline | null | undefined): CrmDealFact {\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n title: crmFactProse(deal.title, CRM_FACTS_LABEL_MAX),\n stage: crmFactText(stage?.name, CRM_FACTS_LABEL_MAX),\n status: deal.status === 'won' || deal.status === 'lost' ? deal.status : 'open',\n amount: crmFactMoney(deal.amountCents, deal.currency),\n expectedClose: crmFactDay(deal.expectedCloseAtMs),\n }\n}\n\n/** Deals, open ones first, then most recently updated. */\nfunction disclosedDealSummaries(\n deals: ReadonlyArray<Partial<CrmDeal>>,\n pipelines: ReadonlyMap<string, CrmPipeline>,\n): CrmDealFact[] {\n return deals\n .map((deal, index) => ({ deal, index, updated: crmFactMillis(deal.updatedAt) ?? 0 }))\n .sort(\n (a, b) =>\n Number(a.deal.status !== 'open') - Number(b.deal.status !== 'open') ||\n b.updated - a.updated ||\n a.index - b.index,\n )\n .slice(0, CRM_FACTS_DEALS_MAX)\n .map(({ deal }) => disclosedDealSummary(deal, pipelines.get(String(deal.pipelineId ?? ''))))\n}\n\nexport function disclosedContactFacts(input: ContactFactsInput): CrmDisclosedContactFacts {\n const { row, group } = input\n const facet = readContactFacet(row, group.groupId)\n const sources = Object.entries(facet.sources ?? {})\n .filter(([, on]) => on === true)\n .map(([source]) => CONTACT_SOURCE_LABELS[source as keyof typeof CONTACT_SOURCE_LABELS] ?? '')\n .filter(Boolean)\n const interactions = interactionsForGroup(facet.interactions, group.hostIds)\n return {\n record: 'contact',\n name: crmFactProse(contactDisplayName(row, group.groupId), CRM_FACTS_LABEL_MAX),\n salutation: crmFactProse(facet.salutation, CRM_FACTS_LABEL_MAX),\n jobTitle: crmFactProse(facet.jobTitle, CRM_FACTS_LABEL_MAX),\n department: crmFactProse(facet.department, CRM_FACTS_LABEL_MAX),\n doNotCall: facet.doNotCall === true,\n company: crmFactProse(facet.companyName, CRM_FACTS_LABEL_MAX),\n lifecycleStage: isContactLifecycleStage(facet.lifecycleStage)\n ? CONTACT_LIFECYCLE_STAGE_LABELS[facet.lifecycleStage]\n : '',\n tags: tagsOf(facet.tags),\n sources: [...new Set(sources)].sort(),\n orders: typeof facet.ordersCount === 'number' && facet.ordersCount > 0 ? Math.floor(facet.ordersCount) : 0,\n lastPurchase: crmFactDay(facet.lastPurchaseAtMs),\n since: crmFactDay(row['createdAt']),\n lastEmailEngagement: crmFactDay(facet.lastEmailEngagementAtMs),\n notes: crmFactProse(facet.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts([\n ...input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ...interactions.map((interaction) => ({ atMs: interaction.atMs, fact: crmDisclosedInteractionFact(interaction) })),\n ]),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n deals: disclosedDealSummaries(input.deals, input.pipelines),\n }\n}\n\nexport function disclosedCompanyFacts(input: CompanyFactsInput): CrmDisclosedCompanyFacts {\n const { company } = input\n return {\n record: 'company',\n name: crmFactProse(company.name, CRM_FACTS_LABEL_MAX),\n domain: crmFactText(company.domain, CRM_FACTS_LABEL_MAX),\n industry: crmFactProse(company.industry, CRM_FACTS_LABEL_MAX),\n type: crmFactProse(company.type, CRM_FACTS_LABEL_MAX),\n rating: crmFactProse(company.rating, CRM_FACTS_LABEL_MAX),\n ownership: crmFactProse(company.ownership, CRM_FACTS_LABEL_MAX),\n accountSource: crmFactProse(company.accountSource, CRM_FACTS_LABEL_MAX),\n employees:\n typeof company.numberOfEmployees === 'number' && company.numberOfEmployees >= 0\n ? Math.floor(company.numberOfEmployees)\n : null,\n annualRevenue: crmFactMoney(company.annualRevenueCents, company.currency),\n tags: tagsOf(company.tags),\n people:\n typeof company.contactsCount === 'number' && company.contactsCount > 0\n ? Math.floor(company.contactsCount)\n : 0,\n since: crmFactDay(company.createdAt),\n notes: crmFactProse(company.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n deals: disclosedDealSummaries(input.deals, input.pipelines),\n }\n}\n\n/**\n * Every contact on a deal and the part each plays (AGL-3521), the Primary\n * first and at most {@link CRM_FACTS_CONTACT_ROLES_MAX}: each named as the\n * reader resolved them, with an address or a number in a name replaced. A\n * contact the reader could not see is left out rather than named by id.\n */\nfunction disclosedContactRoleFacts(\n deal: { contactId?: unknown; contactRoles?: unknown },\n names: ReadonlyMap<string, string> = new Map(),\n): CrmContactRoleFact[] {\n return dealContactRolesOf(deal)\n .sort((a, b) => Number(b.primary) - Number(a.primary))\n .filter((row) => names.has(row.contactId))\n .slice(0, CRM_FACTS_CONTACT_ROLES_MAX)\n .map((row) => ({\n name: crmFactProse(names.get(row.contactId), CRM_FACTS_LABEL_MAX),\n role: crmFactText(row.role, CRM_FACTS_LABEL_MAX),\n primary: row.primary,\n }))\n}\n\nexport function disclosedDealFacts(input: DealFactsInput): CrmDisclosedDealFacts {\n const { deal, pipeline } = input\n const summary = disclosedDealSummary(deal, pipeline)\n const stages = [...(pipeline?.stages ?? [])]\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))\n .map((stage) => ({\n id: String(stage.id),\n name: crmFactText(stage.name, CRM_FACTS_LABEL_MAX),\n kind: stage.kind === 'won' || stage.kind === 'lost' ? stage.kind : ('open' as const),\n probability: Math.min(100, Math.max(0, Number(stage.probability) || 0)),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealStageForecastCategory(stage)],\n }))\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n record: 'deal',\n title: summary.title,\n pipeline: crmFactText(pipeline?.name, CRM_FACTS_LABEL_MAX),\n stages,\n stageId: String(deal.stageId ?? ''),\n stage: summary.stage,\n status: summary.status,\n amount: summary.amount,\n expectedClose: summary.expectedClose,\n inStageSince: crmFactDay(deal.stageChangedAtMs),\n lostReason: crmFactProse(deal.lostReason, CRM_FACTS_LABEL_MAX),\n type: crmFactText(deal.type, CRM_FACTS_LABEL_MAX),\n leadSource: crmFactText(deal.leadSource, CRM_FACTS_LABEL_MAX),\n nextStep: crmFactProse(deal.nextStep, DEAL_NEXT_STEP_MAX),\n probability: dealProbability(deal, stage),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealForecastCategory(deal, stage)],\n contact: crmFactProse(deal.contactName, CRM_FACTS_LABEL_MAX),\n contactRoles: disclosedContactRoleFacts(deal, input.contactNames),\n company: crmFactProse(deal.companyName, CRM_FACTS_LABEL_MAX),\n products: Array.isArray(deal.lineItems) ? deal.lineItems.length : 0,\n since: crmFactDay(deal.createdAt),\n notes: crmFactProse(deal.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n }\n}\n\nexport function disclosedLeadFacts(input: LeadFactsInput): CrmDisclosedLeadFacts {\n const { lead, leadStatuses } = input\n const rawSources = Array.isArray(lead['sources'])\n ? lead['sources']\n : typeof lead['source'] === 'string'\n ? [lead['source']]\n : []\n const captures = Number(lead['submissionCount'])\n return {\n record: 'lead',\n name: crmFactProse(lead['name'], CRM_FACTS_LABEL_MAX),\n salutation: crmFactProse(lead['salutation'], CRM_FACTS_LABEL_MAX),\n company: crmFactProse(lead['company'], CRM_FACTS_LABEL_MAX),\n jobTitle: crmFactProse(lead['jobTitle'], CRM_FACTS_LABEL_MAX),\n leadSource: crmFactText(lead['leadSource'], CRM_FACTS_LABEL_MAX),\n industry: crmFactText(lead['industry'], CRM_FACTS_LABEL_MAX),\n rating: crmFactText(lead['rating'], CRM_FACTS_LABEL_MAX),\n employees:\n typeof lead['numberOfEmployees'] === 'number' && lead['numberOfEmployees'] >= 0\n ? Math.floor(lead['numberOfEmployees'])\n : null,\n annualRevenue: crmFactMoney(lead['annualRevenueCents'], lead['currency']),\n doNotCall: lead['doNotCall'] === true,\n status: crmLeadStatusLabel(lead as { status?: never; statusLabel?: string }, leadStatuses),\n sources: [...new Set(rawSources.map((source) => leadSourceFact(String(source))))].sort(),\n captures: Number.isFinite(captures) && captures > 0 ? Math.floor(captures) : 0,\n firstSeen: crmFactDay(lead['firstSeenAtMs']),\n lastSeen: crmFactDay(lead['lastSeenAtMs']),\n assigned: typeof lead['ownerUid'] === 'string' && lead['ownerUid'] !== '',\n converted: typeof lead['convertedContactId'] === 'string' && lead['convertedContactId'] !== '',\n unqualifiedReason: crmFactProse(lead['unqualifiedReason'], CRM_FACTS_LABEL_MAX),\n notes: crmFactProse(lead['notes'], CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n }\n}\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","CRM_ACTIVITY_KIND_LABELS","CRM_EMAIL_DELIVERY_STATE_LABELS","CRM_FORECAST_CATEGORY_LABELS","CRM_TASK_KIND_LABELS","contactDisplayName","crmActivityDirection","crmLeadStatusLabel","DEAL_NEXT_STEP_MAX","dealContactRolesOf","dealForecastCategory","dealProbability","dealStageById","dealStageForecastCategory","interactionsForGroup","isContactLifecycleStage","isCrmEmailDeliveryState","readContactFacet","CRM_FACTS_CONTACT_ROLES_MAX","CRM_FACTS_DEALS_MAX","CRM_FACTS_LABEL_MAX","CRM_FACTS_NOTES_MAX","CRM_FACTS_TAGS_MAX","CRM_FACTS_TASKS_MAX","CRM_FACTS_TEXT_MAX","crmFactDay","crmFactMillis","crmFactMoney","crmFactText","crmTimelineFacts","leadSourceFact","EMAIL_IN_PROSE","DIGIT_RUN_IN_PROSE","DAYS_ONLY","CRM_FACTS_EMAIL_PLACEHOLDER","CRM_FACTS_PHONE_PLACEHOLDER","DAY_MS","crmFactProse","value","max","scrubbed","replace","run","test","trim","length","tagsOf","Array","isArray","map","tag","filter","Boolean","slice","crmDisclosedActivityFact","activity","on","atMs","kind","fact","direction","subject","threadSubject","text","body","outcome","deliveryState","delivery","crmDisclosedInteractionFact","interaction","type","summary","crmDisclosedOpenTaskFacts","tasks","nowMs","task","status","title","index","due","dueAtMs","sort","a","b","Number","POSITIVE_INFINITY","typeLabel","priority","statusLabel","overdue","Math","floor","disclosedDealSummary","deal","pipeline","stage","undefined","String","stageId","name","amount","amountCents","currency","expectedClose","expectedCloseAtMs","disclosedDealSummaries","deals","pipelines","updated","updatedAt","get","pipelineId","disclosedContactFacts","input","facet","row","group","groupId","sources","Object","entries","source","interactions","hostIds","record","salutation","jobTitle","department","doNotCall","company","companyName","lifecycleStage","tags","Set","orders","ordersCount","lastPurchase","lastPurchaseAtMs","since","lastEmailEngagement","lastEmailEngagementAtMs","notes","timeline","activities","openTasks","disclosedCompanyFacts","domain","industry","rating","ownership","accountSource","employees","numberOfEmployees","annualRevenue","annualRevenueCents","people","contactsCount","createdAt","disclosedContactRoleFacts","names","Map","primary","has","contactId","role","disclosedDealFacts","stages","order","id","probability","min","forecastCategory","inStageSince","stageChangedAtMs","lostReason","leadSource","nextStep","contact","contactName","contactRoles","contactNames","products","lineItems","disclosedLeadFacts","lead","leadStatuses","rawSources","captures","isFinite","firstSeen","lastSeen","assigned","converted","unqualifiedReason"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/record-facts-disclosed.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CRM_ACTIVITY_KIND_LABELS,\n CRM_EMAIL_DELIVERY_STATE_LABELS,\n CRM_FORECAST_CATEGORY_LABELS,\n CRM_TASK_KIND_LABELS,\n contactDisplayName,\n crmActivityDirection,\n crmLeadStatusLabel,\n DEAL_NEXT_STEP_MAX,\n dealContactRolesOf,\n dealForecastCategory,\n dealProbability,\n dealStageById,\n dealStageForecastCategory,\n interactionsForGroup,\n isContactLifecycleStage,\n isCrmEmailDeliveryState,\n readContactFacet,\n type ContactInteraction,\n type CrmActivity,\n type CrmDeal,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport { labelContactSource } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CRM_FACTS_CONTACT_ROLES_MAX,\n CRM_FACTS_DEALS_MAX,\n CRM_FACTS_LABEL_MAX,\n CRM_FACTS_NOTES_MAX,\n CRM_FACTS_TAGS_MAX,\n CRM_FACTS_TASKS_MAX,\n CRM_FACTS_TEXT_MAX,\n crmFactDay,\n crmFactMillis,\n crmFactMoney,\n crmFactText,\n crmTimelineFacts,\n leadSourceFact,\n type CompanyFactsInput,\n type ContactFactsInput,\n type CrmCompanyFacts,\n type CrmContactFacts,\n type CrmContactRoleFact,\n type CrmDealFact,\n type CrmDealFacts,\n type CrmLeadFacts,\n type CrmTaskFact,\n type CrmTimelineFact,\n type DealFactsInput,\n type LeadFactsInput,\n} from './record-facts'\n\n/**\n * THE DISCLOSED FACTS: what CRM assistance sends while\n * `release_crm_assist_whole_record` is off.\n *\n * The published Privacy Policy (section 2) and the Anthropic row on\n * `/legal/subprocessors` promise that CRM assistance sends a record's name\n * and, by kind, its job title, company, lifecycle stage, tags, capture\n * history and counts, domain, industry, headcount, pipeline stages, status,\n * amount, dates, lost reason, parties and lead status, with its notes,\n * newest timeline entries and open tasks and deals — and never an email,\n * phone or postal field, marketing consent, custom field value, team member\n * or record id. These builders report exactly that, so production keeps the\n * promise until those pages are republished and the flag is turned on;\n * `record-facts.ts` builds the whole record for the flag's on side.\n *\n * ## What never leaves\n *\n * No email address, phone number or postal address of anyone; no birthdate;\n * no assistant and no reports-to; no marketing consent; no custom field\n * VALUE; no team member's name or id; no document id of any record; no\n * file; no order or payment detail beyond a count. A logged email carries its\n * subject and a cut of its body, never the address it went to or came from.\n *\n * Every text a person wrote into a record — a name, a job title, a tag, notes,\n * a logged activity, a capture's summary, a task's or a deal's title, a\n * reason — is reported as written, except that an email address or a phone\n * number inside it is replaced by a placeholder first ({@link crmFactProse}):\n * an import that put an address in the name column leaves no address behind.\n * What the workspace configured — pipeline, stage and field names — and a\n * company's domain are reported as they are. A postal address typed into a\n * note is not recognized and leaves as typed.\n *\n * The bytes are as stable as the whole record's: one key order, UTC days,\n * money as a code and a fixed-point amount.\n */\n\nexport type CrmDisclosedContactFacts = Pick<\n CrmContactFacts,\n | 'record'\n | 'name'\n | 'salutation'\n | 'jobTitle'\n | 'department'\n | 'doNotCall'\n | 'company'\n | 'lifecycleStage'\n | 'tags'\n | 'sources'\n | 'orders'\n | 'lastPurchase'\n | 'since'\n | 'lastEmailEngagement'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n | 'deals'\n>\n\nexport type CrmDisclosedCompanyFacts = Pick<\n CrmCompanyFacts,\n | 'record'\n | 'name'\n | 'domain'\n | 'industry'\n | 'type'\n | 'rating'\n | 'ownership'\n | 'accountSource'\n | 'employees'\n | 'annualRevenue'\n | 'tags'\n | 'people'\n | 'since'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n | 'deals'\n>\n\nexport type CrmDisclosedDealFacts = Pick<\n CrmDealFacts,\n | 'record'\n | 'title'\n | 'pipeline'\n | 'stages'\n | 'stageId'\n | 'stage'\n | 'status'\n | 'amount'\n | 'expectedClose'\n | 'inStageSince'\n | 'lostReason'\n | 'type'\n | 'leadSource'\n | 'nextStep'\n | 'probability'\n | 'forecastCategory'\n | 'contact'\n | 'contactRoles'\n | 'company'\n | 'since'\n | 'notes'\n | 'timeline'\n | 'openTasks'\n> & {\n /** How many products the deal holds — a count, never their names or prices. */\n products: number\n}\n\nexport type CrmDisclosedLeadFacts = Pick<\n CrmLeadFacts,\n | 'record'\n | 'name'\n | 'salutation'\n | 'company'\n | 'jobTitle'\n | 'leadSource'\n | 'industry'\n | 'rating'\n | 'employees'\n | 'annualRevenue'\n | 'doNotCall'\n | 'status'\n | 'sources'\n | 'captures'\n | 'firstSeen'\n | 'lastSeen'\n | 'assigned'\n | 'converted'\n | 'unqualifiedReason'\n | 'notes'\n | 'timeline'\n>\n\n/** An email address written inside free text. */\nconst EMAIL_IN_PROSE = /[\\w.+-]+@[\\w-]+(?:\\.[\\w-]+)+/g\n/** A run of digits and the separators a phone number is written with. */\nconst DIGIT_RUN_IN_PROSE = /\\+?\\(?\\d[\\d\\s().-]{7,}\\d/g\n/** Calendar days, which a run of digits may be and a phone number is not. */\nconst DAYS_ONLY = /^\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{4}-\\d{2}-\\d{2})*$/\n\nexport const CRM_FACTS_EMAIL_PLACEHOLDER = '[email address]'\nexport const CRM_FACTS_PHONE_PLACEHOLDER = '[phone number]'\n\nconst DAY_MS = 86_400_000\n\n/**\n * Free text the team wrote, as {@link crmFactText} reports it, with every\n * email address, and every run of ten digits or more that is not a list of\n * days, replaced by a placeholder. The fields that hold an address or a number\n * are never reported, and this keeps one typed into a note from leaving\n * either. Replaced before the cut, so a cut never halves an address into\n * something unrecognized.\n */\nexport function crmFactProse(value: unknown, max: number): string {\n if (typeof value !== 'string') return ''\n const scrubbed = value\n .replace(EMAIL_IN_PROSE, CRM_FACTS_EMAIL_PLACEHOLDER)\n .replace(DIGIT_RUN_IN_PROSE, (run) =>\n DAYS_ONLY.test(run.trim()) || run.replace(/\\D/g, '').length < 10 ? run : CRM_FACTS_PHONE_PLACEHOLDER,\n )\n return crmFactText(scrubbed, max)\n}\n\nfunction tagsOf(value: unknown): string[] {\n return Array.isArray(value)\n ? value\n .map((tag) => crmFactProse(tag, 40))\n .filter(Boolean)\n .slice(0, CRM_FACTS_TAGS_MAX)\n : []\n}\n\n/** A logged activity as a timeline entry: never the address it went to or came from. */\nexport function crmDisclosedActivityFact(activity: Partial<CrmActivity>): CrmTimelineFact | null {\n const on = crmFactDay(activity.atMs)\n if (!on) return null\n const kind = activity.kind && CRM_ACTIVITY_KIND_LABELS[activity.kind] ? activity.kind : 'other'\n const fact: CrmTimelineFact = { on, kind: CRM_ACTIVITY_KIND_LABELS[kind] }\n const direction = crmActivityDirection(kind, activity.direction)\n if (direction) fact.direction = direction\n const subject = crmFactProse(activity.subject ?? activity.threadSubject, CRM_FACTS_LABEL_MAX)\n if (subject) fact.subject = subject\n const text = crmFactProse(activity.body, CRM_FACTS_TEXT_MAX)\n if (text) fact.text = text\n const outcome = crmFactProse(activity.outcome, CRM_FACTS_LABEL_MAX)\n if (outcome) fact.outcome = outcome\n if (kind === 'email' && isCrmEmailDeliveryState(activity.deliveryState)) {\n fact.delivery = CRM_EMAIL_DELIVERY_STATE_LABELS[activity.deliveryState]\n }\n return fact\n}\n\n/** A capture the platform recorded on a contact, as a timeline entry. */\nexport function crmDisclosedInteractionFact(interaction: Partial<ContactInteraction>): CrmTimelineFact | null {\n const on = crmFactDay(interaction.atMs)\n if (!on) return null\n const kind = labelContactSource(interaction.type, 'Capture')\n const text = crmFactProse(interaction.summary, CRM_FACTS_TEXT_MAX)\n return text ? { on, kind, text } : { on, kind }\n}\n\n/** The open tasks among `tasks`, soonest due first, undated last: never who holds one, nor its notes. */\nexport function crmDisclosedOpenTaskFacts(tasks: ReadonlyArray<Partial<CrmTask>>, nowMs: number): CrmTaskFact[] {\n return tasks\n .filter((task) => task.status !== 'done' && crmFactText(task.title, CRM_FACTS_LABEL_MAX))\n .map((task, index) => ({ task, index, due: crmFactMillis(task.dueAtMs) }))\n .sort((a, b) => (a.due ?? Number.POSITIVE_INFINITY) - (b.due ?? Number.POSITIVE_INFINITY) || a.index - b.index)\n .slice(0, CRM_FACTS_TASKS_MAX)\n .map(({ task, due }) => ({\n title: crmFactProse(task.title, CRM_FACTS_LABEL_MAX),\n kind:\n crmFactProse(task.typeLabel, CRM_FACTS_LABEL_MAX) ||\n (task.kind && CRM_TASK_KIND_LABELS[task.kind] ? CRM_TASK_KIND_LABELS[task.kind] : 'To-do'),\n priority: task.priority === 'high' || task.priority === 'low' ? task.priority : 'normal',\n ...(crmFactProse(task.statusLabel, CRM_FACTS_LABEL_MAX)\n ? { status: crmFactProse(task.statusLabel, CRM_FACTS_LABEL_MAX) }\n : {}),\n due: due === null ? null : crmFactDay(due),\n // Overdue by the UTC day: a task due today is not overdue until tomorrow.\n overdue: due !== null && Math.floor(due / DAY_MS) < Math.floor(nowMs / DAY_MS),\n }))\n}\n\n/** One deal, with its stage named by the pipeline it is in. */\nfunction disclosedDealSummary(deal: Partial<CrmDeal>, pipeline: CrmPipeline | null | undefined): CrmDealFact {\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n title: crmFactProse(deal.title, CRM_FACTS_LABEL_MAX),\n stage: crmFactText(stage?.name, CRM_FACTS_LABEL_MAX),\n status: deal.status === 'won' || deal.status === 'lost' ? deal.status : 'open',\n amount: crmFactMoney(deal.amountCents, deal.currency),\n expectedClose: crmFactDay(deal.expectedCloseAtMs),\n }\n}\n\n/** Deals, open ones first, then most recently updated. */\nfunction disclosedDealSummaries(\n deals: ReadonlyArray<Partial<CrmDeal>>,\n pipelines: ReadonlyMap<string, CrmPipeline>,\n): CrmDealFact[] {\n return deals\n .map((deal, index) => ({ deal, index, updated: crmFactMillis(deal.updatedAt) ?? 0 }))\n .sort(\n (a, b) =>\n Number(a.deal.status !== 'open') - Number(b.deal.status !== 'open') ||\n b.updated - a.updated ||\n a.index - b.index,\n )\n .slice(0, CRM_FACTS_DEALS_MAX)\n .map(({ deal }) => disclosedDealSummary(deal, pipelines.get(String(deal.pipelineId ?? ''))))\n}\n\nexport function disclosedContactFacts(input: ContactFactsInput): CrmDisclosedContactFacts {\n const { row, group } = input\n const facet = readContactFacet(row, group.groupId)\n const sources = Object.entries(facet.sources ?? {})\n .filter(([, on]) => on === true)\n .map(([source]) => labelContactSource(source, ''))\n .filter(Boolean)\n const interactions = interactionsForGroup(facet.interactions, group.hostIds)\n return {\n record: 'contact',\n name: crmFactProse(contactDisplayName(row, group.groupId), CRM_FACTS_LABEL_MAX),\n salutation: crmFactProse(facet.salutation, CRM_FACTS_LABEL_MAX),\n jobTitle: crmFactProse(facet.jobTitle, CRM_FACTS_LABEL_MAX),\n department: crmFactProse(facet.department, CRM_FACTS_LABEL_MAX),\n doNotCall: facet.doNotCall === true,\n company: crmFactProse(facet.companyName, CRM_FACTS_LABEL_MAX),\n lifecycleStage: isContactLifecycleStage(facet.lifecycleStage)\n ? CONTACT_LIFECYCLE_STAGE_LABELS[facet.lifecycleStage]\n : '',\n tags: tagsOf(facet.tags),\n sources: [...new Set(sources)].sort(),\n orders: typeof facet.ordersCount === 'number' && facet.ordersCount > 0 ? Math.floor(facet.ordersCount) : 0,\n lastPurchase: crmFactDay(facet.lastPurchaseAtMs),\n since: crmFactDay(row['createdAt']),\n lastEmailEngagement: crmFactDay(facet.lastEmailEngagementAtMs),\n notes: crmFactProse(facet.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts([\n ...input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ...interactions.map((interaction) => ({ atMs: interaction.atMs, fact: crmDisclosedInteractionFact(interaction) })),\n ]),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n deals: disclosedDealSummaries(input.deals, input.pipelines),\n }\n}\n\nexport function disclosedCompanyFacts(input: CompanyFactsInput): CrmDisclosedCompanyFacts {\n const { company } = input\n return {\n record: 'company',\n name: crmFactProse(company.name, CRM_FACTS_LABEL_MAX),\n domain: crmFactText(company.domain, CRM_FACTS_LABEL_MAX),\n industry: crmFactProse(company.industry, CRM_FACTS_LABEL_MAX),\n type: crmFactProse(company.type, CRM_FACTS_LABEL_MAX),\n rating: crmFactProse(company.rating, CRM_FACTS_LABEL_MAX),\n ownership: crmFactProse(company.ownership, CRM_FACTS_LABEL_MAX),\n accountSource: crmFactProse(company.accountSource, CRM_FACTS_LABEL_MAX),\n employees:\n typeof company.numberOfEmployees === 'number' && company.numberOfEmployees >= 0\n ? Math.floor(company.numberOfEmployees)\n : null,\n annualRevenue: crmFactMoney(company.annualRevenueCents, company.currency),\n tags: tagsOf(company.tags),\n people:\n typeof company.contactsCount === 'number' && company.contactsCount > 0\n ? Math.floor(company.contactsCount)\n : 0,\n since: crmFactDay(company.createdAt),\n notes: crmFactProse(company.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n deals: disclosedDealSummaries(input.deals, input.pipelines),\n }\n}\n\n/**\n * Every contact on a deal and the part each plays (AGL-3521), the Primary\n * first and at most {@link CRM_FACTS_CONTACT_ROLES_MAX}: each named as the\n * reader resolved them, with an address or a number in a name replaced. A\n * contact the reader could not see is left out rather than named by id.\n */\nfunction disclosedContactRoleFacts(\n deal: { contactId?: unknown; contactRoles?: unknown },\n names: ReadonlyMap<string, string> = new Map(),\n): CrmContactRoleFact[] {\n return dealContactRolesOf(deal)\n .sort((a, b) => Number(b.primary) - Number(a.primary))\n .filter((row) => names.has(row.contactId))\n .slice(0, CRM_FACTS_CONTACT_ROLES_MAX)\n .map((row) => ({\n name: crmFactProse(names.get(row.contactId), CRM_FACTS_LABEL_MAX),\n role: crmFactText(row.role, CRM_FACTS_LABEL_MAX),\n primary: row.primary,\n }))\n}\n\nexport function disclosedDealFacts(input: DealFactsInput): CrmDisclosedDealFacts {\n const { deal, pipeline } = input\n const summary = disclosedDealSummary(deal, pipeline)\n const stages = [...(pipeline?.stages ?? [])]\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))\n .map((stage) => ({\n id: String(stage.id),\n name: crmFactText(stage.name, CRM_FACTS_LABEL_MAX),\n kind: stage.kind === 'won' || stage.kind === 'lost' ? stage.kind : ('open' as const),\n probability: Math.min(100, Math.max(0, Number(stage.probability) || 0)),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealStageForecastCategory(stage)],\n }))\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n record: 'deal',\n title: summary.title,\n pipeline: crmFactText(pipeline?.name, CRM_FACTS_LABEL_MAX),\n stages,\n stageId: String(deal.stageId ?? ''),\n stage: summary.stage,\n status: summary.status,\n amount: summary.amount,\n expectedClose: summary.expectedClose,\n inStageSince: crmFactDay(deal.stageChangedAtMs),\n lostReason: crmFactProse(deal.lostReason, CRM_FACTS_LABEL_MAX),\n type: crmFactText(deal.type, CRM_FACTS_LABEL_MAX),\n leadSource: crmFactText(deal.leadSource, CRM_FACTS_LABEL_MAX),\n nextStep: crmFactProse(deal.nextStep, DEAL_NEXT_STEP_MAX),\n probability: dealProbability(deal, stage),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealForecastCategory(deal, stage)],\n contact: crmFactProse(deal.contactName, CRM_FACTS_LABEL_MAX),\n contactRoles: disclosedContactRoleFacts(deal, input.contactNames),\n company: crmFactProse(deal.companyName, CRM_FACTS_LABEL_MAX),\n products: Array.isArray(deal.lineItems) ? deal.lineItems.length : 0,\n since: crmFactDay(deal.createdAt),\n notes: crmFactProse(deal.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n openTasks: crmDisclosedOpenTaskFacts(input.tasks, input.nowMs),\n }\n}\n\nexport function disclosedLeadFacts(input: LeadFactsInput): CrmDisclosedLeadFacts {\n const { lead, leadStatuses } = input\n const rawSources = Array.isArray(lead['sources'])\n ? lead['sources']\n : typeof lead['source'] === 'string'\n ? [lead['source']]\n : []\n const captures = Number(lead['submissionCount'])\n return {\n record: 'lead',\n name: crmFactProse(lead['name'], CRM_FACTS_LABEL_MAX),\n salutation: crmFactProse(lead['salutation'], CRM_FACTS_LABEL_MAX),\n company: crmFactProse(lead['company'], CRM_FACTS_LABEL_MAX),\n jobTitle: crmFactProse(lead['jobTitle'], CRM_FACTS_LABEL_MAX),\n leadSource: crmFactText(lead['leadSource'], CRM_FACTS_LABEL_MAX),\n industry: crmFactText(lead['industry'], CRM_FACTS_LABEL_MAX),\n rating: crmFactText(lead['rating'], CRM_FACTS_LABEL_MAX),\n employees:\n typeof lead['numberOfEmployees'] === 'number' && lead['numberOfEmployees'] >= 0\n ? Math.floor(lead['numberOfEmployees'])\n : null,\n annualRevenue: crmFactMoney(lead['annualRevenueCents'], lead['currency']),\n doNotCall: lead['doNotCall'] === true,\n status: crmLeadStatusLabel(lead as { status?: never; statusLabel?: string }, leadStatuses),\n sources: [...new Set(rawSources.map((source) => leadSourceFact(String(source))))].sort(),\n captures: Number.isFinite(captures) && captures > 0 ? Math.floor(captures) : 0,\n firstSeen: crmFactDay(lead['firstSeenAtMs']),\n lastSeen: crmFactDay(lead['lastSeenAtMs']),\n assigned: typeof lead['ownerUid'] === 'string' && lead['ownerUid'] !== '',\n converted: typeof lead['convertedContactId'] === 'string' && lead['convertedContactId'] !== '',\n unqualifiedReason: crmFactProse(lead['unqualifiedReason'], CRM_FACTS_LABEL_MAX),\n notes: crmFactProse(lead['notes'], CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmDisclosedActivityFact(activity) })),\n ),\n }\n}\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CRM_ACTIVITY_KIND_LABELS","CRM_EMAIL_DELIVERY_STATE_LABELS","CRM_FORECAST_CATEGORY_LABELS","CRM_TASK_KIND_LABELS","contactDisplayName","crmActivityDirection","crmLeadStatusLabel","DEAL_NEXT_STEP_MAX","dealContactRolesOf","dealForecastCategory","dealProbability","dealStageById","dealStageForecastCategory","interactionsForGroup","isContactLifecycleStage","isCrmEmailDeliveryState","readContactFacet","labelContactSource","CRM_FACTS_CONTACT_ROLES_MAX","CRM_FACTS_DEALS_MAX","CRM_FACTS_LABEL_MAX","CRM_FACTS_NOTES_MAX","CRM_FACTS_TAGS_MAX","CRM_FACTS_TASKS_MAX","CRM_FACTS_TEXT_MAX","crmFactDay","crmFactMillis","crmFactMoney","crmFactText","crmTimelineFacts","leadSourceFact","EMAIL_IN_PROSE","DIGIT_RUN_IN_PROSE","DAYS_ONLY","CRM_FACTS_EMAIL_PLACEHOLDER","CRM_FACTS_PHONE_PLACEHOLDER","DAY_MS","crmFactProse","value","max","scrubbed","replace","run","test","trim","length","tagsOf","Array","isArray","map","tag","filter","Boolean","slice","crmDisclosedActivityFact","activity","on","atMs","kind","fact","direction","subject","threadSubject","text","body","outcome","deliveryState","delivery","crmDisclosedInteractionFact","interaction","type","summary","crmDisclosedOpenTaskFacts","tasks","nowMs","task","status","title","index","due","dueAtMs","sort","a","b","Number","POSITIVE_INFINITY","typeLabel","priority","statusLabel","overdue","Math","floor","disclosedDealSummary","deal","pipeline","stage","undefined","String","stageId","name","amount","amountCents","currency","expectedClose","expectedCloseAtMs","disclosedDealSummaries","deals","pipelines","updated","updatedAt","get","pipelineId","disclosedContactFacts","input","facet","row","group","groupId","sources","Object","entries","source","interactions","hostIds","record","salutation","jobTitle","department","doNotCall","company","companyName","lifecycleStage","tags","Set","orders","ordersCount","lastPurchase","lastPurchaseAtMs","since","lastEmailEngagement","lastEmailEngagementAtMs","notes","timeline","activities","openTasks","disclosedCompanyFacts","domain","industry","rating","ownership","accountSource","employees","numberOfEmployees","annualRevenue","annualRevenueCents","people","contactsCount","createdAt","disclosedContactRoleFacts","names","Map","primary","has","contactId","role","disclosedDealFacts","stages","order","id","probability","min","forecastCategory","inStageSince","stageChangedAtMs","lostReason","leadSource","nextStep","contact","contactName","contactRoles","contactNames","products","lineItems","disclosedLeadFacts","lead","leadStatuses","rawSources","captures","isFinite","firstSeen","lastSeen","assigned","converted","unqualifiedReason"],"mappings":"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"}
|