@aglyn/plugins-crm 1.0.0-beta.231 → 1.0.0-beta.232
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/activity-timeline.js +1 -0
- package/src/lib/components/activity-timeline.js.map +1 -1
- package/src/lib/components/assignment-rule-drawer.js +1 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +1 -0
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-duplicates-card.js +1 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +1 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contact-known-by-card.js +1 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +1 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +1 -0
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-record-header.js +2 -0
- package/src/lib/components/crm-record-header.js.map +1 -1
- package/src/lib/components/deals-section.js +1 -0
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-history-card.js +1 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +1 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +1 -0
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +1 -0
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/recipes-card.js +1 -0
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -0
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +1 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +1 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +1 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.js +1 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +1 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +1 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +1 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports-section.js +2 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/recipes-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n planLabelGrantingFeature,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink, CardDisplay, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Drawer,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection } from 'firebase/firestore'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\nimport type {\n CrmRecipeInstallResult,\n CrmRecipeSiteStatus,\n} from '../constants/api-routes'\nimport { type CrmOrgMount, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport {\n CRM_ACTION_RECIPES,\n type CrmActionRecipe,\n type CrmActionRecipeId,\n} from '../model/crm-recipes'\nimport { useCrmApi } from './use-crm-api'\n\n/** How many of a site's forms the picker offers before it says it stopped. */\nconst FORM_OPTION_CEILING = 100\n\n/**\n * The site's list of actions — where an installed action lives and is\n * edited — at the address the plugin that keeps actions publishes, or `null`\n * for a site whose subdomain the mount could not answer or a workspace where\n * no plugin keeps actions, which is named and not linked.\n */\nfunction siteActionsHref(mount: CrmOrgMount, hostId: string): string | null {\n const subdomain = mount.siteSubdomain(hostId)\n return subdomain\n ? pluginRecordListHref('action', { orgSlug: mount.orgSlug, host: subdomain })\n : null\n}\n\n/**\n * Where a site stands with one recipe, read off the stamps the status route\n * answered. `maybe` is the third answer and the honest one: the site\n * carries actions saved before the stamp existed, any of which could have\n * started from the recipe, so the card neither lists the site as carrying\n * it nor offers the install as if to a blank site. `unread` is the answer\n * before the route has answered, or for a site it did not name.\n */\ntype SiteStanding = 'installed' | 'maybe' | 'clear' | 'unread'\n\nfunction standingOf(\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null,\n hostId: string,\n recipeId: CrmActionRecipeId,\n): SiteStanding {\n const site = status?.get(hostId)\n if (!site) return 'unread'\n if (site.installed.includes(recipeId)) return 'installed'\n return site.unstamped > 0 ? 'maybe' : 'clear'\n}\n\n/**\n * The stamps of every site of the org, read once the site list is known\n * and again after each install, so the card reports what the documents\n * say rather than what it last did.\n */\nfunction useRecipeStatus(mount: CrmOrgMount | null) {\n const api = useCrmApi(null)\n const [sites, setSites] = useState<Map<string, CrmRecipeSiteStatus> | null>(null)\n const [error, setError] = useState<string | null>(null)\n const refresh = useCallback(async () => {\n try {\n const { response, payload } = await api('recipe-status', {})\n if (!response.ok) {\n setError(payload?.error ?? 'Which sites carry a recipe could not be read.')\n return\n }\n const answered = (payload.sites ?? []) as CrmRecipeSiteStatus[]\n setSites(new Map(answered.map((site) => [site.hostId, site])))\n setError(null)\n } catch (caught) {\n console.error(caught)\n setError('Which sites carry a recipe could not be read.')\n }\n }, [api])\n const hostsReady = Boolean(mount?.hostsReady)\n useEffect(() => {\n if (hostsReady) void refresh()\n }, [hostsReady, refresh])\n return { sites, error, refresh }\n}\n\n/** A site's name, linked into its Actions page when the link is known. */\nfunction SiteLink(props: { mount: CrmOrgMount; hostId: string }) {\n const { mount, hostId } = props\n const href = siteActionsHref(mount, hostId)\n const name = mount.siteName(hostId)\n return href ? <AppLink href={href}>{name}</AppLink> : <>{name}</>\n}\nSiteLink.displayName = 'SiteLink'\n\n/** One recipe's \"Installed on\" cell: the sites that carry it, and the ones that may. */\nfunction InstalledOn(props: {\n mount: CrmOrgMount\n recipe: CrmActionRecipe\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null\n}) {\n const { mount, recipe, status } = props\n if (!status) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Reading…'}\n </Typography>\n )\n }\n const installed = mount.hosts.filter(\n (host) => standingOf(status, host.id, recipe.id) === 'installed',\n )\n const maybe = mount.hosts.filter(\n (host) => standingOf(status, host.id, recipe.id) === 'maybe',\n )\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\">\n {installed.length === 0\n ? 'Not installed on any site yet.'\n : installed.map((host, index) => (\n <Fragment key={host.id}>\n {index > 0 ? ', ' : null}\n <SiteLink mount={mount} hostId={host.id} />\n </Fragment>\n ))}\n </Typography>\n {maybe.length > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`May already have it: ${maybe.map((host) => host.name).join(', ')} — ` +\n 'actions there were saved before recipes were tracked.'}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nInstalledOn.displayName = 'InstalledOn'\n\ninterface RecipeInstallFormProps {\n recipe: CrmActionRecipe\n mount: CrmOrgMount\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null\n onClose: () => void\n onInstalled: (result: CrmRecipeInstallResult, hostId: string) => void\n}\n\n/**\n * The install drawer's body: pick the site, pick the form a recipe that\n * needs one is keyed on, confirm. Mounted fresh on every opening — the\n * Drawer unmounts it when closed — so the picks start over each time from\n * the session's site, when that site does not already carry the recipe.\n *\n * The forms are read from the picked site only once a site is picked and\n * only for a recipe that needs one, so a reader installing the other three\n * pays no read. Live forms only: an archived form collects nothing, and a\n * recipe keyed on it would never fire.\n */\nfunction RecipeInstallForm(props: RecipeInstallFormProps) {\n const { recipe, mount, status, onClose, onInstalled } = props\n const firestore = useFirestore()\n const api = useCrmApi(null)\n const needsForm = recipe.picks?.kind === 'form'\n const [hostId, setHostId] = useState(() => {\n const pick = mount.createHostId\n return pick && standingOf(status, pick, recipe.id) !== 'installed' ? pick : ''\n })\n const [formId, setFormId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const { data: formRead, status: formsStatus } = useFirestoreCollection<any>(\n () =>\n needsForm && hostId\n ? collectionCeiling(\n collection(firestore, 'hosts', hostId, 'forms'),\n FORM_OPTION_CEILING,\n )\n : null,\n [firestore, needsForm, hostId],\n { idField: '$id' },\n )\n const { rows: formDocs, truncated: formsTruncated } = ceilingedWindow<any>(\n formRead,\n FORM_OPTION_CEILING,\n )\n const formOptions = useMemo(\n () =>\n (formDocs ?? [])\n .filter((form: any) => !form.archivedAt)\n .map((form: any) => ({\n id: form.$id as string,\n name: (String(form.displayName ?? '').trim() || form.$id) as string,\n }))\n .sort((a: { name: string }, b: { name: string }) =>\n a.name.localeCompare(b.name),\n ),\n [formDocs],\n )\n const formsLoading = needsForm && Boolean(hostId) && formsStatus === 'loading'\n\n const canInstall =\n Boolean(hostId) && (!needsForm || Boolean(formId)) && !busy && !formsLoading\n\n const handleInstall = async () => {\n if (!canInstall) return\n setBusy(true)\n setError(null)\n try {\n const { response, payload } = await api('recipe-install', {\n hostId,\n recipeId: recipe.id,\n ...(formId ? { formId } : {}),\n })\n if (!response.ok) {\n setError(payload?.error ?? 'The recipe could not be installed.')\n return\n }\n onInstalled(payload as CrmRecipeInstallResult, hostId)\n } catch (caught) {\n console.error(caught)\n setError('The recipe could not be installed.')\n } finally {\n setBusy(false)\n }\n }\n\n const siteSuffix = (id: string) => {\n const standing = standingOf(status, id, recipe.id)\n if (standing === 'installed') return ' — installed'\n if (standing === 'maybe') return ' — may already have it'\n return ''\n }\n\n return (\n <Stack spacing={2} sx={{ width: 380, p: 3 }}>\n <Typography variant=\"h6\">{`Install “${recipe.title}”`}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {recipe.description}\n </Typography>\n <TextField\n select\n size=\"small\"\n label=\"Site\"\n value={hostId}\n onChange={(event) => {\n setHostId(event.target.value)\n setFormId('')\n setError(null)\n }}\n disabled={busy}\n helperText={\n mount.hosts.length === 0\n ? 'This organization has no sites yet.'\n : 'The action is written into this site’s Automation → Actions.'\n }\n fullWidth\n >\n {mount.hosts.map((host) => (\n <MenuItem\n key={host.id}\n value={host.id}\n disabled={standingOf(status, host.id, recipe.id) === 'installed'}\n >\n {`${host.name}${siteSuffix(host.id)}`}\n </MenuItem>\n ))}\n </TextField>\n {needsForm ? (\n <TextField\n select\n size=\"small\"\n label=\"Form\"\n value={formId}\n onChange={(event) => setFormId(event.target.value)}\n disabled={!hostId || busy || formsLoading}\n helperText={\n !hostId\n ? 'Pick the site first; the recipe is keyed on one of its forms.'\n : formsLoading\n ? 'Loading the site’s forms…'\n : formOptions.length === 0\n ? 'This site has no live forms.'\n : formsTruncated\n ? `Showing the first ${FORM_OPTION_CEILING} forms.`\n : 'Contacts this form makes are tagged with its name.'\n }\n fullWidth\n >\n {formOptions.map((form) => (\n <MenuItem key={form.id} value={form.id}>\n {form.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The action is created enabled, exactly as the recipe defines it. ' +\n 'Change anything afterwards from the site’s Automation → Actions page.'}\n </Typography>\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canInstall}\n onClick={() => void handleInstall()}\n >\n {busy ? 'Installing…' : 'Install'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\nRecipeInstallForm.displayName = 'RecipeInstallForm'\n\nexport interface RecipesCardProps {\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Recipes\" — the CRM's ready-made automations, installed on a site from\n * the organization (AGL-2639).\n *\n * ## Why this card exists at the org level and nowhere else\n *\n * Automation is a site feature: a site's Actions page has a Recipes menu\n * that opens the editor prefilled, and the person saves what they see.\n * The org hub mounts over every site with no editor of its own, so it had\n * no recipes at all, and an org running five sites installed \"Welcome a\n * new lead\" five times by hand. This card is the org's door: every recipe,\n * the sites that already carry it, and Install, which writes the action\n * into the site the reader picks through the server — the server stamps\n * the recipe, refuses a site outside the org, and refuses a second copy.\n * Under a site the section does not mount it; the site's own menu is the\n * right door there.\n *\n * ## \"Installed on\" reads the stamp, and says when it cannot\n *\n * The status route answers, per site, the recipes a live action is stamped\n * with and how many live actions carry no stamp. A site with any of the\n * latter is named as one that MAY already have the recipe rather than\n * listed as clear: those actions were saved before the stamp existed and\n * the card cannot tell a hand-built welcome from an installed one.\n *\n * ## Creation opens a drawer\n *\n * Install is a button per row; the picks — the site, and for Tag by form\n * one of that site's forms — live in a drawer, never above the list.\n */\nexport function RecipesCard(props: RecipesCardProps) {\n const { org } = props\n const mount = useCrmOrgMount()\n const { enqueueSnackbar } = useSnackbar()\n const status = useRecipeStatus(mount)\n const refreshStatus = status.refresh\n const [installing, setInstalling] = useState<CrmActionRecipe | null>(null)\n const [lastInstall, setLastInstall] = useState<{\n hostId: string\n name: string\n } | null>(null)\n\n const entitled = checkEntitlement(org, 'actions')\n const canInstall = Boolean(mount?.hostsReady) && entitled\n\n const handleInstalled = useCallback(\n (result: CrmRecipeInstallResult, hostId: string) => {\n setInstalling(null)\n setLastInstall({ hostId, name: result.name })\n enqueueSnackbar(\n `Installed “${result.name}” on ${mount?.siteName(hostId) ?? hostId}`,\n { variant: 'success', persist: false },\n )\n void refreshStatus()\n },\n [enqueueSnackbar, mount, refreshStatus],\n )\n\n // Mounted by the section only beneath the org hub's provider; the hooks\n // above run either way, so the render order never depends on the mount.\n if (!mount) return null\n\n const lastInstallHref = lastInstall ? siteActionsHref(mount, lastInstall.hostId) : null\n return (\n <CardDisplay\n header={'Recipes'}\n help={pluginDocsHelp('crmSettings', { anchor: '#recipes' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Ready-made automations for the CRM. Install one on a site and the ' +\n 'action is written into that site’s Automation → Actions as the ' +\n 'recipe defines it, where it can be edited like any other action.'}\n </Typography>\n {lastInstall ? (\n <Alert\n severity=\"success\"\n onClose={() => setLastInstall(null)}\n action={\n lastInstallHref ? (\n // A link dressed as a button: the anchor carries its own\n // href, so it keeps the link role a navigation door has.\n <Button\n component={AppLink as any}\n {...({\n componentVariant: 'naked',\n nativeButton: false,\n } as any)}\n href={lastInstallHref}\n size=\"small\"\n color=\"inherit\"\n >\n {'Open Automation → Actions'}\n </Button>\n ) : undefined\n }\n >\n {`Installed “${lastInstall.name}” on ${mount.siteName(lastInstall.hostId)}.`}\n </Alert>\n ) : null}\n {status.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {status.error}\n </Typography>\n ) : null}\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Recipe'}</TableCell>\n <TableCell>{'Installed on'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {CRM_ACTION_RECIPES.map((recipe) => (\n <TableRow key={recipe.id} hover>\n <TableCell sx={{ maxWidth: 360 }}>\n <Typography variant=\"body2\">{recipe.title}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {recipe.description}\n </Typography>\n </TableCell>\n <TableCell>\n <InstalledOn mount={mount} recipe={recipe} status={status.sites} />\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 120 }}>\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={!canInstall}\n onClick={() => setInstalling(recipe)}\n >\n {'Install'}\n <SrOnly>{` ${recipe.title}`}</SrOnly>\n </Button>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {!entitled ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The actions builder requires the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade.'}\n </Typography>\n ) : null}\n {mount.hostsReady && mount.hosts.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nowhere to install a recipe.'}\n </Typography>\n ) : null}\n </Stack>\n <Drawer anchor=\"right\" open={Boolean(installing)} onClose={() => setInstalling(null)}>\n {installing ? (\n <RecipeInstallForm\n recipe={installing}\n mount={mount}\n status={status.sites}\n onClose={() => setInstalling(null)}\n onInstalled={handleInstalled}\n />\n ) : null}\n </Drawer>\n </CardDisplay>\n )\n}\nRecipesCard.displayName = 'RecipesCard'\n\nexport default RecipesCard\n"],"names":["checkEntitlement","planLabelGrantingFeature","pluginDocsHelp","pluginRecordListHref","AppLink","CardDisplay","SrOnly","ScrollTable","useSnackbar","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","Alert","Button","Drawer","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","Fragment","useCallback","useEffect","useMemo","useState","useCrmOrgMount","CRM_ACTION_RECIPES","useCrmApi","FORM_OPTION_CEILING","siteActionsHref","mount","hostId","subdomain","siteSubdomain","orgSlug","host","standingOf","status","recipeId","site","get","installed","includes","unstamped","useRecipeStatus","api","sites","setSites","error","setError","refresh","payload","response","ok","answered","Map","map","caught","console","hostsReady","Boolean","SiteLink","props","href","name","siteName","displayName","InstalledOn","recipe","variant","color","hosts","filter","id","maybe","spacing","length","index","join","RecipeInstallForm","onClose","onInstalled","firestore","needsForm","picks","kind","setHostId","pick","createHostId","formId","setFormId","busy","setBusy","data","formRead","formsStatus","idField","rows","formDocs","truncated","formsTruncated","formOptions","form","archivedAt","$id","String","trim","sort","a","b","localeCompare","formsLoading","canInstall","handleInstall","siteSuffix","standing","sx","width","p","title","description","select","size","label","value","onChange","event","target","disabled","helperText","fullWidth","direction","onClick","RecipesCard","org","enqueueSnackbar","refreshStatus","installing","setInstalling","lastInstall","setLastInstall","entitled","handleInstalled","result","persist","lastInstallHref","header","help","anchor","contentGutterX","contentGutterY","contentBordered","severity","action","component","componentVariant","nativeButton","undefined","align","hover","maxWidth","open"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/recipes-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n planLabelGrantingFeature,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink, CardDisplay, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Drawer,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection } from 'firebase/firestore'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\nimport type {\n CrmRecipeInstallResult,\n CrmRecipeSiteStatus,\n} from '../constants/api-routes'\nimport { type CrmOrgMount, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport {\n CRM_ACTION_RECIPES,\n type CrmActionRecipe,\n type CrmActionRecipeId,\n} from '../model/crm-recipes'\nimport { useCrmApi } from './use-crm-api'\n\n/** How many of a site's forms the picker offers before it says it stopped. */\nconst FORM_OPTION_CEILING = 100\n\n/**\n * The site's list of actions — where an installed action lives and is\n * edited — at the address the plugin that keeps actions publishes, or `null`\n * for a site whose subdomain the mount could not answer or a workspace where\n * no plugin keeps actions, which is named and not linked.\n */\nfunction siteActionsHref(mount: CrmOrgMount, hostId: string): string | null {\n const subdomain = mount.siteSubdomain(hostId)\n return subdomain\n ? pluginRecordListHref('action', { orgSlug: mount.orgSlug, host: subdomain })\n : null\n}\n\n/**\n * Where a site stands with one recipe, read off the stamps the status route\n * answered. `maybe` is the third answer and the honest one: the site\n * carries actions saved before the stamp existed, any of which could have\n * started from the recipe, so the card neither lists the site as carrying\n * it nor offers the install as if to a blank site. `unread` is the answer\n * before the route has answered, or for a site it did not name.\n */\ntype SiteStanding = 'installed' | 'maybe' | 'clear' | 'unread'\n\nfunction standingOf(\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null,\n hostId: string,\n recipeId: CrmActionRecipeId,\n): SiteStanding {\n const site = status?.get(hostId)\n if (!site) return 'unread'\n if (site.installed.includes(recipeId)) return 'installed'\n return site.unstamped > 0 ? 'maybe' : 'clear'\n}\n\n/**\n * The stamps of every site of the org, read once the site list is known\n * and again after each install, so the card reports what the documents\n * say rather than what it last did.\n */\nfunction useRecipeStatus(mount: CrmOrgMount | null) {\n const api = useCrmApi(null)\n const [sites, setSites] = useState<Map<string, CrmRecipeSiteStatus> | null>(null)\n const [error, setError] = useState<string | null>(null)\n const refresh = useCallback(async () => {\n try {\n const { response, payload } = await api('recipe-status', {})\n if (!response.ok) {\n setError(payload?.error ?? 'Which sites carry a recipe could not be read.')\n return\n }\n const answered = (payload.sites ?? []) as CrmRecipeSiteStatus[]\n setSites(new Map(answered.map((site) => [site.hostId, site])))\n setError(null)\n } catch (caught) {\n console.error(caught)\n setError('Which sites carry a recipe could not be read.')\n }\n }, [api])\n const hostsReady = Boolean(mount?.hostsReady)\n useEffect(() => {\n if (hostsReady) void refresh()\n }, [hostsReady, refresh])\n return { sites, error, refresh }\n}\n\n/** A site's name, linked into its Actions page when the link is known. */\nfunction SiteLink(props: { mount: CrmOrgMount; hostId: string }) {\n const { mount, hostId } = props\n const href = siteActionsHref(mount, hostId)\n const name = mount.siteName(hostId)\n return href ? <AppLink href={href}>{name}</AppLink> : <>{name}</>\n}\nSiteLink.displayName = 'SiteLink'\n\n/** One recipe's \"Installed on\" cell: the sites that carry it, and the ones that may. */\nfunction InstalledOn(props: {\n mount: CrmOrgMount\n recipe: CrmActionRecipe\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null\n}) {\n const { mount, recipe, status } = props\n if (!status) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Reading…'}\n </Typography>\n )\n }\n const installed = mount.hosts.filter(\n (host) => standingOf(status, host.id, recipe.id) === 'installed',\n )\n const maybe = mount.hosts.filter(\n (host) => standingOf(status, host.id, recipe.id) === 'maybe',\n )\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\">\n {installed.length === 0\n ? 'Not installed on any site yet.'\n : installed.map((host, index) => (\n <Fragment key={host.id}>\n {index > 0 ? ', ' : null}\n <SiteLink mount={mount} hostId={host.id} />\n </Fragment>\n ))}\n </Typography>\n {maybe.length > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`May already have it: ${maybe.map((host) => host.name).join(', ')} — ` +\n 'actions there were saved before recipes were tracked.'}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nInstalledOn.displayName = 'InstalledOn'\n\ninterface RecipeInstallFormProps {\n recipe: CrmActionRecipe\n mount: CrmOrgMount\n status: ReadonlyMap<string, CrmRecipeSiteStatus> | null\n onClose: () => void\n onInstalled: (result: CrmRecipeInstallResult, hostId: string) => void\n}\n\n/**\n * The install drawer's body: pick the site, pick the form a recipe that\n * needs one is keyed on, confirm. Mounted fresh on every opening — the\n * Drawer unmounts it when closed — so the picks start over each time from\n * the session's site, when that site does not already carry the recipe.\n *\n * The forms are read from the picked site only once a site is picked and\n * only for a recipe that needs one, so a reader installing the other three\n * pays no read. Live forms only: an archived form collects nothing, and a\n * recipe keyed on it would never fire.\n */\nfunction RecipeInstallForm(props: RecipeInstallFormProps) {\n const { recipe, mount, status, onClose, onInstalled } = props\n const firestore = useFirestore()\n const api = useCrmApi(null)\n const needsForm = recipe.picks?.kind === 'form'\n const [hostId, setHostId] = useState(() => {\n const pick = mount.createHostId\n return pick && standingOf(status, pick, recipe.id) !== 'installed' ? pick : ''\n })\n const [formId, setFormId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const { data: formRead, status: formsStatus } = useFirestoreCollection<any>(\n () =>\n needsForm && hostId\n ? collectionCeiling(\n collection(firestore, 'hosts', hostId, 'forms'),\n FORM_OPTION_CEILING,\n )\n : null,\n [firestore, needsForm, hostId],\n { idField: '$id' },\n )\n const { rows: formDocs, truncated: formsTruncated } = ceilingedWindow<any>(\n formRead,\n FORM_OPTION_CEILING,\n )\n const formOptions = useMemo(\n () =>\n (formDocs ?? [])\n .filter((form: any) => !form.archivedAt)\n .map((form: any) => ({\n id: form.$id as string,\n name: (String(form.displayName ?? '').trim() || form.$id) as string,\n }))\n .sort((a: { name: string }, b: { name: string }) =>\n a.name.localeCompare(b.name),\n ),\n [formDocs],\n )\n const formsLoading = needsForm && Boolean(hostId) && formsStatus === 'loading'\n\n const canInstall =\n Boolean(hostId) && (!needsForm || Boolean(formId)) && !busy && !formsLoading\n\n const handleInstall = async () => {\n if (!canInstall) return\n setBusy(true)\n setError(null)\n try {\n const { response, payload } = await api('recipe-install', {\n hostId,\n recipeId: recipe.id,\n ...(formId ? { formId } : {}),\n })\n if (!response.ok) {\n setError(payload?.error ?? 'The recipe could not be installed.')\n return\n }\n onInstalled(payload as CrmRecipeInstallResult, hostId)\n } catch (caught) {\n console.error(caught)\n setError('The recipe could not be installed.')\n } finally {\n setBusy(false)\n }\n }\n\n const siteSuffix = (id: string) => {\n const standing = standingOf(status, id, recipe.id)\n if (standing === 'installed') return ' — installed'\n if (standing === 'maybe') return ' — may already have it'\n return ''\n }\n\n return (\n <Stack spacing={2} sx={{ width: 380, maxWidth: '100vw', p: 3 }}>\n <Typography variant=\"h6\">{`Install “${recipe.title}”`}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {recipe.description}\n </Typography>\n <TextField\n select\n size=\"small\"\n label=\"Site\"\n value={hostId}\n onChange={(event) => {\n setHostId(event.target.value)\n setFormId('')\n setError(null)\n }}\n disabled={busy}\n helperText={\n mount.hosts.length === 0\n ? 'This organization has no sites yet.'\n : 'The action is written into this site’s Automation → Actions.'\n }\n fullWidth\n >\n {mount.hosts.map((host) => (\n <MenuItem\n key={host.id}\n value={host.id}\n disabled={standingOf(status, host.id, recipe.id) === 'installed'}\n >\n {`${host.name}${siteSuffix(host.id)}`}\n </MenuItem>\n ))}\n </TextField>\n {needsForm ? (\n <TextField\n select\n size=\"small\"\n label=\"Form\"\n value={formId}\n onChange={(event) => setFormId(event.target.value)}\n disabled={!hostId || busy || formsLoading}\n helperText={\n !hostId\n ? 'Pick the site first; the recipe is keyed on one of its forms.'\n : formsLoading\n ? 'Loading the site’s forms…'\n : formOptions.length === 0\n ? 'This site has no live forms.'\n : formsTruncated\n ? `Showing the first ${FORM_OPTION_CEILING} forms.`\n : 'Contacts this form makes are tagged with its name.'\n }\n fullWidth\n >\n {formOptions.map((form) => (\n <MenuItem key={form.id} value={form.id}>\n {form.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The action is created enabled, exactly as the recipe defines it. ' +\n 'Change anything afterwards from the site’s Automation → Actions page.'}\n </Typography>\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canInstall}\n onClick={() => void handleInstall()}\n >\n {busy ? 'Installing…' : 'Install'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\nRecipeInstallForm.displayName = 'RecipeInstallForm'\n\nexport interface RecipesCardProps {\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Recipes\" — the CRM's ready-made automations, installed on a site from\n * the organization (AGL-2639).\n *\n * ## Why this card exists at the org level and nowhere else\n *\n * Automation is a site feature: a site's Actions page has a Recipes menu\n * that opens the editor prefilled, and the person saves what they see.\n * The org hub mounts over every site with no editor of its own, so it had\n * no recipes at all, and an org running five sites installed \"Welcome a\n * new lead\" five times by hand. This card is the org's door: every recipe,\n * the sites that already carry it, and Install, which writes the action\n * into the site the reader picks through the server — the server stamps\n * the recipe, refuses a site outside the org, and refuses a second copy.\n * Under a site the section does not mount it; the site's own menu is the\n * right door there.\n *\n * ## \"Installed on\" reads the stamp, and says when it cannot\n *\n * The status route answers, per site, the recipes a live action is stamped\n * with and how many live actions carry no stamp. A site with any of the\n * latter is named as one that MAY already have the recipe rather than\n * listed as clear: those actions were saved before the stamp existed and\n * the card cannot tell a hand-built welcome from an installed one.\n *\n * ## Creation opens a drawer\n *\n * Install is a button per row; the picks — the site, and for Tag by form\n * one of that site's forms — live in a drawer, never above the list.\n */\nexport function RecipesCard(props: RecipesCardProps) {\n const { org } = props\n const mount = useCrmOrgMount()\n const { enqueueSnackbar } = useSnackbar()\n const status = useRecipeStatus(mount)\n const refreshStatus = status.refresh\n const [installing, setInstalling] = useState<CrmActionRecipe | null>(null)\n const [lastInstall, setLastInstall] = useState<{\n hostId: string\n name: string\n } | null>(null)\n\n const entitled = checkEntitlement(org, 'actions')\n const canInstall = Boolean(mount?.hostsReady) && entitled\n\n const handleInstalled = useCallback(\n (result: CrmRecipeInstallResult, hostId: string) => {\n setInstalling(null)\n setLastInstall({ hostId, name: result.name })\n enqueueSnackbar(\n `Installed “${result.name}” on ${mount?.siteName(hostId) ?? hostId}`,\n { variant: 'success', persist: false },\n )\n void refreshStatus()\n },\n [enqueueSnackbar, mount, refreshStatus],\n )\n\n // Mounted by the section only beneath the org hub's provider; the hooks\n // above run either way, so the render order never depends on the mount.\n if (!mount) return null\n\n const lastInstallHref = lastInstall ? siteActionsHref(mount, lastInstall.hostId) : null\n return (\n <CardDisplay\n header={'Recipes'}\n help={pluginDocsHelp('crmSettings', { anchor: '#recipes' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Ready-made automations for the CRM. Install one on a site and the ' +\n 'action is written into that site’s Automation → Actions as the ' +\n 'recipe defines it, where it can be edited like any other action.'}\n </Typography>\n {lastInstall ? (\n <Alert\n severity=\"success\"\n onClose={() => setLastInstall(null)}\n action={\n lastInstallHref ? (\n // A link dressed as a button: the anchor carries its own\n // href, so it keeps the link role a navigation door has.\n <Button\n component={AppLink as any}\n {...({\n componentVariant: 'naked',\n nativeButton: false,\n } as any)}\n href={lastInstallHref}\n size=\"small\"\n color=\"inherit\"\n >\n {'Open Automation → Actions'}\n </Button>\n ) : undefined\n }\n >\n {`Installed “${lastInstall.name}” on ${mount.siteName(lastInstall.hostId)}.`}\n </Alert>\n ) : null}\n {status.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {status.error}\n </Typography>\n ) : null}\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Recipe'}</TableCell>\n <TableCell>{'Installed on'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {CRM_ACTION_RECIPES.map((recipe) => (\n <TableRow key={recipe.id} hover>\n <TableCell sx={{ maxWidth: 360 }}>\n <Typography variant=\"body2\">{recipe.title}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {recipe.description}\n </Typography>\n </TableCell>\n <TableCell>\n <InstalledOn mount={mount} recipe={recipe} status={status.sites} />\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 120 }}>\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={!canInstall}\n onClick={() => setInstalling(recipe)}\n >\n {'Install'}\n <SrOnly>{` ${recipe.title}`}</SrOnly>\n </Button>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {!entitled ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The actions builder requires the ${planLabelGrantingFeature('actions') ?? 'Pro'} ` +\n 'plan — see Billing to upgrade.'}\n </Typography>\n ) : null}\n {mount.hostsReady && mount.hosts.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nowhere to install a recipe.'}\n </Typography>\n ) : null}\n </Stack>\n <Drawer anchor=\"right\" open={Boolean(installing)} onClose={() => setInstalling(null)}>\n {installing ? (\n <RecipeInstallForm\n recipe={installing}\n mount={mount}\n status={status.sites}\n onClose={() => setInstalling(null)}\n onInstalled={handleInstalled}\n />\n ) : null}\n </Drawer>\n </CardDisplay>\n )\n}\nRecipesCard.displayName = 'RecipesCard'\n\nexport default RecipesCard\n"],"names":["checkEntitlement","planLabelGrantingFeature","pluginDocsHelp","pluginRecordListHref","AppLink","CardDisplay","SrOnly","ScrollTable","useSnackbar","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","Alert","Button","Drawer","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","Fragment","useCallback","useEffect","useMemo","useState","useCrmOrgMount","CRM_ACTION_RECIPES","useCrmApi","FORM_OPTION_CEILING","siteActionsHref","mount","hostId","subdomain","siteSubdomain","orgSlug","host","standingOf","status","recipeId","site","get","installed","includes","unstamped","useRecipeStatus","api","sites","setSites","error","setError","refresh","payload","response","ok","answered","Map","map","caught","console","hostsReady","Boolean","SiteLink","props","href","name","siteName","displayName","InstalledOn","recipe","variant","color","hosts","filter","id","maybe","spacing","length","index","join","RecipeInstallForm","onClose","onInstalled","firestore","needsForm","picks","kind","setHostId","pick","createHostId","formId","setFormId","busy","setBusy","data","formRead","formsStatus","idField","rows","formDocs","truncated","formsTruncated","formOptions","form","archivedAt","$id","String","trim","sort","a","b","localeCompare","formsLoading","canInstall","handleInstall","siteSuffix","standing","sx","width","maxWidth","p","title","description","select","size","label","value","onChange","event","target","disabled","helperText","fullWidth","direction","onClick","RecipesCard","org","enqueueSnackbar","refreshStatus","installing","setInstalling","lastInstall","setLastInstall","entitled","handleInstalled","result","persist","lastInstallHref","header","help","anchor","contentGutterX","contentGutterY","contentBordered","severity","action","component","componentVariant","nativeButton","undefined","align","hover","open"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-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 { Section } 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 where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\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 {\n activityLeaderboard,\n type ActivityLeaderboardRow,\n deltaPercent,\n} from '../../model/crm-reports'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {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 {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${caption}; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","activityLeaderboard","deltaPercent","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","kinds","tasksDone","disabled","spacing","direction","sx","flexWrap","deltaPct","note","href","section","severity","title","size","column","index","align","variant","color","some","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-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 { Section } 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 where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\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 {\n activityLeaderboard,\n type ActivityLeaderboardRow,\n deltaPercent,\n} from '../../model/crm-reports'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {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 {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${caption}; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","activityLeaderboard","deltaPercent","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","kinds","tasksDone","disabled","spacing","useFlexGap","direction","sx","flexWrap","deltaPct","note","href","section","severity","title","size","column","index","align","variant","color","some","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-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 { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\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 { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } 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 { bucketByWeek, currencyOfDeals } from '../../model/crm-reports'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","bucketByWeek","currencyOfDeals","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-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 { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\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 { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } 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 { bucketByWeek, currencyOfDeals } from '../../model/crm-reports'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","bucketByWeek","currencyOfDeals","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","useFlexGap","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-trend-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { getCountFromServer, query, Timestamp, where } from 'firebase/firestore'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { type AggregateRead, useAggregateRead } from './use-aggregate-read'\nimport { deltaPercent, weekBuckets } from '../../model/crm-reports'\n\nexport interface ContactsTrendCardProps {\n report: CrmReportScope\n /** The section's one count of every contact this reader may see. */\n totalContacts: AggregateRead<number>\n}\n\ninterface ContactsTrend {\n /** New contacts in the period — the sum of the weeks. */\n current: number\n /** New contacts in the period before it. */\n previous: number\n weekly: Array<{ start: number; end: number; count: number }>\n}\n\n/**\n * New contacts over the period, against the period before, week by week\n * (AGL-2604).\n *\n * ## Every number here is a server count\n *\n * One `getCountFromServer` per week of the period and one for the previous\n * period — fourteen reads for ninety days, billed as index entries rather\n * than documents — and NO document is downloaded to be counted. The period\n * figure is the sum of its weeks rather than a fifteenth read, so the tile\n * and the bars cannot disagree. The contacts list's head-count learned the\n * same lesson (AGL-1706): a count taken from a capped listener saturates at\n * the cap, and the org this page is for is the one with more contacts than\n * any listener should carry.\n *\n * The range is on `createdAt`, the timestamp every contact writer stamps —\n * the server upsert with `serverTimestamp()`, the console's add-by-hand with\n * a `Date` — so \"new\" means first captured, not last touched.\n */\nexport function ContactsTrendCard(props: ContactsTrendCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const trend = useAggregateRead<ContactsTrend>(() => {\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const createdBetween = (from: number, to: number) =>\n getCountFromServer(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(from)),\n where('createdAt', '<', Timestamp.fromMillis(to)),\n ),\n ).then((snapshot) => snapshot.data().count)\n const buckets = weekBuckets(range.from, range.to)\n return Promise.all([\n createdBetween(range.previousFrom, range.previousTo),\n ...buckets.map((bucket) => createdBetween(bucket.start, bucket.end)),\n ]).then(([previous, ...weekly]) => ({\n previous,\n current: weekly.reduce((sum, count) => sum + count, 0),\n weekly: buckets.map((bucket, index) => ({\n ...bucket,\n count: weekly[index] ?? 0,\n })),\n }))\n }, [firestore, scope, tokens, range], { cacheKey: reportCacheKey(report, 'contacts:trend') })\n\n const figures = trend.value\n return (\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('crmReports', {\n anchor: '#contacts',\n excerpt:\n 'New contacts in the period against the period before it, week ' +\n 'by week, and how many contacts this site can see in all. Counted ' +\n 'on the server by when each contact was first captured.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'New contacts'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={\n figures\n ? figures.previous\n ? `${figures.previous.toLocaleString()} in the previous period`\n : 'none in the previous period'\n : undefined\n }\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Total contacts'}\n value={\n totalContacts.value === null\n ? null\n : totalContacts.value.toLocaleString()\n }\n note={'visible to this site'}\n href={routes.section('contacts')}\n />\n </Stack>\n {trend.status === 'error' || totalContacts.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The contact counts could not be read. The rest of the page is unaffected.'}\n </Alert>\n ) : null}\n <Section title={'New contacts per week'}>\n <ReportBarChart\n labels={(figures?.weekly ?? []).map((week) => weekLabel(week.start))}\n series={[\n {\n name: 'New contacts',\n color: 'primary.main',\n values: (figures?.weekly ?? []).map((week) => week.count),\n },\n ]}\n emptyText={\n trend.status === 'loading' ? 'Counting…' : 'Nothing in this period.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each bar is seven days from the start of the period; the last ' +\n 'may be shorter.'}\n </Typography>\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsTrendCard.displayName = 'ContactsTrendCard'\n\nexport default ContactsTrendCard\n"],"names":["pluginDocsHelp","CardDisplay","Section","Alert","Stack","Typography","getCountFromServer","query","Timestamp","where","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","deltaPercent","weekBuckets","ContactsTrendCard","props","report","totalContacts","scope","tokens","range","routes","firestore","trend","contacts","createdBetween","from","to","fromMillis","then","snapshot","data","count","buckets","Promise","all","previousFrom","previousTo","map","bucket","start","end","previous","weekly","current","reduce","sum","index","cacheKey","figures","value","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","deltaPct","note","undefined","href","section","status","severity","title","labels","week","series","name","color","values","emptyText","variant","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,OAAO,QAAQ,6DAA4D;AACpF,SAASC,KAAK,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACxD,SAASC,kBAAkB,EAAEC,KAAK,EAAEC,SAAS,EAAEC,KAAK,QAAQ,qBAAoB;AAChF,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,SAAS,QAAQ,qBAAiB;AAC3C,SAEEC,cAAc,EACdC,gBAAgB,EAChBC,eAAe,QACV,oBAAgB;AACvB,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAA6BC,gBAAgB,QAAQ,0BAAsB;AAC3E,SAASC,YAAY,EAAEC,WAAW,QAAQ,6BAAyB;AAgBnE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;;IAC7D,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAE,GAAGF;IAClC,MAAM,EAAEG,KAAK,EAAEC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAE,GAAGL;IACzC,MAAMM,YAAYlB;IAElB,MAAMmB,QAAQZ,iBAAgC;QAC5C,MAAMa,WAAWhB,iBAAiBc,WAAWJ,OAAO;QACpD,MAAMO,iBAAiB,CAACC,MAAcC,KACpC3B,mBACEC,MACEuB,aACGf,gBAAgBU,SACnBhB,MAAM,aAAa,MAAMD,UAAU0B,UAAU,CAACF,QAC9CvB,MAAM,aAAa,KAAKD,UAAU0B,UAAU,CAACD,OAE/CE,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK;QAC5C,MAAMC,UAAUpB,YAAYO,MAAMM,IAAI,EAAEN,MAAMO,EAAE;QAChD,OAAOO,QAAQC,GAAG,CAAC;YACjBV,eAAeL,MAAMgB,YAAY,EAAEhB,MAAMiB,UAAU;eAChDJ,QAAQK,GAAG,CAAC,CAACC,SAAWd,eAAec,OAAOC,KAAK,EAAED,OAAOE,GAAG;SACnE,EAAEZ,IAAI,CAAC,CAAC,CAACa,UAAU,GAAGC,OAAO,GAAM,CAAA;gBAClCD;gBACAE,SAASD,OAAOE,MAAM,CAAC,CAACC,KAAKd,QAAUc,MAAMd,OAAO;gBACpDW,QAAQV,QAAQK,GAAG,CAAC,CAACC,QAAQQ;wBAEpBJ;2BAF+B,aACnCJ;wBACHP,KAAK,GAAEW,gBAAAA,MAAM,CAACI,MAAM,YAAbJ,gBAAiB;;;YAE5B,CAAA;IACF,GAAG;QAACrB;QAAWJ;QAAOC;QAAQC;KAAM,EAAE;QAAE4B,UAAUzC,eAAeS,QAAQ;IAAkB;IAE3F,MAAMiC,UAAU1B,MAAM2B,KAAK;IAC3B,qBACE,KAACvD;QACCwD,QAAQ;QACRC,MAAM1D,eAAe,cAAc;YACjC2D,QAAQ;YACRC,SACE,mEACA,sEACA;QACJ;QACAC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC1D;YAAM2D,SAAS;;8BACd,MAAC3D;oBAAM4D,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAClD;4BACCmD,OAAO;4BACPX,OAAOD,UAAUA,QAAQL,OAAO,CAACkB,cAAc,KAAK;4BACpDC,UAAUd,UAAUrC,aAAaqC,QAAQL,OAAO,EAAEK,QAAQP,QAAQ,IAAI;4BACtEsB,MACEf,UACIA,QAAQP,QAAQ,GACd,GAAGO,QAAQP,QAAQ,CAACoB,cAAc,GAAG,uBAAuB,CAAC,GAC7D,gCACFG;4BAENC,MAAM7C,OAAO8C,OAAO,CAAC;;sCAEvB,KAACzD;4BACCmD,OAAO;4BACPX,OACEjC,cAAciC,KAAK,KAAK,OACpB,OACAjC,cAAciC,KAAK,CAACY,cAAc;4BAExCE,MAAM;4BACNE,MAAM7C,OAAO8C,OAAO,CAAC;;;;gBAGxB5C,MAAM6C,MAAM,KAAK,WAAWnD,cAAcmD,MAAM,KAAK,wBACpD,KAACvE;oBAAMwE,UAAS;8BACb;qBAED;8BACJ,MAACzE;oBAAQ0E,OAAO;;sCACd,KAACjE;4BACCkE,QAAQ,SAACtB,2BAAAA,QAASN,MAAM,mBAAI,EAAE,EAAEL,GAAG,CAAC,CAACkC,OAASlE,UAAUkE,KAAKhC,KAAK;4BAClEiC,QAAQ;gCACN;oCACEC,MAAM;oCACNC,OAAO;oCACPC,QAAQ,UAAC3B,2BAAAA,QAASN,MAAM,oBAAI,EAAE,EAAEL,GAAG,CAAC,CAACkC,OAASA,KAAKxC,KAAK;gCAC1D;6BACD;4BACD6C,WACEtD,MAAM6C,MAAM,KAAK,YAAY,cAAc;;sCAG/C,KAACrE;4BAAW+E,SAAQ;4BAAUH,OAAM;sCACjC,mEACC;;;;;;;AAMd;AACA7D,kBAAkBiE,WAAW,GAAG;AAEhC,eAAejE,kBAAiB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-trend-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { getCountFromServer, query, Timestamp, where } from 'firebase/firestore'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { type AggregateRead, useAggregateRead } from './use-aggregate-read'\nimport { deltaPercent, weekBuckets } from '../../model/crm-reports'\n\nexport interface ContactsTrendCardProps {\n report: CrmReportScope\n /** The section's one count of every contact this reader may see. */\n totalContacts: AggregateRead<number>\n}\n\ninterface ContactsTrend {\n /** New contacts in the period — the sum of the weeks. */\n current: number\n /** New contacts in the period before it. */\n previous: number\n weekly: Array<{ start: number; end: number; count: number }>\n}\n\n/**\n * New contacts over the period, against the period before, week by week\n * (AGL-2604).\n *\n * ## Every number here is a server count\n *\n * One `getCountFromServer` per week of the period and one for the previous\n * period — fourteen reads for ninety days, billed as index entries rather\n * than documents — and NO document is downloaded to be counted. The period\n * figure is the sum of its weeks rather than a fifteenth read, so the tile\n * and the bars cannot disagree. The contacts list's head-count learned the\n * same lesson (AGL-1706): a count taken from a capped listener saturates at\n * the cap, and the org this page is for is the one with more contacts than\n * any listener should carry.\n *\n * The range is on `createdAt`, the timestamp every contact writer stamps —\n * the server upsert with `serverTimestamp()`, the console's add-by-hand with\n * a `Date` — so \"new\" means first captured, not last touched.\n */\nexport function ContactsTrendCard(props: ContactsTrendCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const trend = useAggregateRead<ContactsTrend>(() => {\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const createdBetween = (from: number, to: number) =>\n getCountFromServer(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(from)),\n where('createdAt', '<', Timestamp.fromMillis(to)),\n ),\n ).then((snapshot) => snapshot.data().count)\n const buckets = weekBuckets(range.from, range.to)\n return Promise.all([\n createdBetween(range.previousFrom, range.previousTo),\n ...buckets.map((bucket) => createdBetween(bucket.start, bucket.end)),\n ]).then(([previous, ...weekly]) => ({\n previous,\n current: weekly.reduce((sum, count) => sum + count, 0),\n weekly: buckets.map((bucket, index) => ({\n ...bucket,\n count: weekly[index] ?? 0,\n })),\n }))\n }, [firestore, scope, tokens, range], { cacheKey: reportCacheKey(report, 'contacts:trend') })\n\n const figures = trend.value\n return (\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('crmReports', {\n anchor: '#contacts',\n excerpt:\n 'New contacts in the period against the period before it, week ' +\n 'by week, and how many contacts this site can see in all. Counted ' +\n 'on the server by when each contact was first captured.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'New contacts'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={\n figures\n ? figures.previous\n ? `${figures.previous.toLocaleString()} in the previous period`\n : 'none in the previous period'\n : undefined\n }\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Total contacts'}\n value={\n totalContacts.value === null\n ? null\n : totalContacts.value.toLocaleString()\n }\n note={'visible to this site'}\n href={routes.section('contacts')}\n />\n </Stack>\n {trend.status === 'error' || totalContacts.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The contact counts could not be read. The rest of the page is unaffected.'}\n </Alert>\n ) : null}\n <Section title={'New contacts per week'}>\n <ReportBarChart\n labels={(figures?.weekly ?? []).map((week) => weekLabel(week.start))}\n series={[\n {\n name: 'New contacts',\n color: 'primary.main',\n values: (figures?.weekly ?? []).map((week) => week.count),\n },\n ]}\n emptyText={\n trend.status === 'loading' ? 'Counting…' : 'Nothing in this period.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each bar is seven days from the start of the period; the last ' +\n 'may be shorter.'}\n </Typography>\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsTrendCard.displayName = 'ContactsTrendCard'\n\nexport default ContactsTrendCard\n"],"names":["pluginDocsHelp","CardDisplay","Section","Alert","Stack","Typography","getCountFromServer","query","Timestamp","where","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","deltaPercent","weekBuckets","ContactsTrendCard","props","report","totalContacts","scope","tokens","range","routes","firestore","trend","contacts","createdBetween","from","to","fromMillis","then","snapshot","data","count","buckets","Promise","all","previousFrom","previousTo","map","bucket","start","end","previous","weekly","current","reduce","sum","index","cacheKey","figures","value","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","useFlexGap","direction","sx","flexWrap","label","toLocaleString","deltaPct","note","undefined","href","section","status","severity","title","labels","week","series","name","color","values","emptyText","variant","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadStatusPicklist } from '../../hooks/use-lead-status-picklist'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\nimport { deltaPercent, leadFunnel } from '../../model/crm-reports'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entries in `cloud/firebase-firestore.indexes.json`\n * — the descending one for the window, the ascending one for the counts,\n * which name no order (AGL-3443).\n */\nexport function leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => leadFunnel(window.rows), [window])\n // Each status counted by its MEANING, named as the org names it (AGL-3512).\n const { picklist: statuses } = useLeadStatusPicklist(report.scope[1])\n const statusName = (status: Aglyn.CrmLeadStatus) => Aglyn.crmLeadStatusLabelFor(statuses, status)\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n statusName(leadStatus),\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={statusName('qualified')}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={statusName('unqualified')}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: statusName(leadStatus),\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","useLeadStatusPicklist","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","deltaPercent","leadFunnel","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","merged","docs","document","$id","sites","subheader","period","routes","funnel","picklist","statuses","statusName","crmLeadStatusLabelFor","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","sx","flexWrap","deltaPct","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","key","emptyText"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadStatusPicklist } from '../../hooks/use-lead-status-picklist'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\nimport { deltaPercent, leadFunnel } from '../../model/crm-reports'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entries in `cloud/firebase-firestore.indexes.json`\n * — the descending one for the window, the ascending one for the counts,\n * which name no order (AGL-3443).\n */\nexport function leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => leadFunnel(window.rows), [window])\n // Each status counted by its MEANING, named as the org names it (AGL-3512).\n const { picklist: statuses } = useLeadStatusPicklist(report.scope[1])\n const statusName = (status: Aglyn.CrmLeadStatus) => Aglyn.crmLeadStatusLabelFor(statuses, status)\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n statusName(leadStatus),\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={statusName('qualified')}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={statusName('unqualified')}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: statusName(leadStatus),\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","useLeadStatusPicklist","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","deltaPercent","leadFunnel","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","merged","docs","document","$id","sites","subheader","period","routes","funnel","picklist","statuses","statusName","crmLeadStatusLabelFor","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","useFlexGap","sx","flexWrap","deltaPct","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","key","emptyText"],"mappings":"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"}
|