@aglyn/plugins-crm 1.0.0-beta.230 → 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 -13
- 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-next-activity-column.d.ts +1 -13
- package/src/lib/components/crm-next-activity-column.js +1 -19
- package/src/lib/components/crm-next-activity-column.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-status-options.d.ts +2 -14
- package/src/lib/components/lead-status-options.js +1 -13
- package/src/lib/components/lead-status-options.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/lead-unqualify-dialog.d.ts +1 -2
- package/src/lib/components/lead-unqualify-dialog.js +2 -1
- package/src/lib/components/lead-unqualify-dialog.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/settings-section.d.ts +18 -0
- package/src/lib/components/settings-section.js +97 -1
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +2 -2
- package/src/lib/constants/company-filters.js +1 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +3 -3
- package/src/lib/constants/contact-filters.js +2 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +3 -3
- package/src/lib/constants/deal-filters.js +1 -1
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +1 -1
- package/src/lib/constants/field-list-query.js +1 -1
- package/src/lib/constants/field-list-query.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contacts-list-seed.d.ts +1 -1
- package/src/lib/model/contacts-list-seed.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -4
- package/src/lib/model/crm-list-query.js +2 -2
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +13 -0
- package/src/lib/model/crm-next-activity.js +19 -0
- package/src/lib/model/crm-next-activity.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +6 -5
- package/src/lib/model/lead-filters.js +3 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-status-choices.d.ts +32 -0
- package/src/lib/model/lead-status-choices.js +31 -0
- package/src/lib/model/lead-status-choices.js.map +1 -0
- package/src/lib/model/task-views.d.ts +2 -2
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/person-records.d.ts +27 -0
- package/src/lib/server/person-records.js +127 -4
- package/src/lib/server/person-records.js.map +1 -1
|
@@ -84,6 +84,7 @@ import { useContactCampaignEmails } from "./use-contact-campaign-emails.js";
|
|
|
84
84
|
}),
|
|
85
85
|
title: (_interaction_summary = interaction.summary) != null ? _interaction_summary : label,
|
|
86
86
|
meta: /*#__PURE__*/ _jsxs(Stack, {
|
|
87
|
+
useFlexGap: true,
|
|
87
88
|
direction: "row",
|
|
88
89
|
spacing: 1,
|
|
89
90
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-timeline-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 type {\n ContactCampaignEmail,\n ContactInteraction,\n ContactSource,\n CrmActivityRow,\n} from '@aglyn/aglyn'\nimport { CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiAccountCircleOutline,\n mdiAccountEditOutline,\n mdiAccountPlusOutline,\n mdiApi,\n mdiBullhornOutline,\n mdiCalendarCheckOutline,\n mdiCartOutline,\n mdiEmailNewsletter,\n mdiFileImportOutline,\n mdiFormSelect,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { Button, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport { useParams } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { contactInteractionHref, INTERACTION_LINK_LABELS } from '../model/contact-interaction-links'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport { ActivityRow } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n useCanEditActivity,\n} from './activity-queries'\nimport {\n TimelineEntry,\n TimelineExpandAllButton,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\nimport { LogActivityDialog } from './log-activity-dialog'\nimport { useContactCampaignEmails } from './use-contact-campaign-emails'\n\n/**\n * One glyph per capture door. Typed against the source union so a door added\n * to it cannot render as the placeholder glyph.\n */\nconst SOURCE_ICONS: Record<ContactSource, { path: string }> = {\n form: mdiFormSelect,\n member: mdiAccountPlusOutline,\n order: mdiCartOutline,\n booking: mdiCalendarCheckOutline,\n newsletter: mdiEmailNewsletter,\n api: mdiApi,\n manual: mdiAccountEditOutline,\n import: mdiFileImportOutline,\n account: mdiAccountCircleOutline,\n}\n\n/**\n * One thing the platform recorded: the door it came through, what the door\n * wrote, when, and the page the person was on.\n *\n * Read-only by nature — nobody logged it, so nobody edits it. The entry\n * point sits beside the timestamp rather than in the summary, the way the\n * contacts drawer draws it: the summary is the sentence the door wrote, the\n * path is a fact the door recorded, and a long path on the sentence's line\n * pushes out the thing the row is about.\n *\n * Everything it holds fits the collapsed row, so it has nothing to open.\n */\nfunction CapturedRow(props: {\n interaction: ContactInteraction\n nowMs: number\n /** The console page the door's record is read on, when it left one. */\n href: string | null\n}) {\n const { interaction, nowMs, href } = props\n const source = interaction.type\n const label = CONTACT_SOURCE_LABELS[source] ?? String(source)\n const icon = SOURCE_ICONS[source]\n const when = new Date(interaction.atMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={icon?.path} />}\n chips={\n <>\n <Chip label={label} size=\"small\" />\n <Chip label=\"Captured\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={interaction.summary ?? label}\n meta={\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap', rowGap: 0.25 }}\n >\n <Tooltip title={when.toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {interaction.path\n ? `${Aglyn.activityTimeLabel(interaction.atMs, nowMs)} · ${interaction.path}`\n : Aglyn.activityTimeLabel(interaction.atMs, nowMs)}\n </Typography>\n </Tooltip>\n {/*\n The record the door left, where the console reads it (AGL-2622):\n the submission in the Inbox reader, the order in its dialog. A door\n that left nothing to open — a newsletter opt-in, an import — has no\n link, and the caption is the whole entry.\n */}\n {href ? (\n <Typography variant=\"caption\">\n <AppLink href={href}>\n {INTERACTION_LINK_LABELS[source] ?? 'Open'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n }\n />\n )\n}\nCapturedRow.displayName = 'CapturedRow'\n\n/**\n * One campaign email the platform sent this person (AGL-2616): which email,\n * and what became of it — `sent · delivered · opened ×2 · clicked`.\n *\n * Read-only like a captured row, and drawn apart from BOTH the captured and\n * the logged rows: its glyph is the campaign's, never the envelope a logged\n * `email` activity carries, and its chips say \"Campaign\" and \"Sent\" where\n * the others say \"Captured\" and \"Logged\". The name links to the email's own\n * report when the page can address it; an email the team has deleted, or\n * one a sibling site sent, keeps its name and loses the link.\n *\n * The delivery line stays on the collapsed row beside the name, so it has\n * nothing to open.\n */\nfunction CampaignEmailRow(props: {\n email: ContactCampaignEmail\n href: string | null\n nowMs: number\n}) {\n const { email, href, nowMs } = props\n const name = email.campaignName ?? email.subject ?? 'Campaign email'\n const summary = Aglyn.campaignEmailSummary(email).join(' · ')\n const when = new Date(email.sentAtMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={mdiBullhornOutline.path} />}\n chips={\n <>\n <Chip label=\"Campaign\" size=\"small\" />\n <Chip label=\"Sent\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={\n <>\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n {` · ${summary}`}\n </>\n }\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {email.subject && email.subject !== name\n ? `${Aglyn.activityTimeLabel(email.sentAtMs, nowMs)} · ${email.subject}`\n : Aglyn.activityTimeLabel(email.sentAtMs, nowMs)}\n </Typography>\n </Tooltip>\n }\n />\n )\n}\nCampaignEmailRow.displayName = 'CampaignEmailRow'\n\nexport interface ContactTimelineCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n contactId: string\n /**\n * The RAW contact document, as the page's listener hands it back. The card\n * resolves the viewing group and reads that group's facet itself, so a\n * page cannot hand it another holder's timeline by flattening the row\n * through the wrong group.\n */\n contact: Record<string, unknown> | null | undefined\n /**\n * Where a campaign entry links to — the email's report — or `null` for\n * one the page cannot address. Handed in rather than built here because\n * the report lives on another surface whose path is the MOUNT's to know:\n * a site's record page reaches its own Emails hub, an org-level mount\n * holds the org's site list and can reach every site's. No builder means\n * no links, never a guessed one.\n */\n campaignHref?: (email: ContactCampaignEmail) => string | null\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The history still draws; logging an activity stands\n * locked, and one already logged cannot be edited.\n */\n suiteLocked?: boolean\n}\n\n/**\n * A person's whole history in one newest-first stream (AGL-2600): what the\n * platform CAPTURED — a form, an order, a booking, a sign-up — what a person\n * LOGGED beside it — a call, an email, a meeting, a note — and, since\n * AGL-2616, the campaign mail the platform SENT them and what became of it.\n *\n * ## Why one stream\n *\n * The histories answer one question, \"what has happened with this person\",\n * and a page showing them as separate lists asks the reader to interleave\n * them by eye. The merge is `mergeContactTimeline`, pure and specced: newest\n * first, stable at a tie, an undated entry last, a campaign at the instant\n * it was sent.\n *\n * Each entry says which it is. A captured entry carries the door's label and\n * a \"Captured\" chip and offers no controls; a logged entry carries its kind\n * and a \"Logged\" chip, and its author may edit or delete it; a campaign\n * entry carries \"Campaign\" and \"Sent\", links to the email's report, and is\n * nobody's to edit.\n *\n * ## What is read\n *\n * The captured side comes off the contact document the page already holds\n * — THIS group's facet, filtered to the sites the group may see, the same\n * projection the contacts list makes — and costs no read. The logged side is\n * the bounded listener every activity surface uses, filtered to this\n * contact, a hundred at a time. The campaign side is one request to\n * `crm/contact-email-history`, which reads down this person's own delivery\n * log, capped, and never a campaign's recipients. The facet's history is\n * capped at fifty by the platform, so past the window the stream is the\n * logged side alone.\n *\n * ## How it reads\n *\n * Every entry is a collapsed row that opens in place, paged by the\n * console's footer over the merged stream; a page turned past the logged\n * window widens it by the next hundred. **Expand all** and **Log activity**\n * sit in the card header, and edit and delete stay on the entry they act on.\n */\nexport function ContactTimelineCard(props: ContactTimelineCardProps) {\n const { hostId, org, contactId, contact, campaignHref, suiteLocked = false } = props\n const scope = useActivityScope(hostId, org)\n /*\n * THIS holder's captured history, and only the visits this group may see.\n * A read off the top of the document, or of another group's facet, would\n * be another business's records — the disclosure the facet shape ended.\n * At the organization level (AGL-2630) the holder is the person's own\n * primary one, the group the record page reads the profile through.\n */\n const consentGroup = useMemo(\n () =>\n scope.consentGroup ??\n contactPrimaryGroup(contact, (org ?? null) as Record<string, unknown> | null),\n [scope.consentGroup, contact, org],\n )\n const interactions = useMemo(() => {\n const facet = Aglyn.readContactFacet(contact, consentGroup.groupId)\n return Aglyn.interactionsForGroup(facet.interactions, consentGroup.hostIds)\n }, [contact, consentGroup])\n const link = useMemo<ActivityRecordLink>(() => ({ contactId }), [contactId])\n const activities = useActivityWindow(scope, link)\n /*\n * Where a captured entry's record is READ (AGL-2622). The site console's\n * two route params are already in the URL this page is on, so the link is\n * built from them rather than from two document reads per timeline. Only\n * an interaction on THIS site links: a sibling site's order is that\n * site's record, read on that site's console, and a link built with this\n * site's segment would 404 or — worse — open the wrong site's row under\n * the same id. An interaction with no site recorded predates the stamp\n * and is left unlinked rather than guessed at. Off a site — the org-level\n * mount — there is no segment to build with, and nothing links.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext = useMemo(\n () =>\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null,\n [params?.orgSlug, params?.host],\n )\n const interactionHref = useCallback(\n (interaction: ContactInteraction): string | null =>\n siteContext && interaction.hostId === hostId\n ? contactInteractionHref(interaction, siteContext)\n : null,\n [siteContext, hostId],\n )\n // The campaign history is asked of a site: the mounted one, or the holder's.\n const campaigns = useContactCampaignEmails(\n hostId ?? (consentGroup.hostId || null),\n contactId,\n )\n // One member read for the whole stream, however many rows it has.\n const canEdit = useCanEditActivity(scope.orgId)\n const entries = useMemo(\n () =>\n Aglyn.mergeContactTimeline(interactions, activities.rows, campaigns.emails),\n [interactions, activities.rows, campaigns.emails],\n )\n // One clock for every row of one paint, so two rows logged a second apart\n // cannot read \"just now\" and \"1 min ago\" from being rendered across a\n // minute boundary.\n const nowMs = Date.now()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editing, setEditing] = useState<CrmActivityRow | null>(null)\n const openNew = useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n const openEdit = useCallback((activity: CrmActivityRow) => {\n setEditing(activity)\n setDialogOpen(true)\n }, [])\n const close = useCallback(() => setDialogOpen(false), [])\n\n const loggedChip = useMemo(\n () => <Chip label=\"Logged\" size=\"small\" variant=\"outlined\" />,\n [],\n )\n const expansion = useTimelineExpansion()\n const pages = useTimelinePages(entries.length, {\n hasMore: activities.hasMore,\n showMore: activities.showMore,\n loading: activities.status === 'loading',\n })\n const logButton = suiteLocked ? (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'Log activity'}\n </CrmSuiteLockedButton>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n )\n\n return (\n <>\n <CardDisplay\n header={'Timeline'}\n help={pluginDocsHelp('contactActivities', { anchor: '#four-kinds-of-history' })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <TimelineExpandAllButton expansion={expansion} disabled={!entries.length} />\n {logButton}\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'Logged activity could not be loaded; what follows is what the platform captured.'}\n </Typography>\n ) : null}\n {campaigns.status === 'error' || campaigns.lookupFailed ? (\n // \"Unknown\", not \"none\": an empty campaign history and one that\n // could not be read lead a reader to opposite conclusions.\n <Typography variant=\"body2\" color=\"error\">\n {'Campaign email could not be loaded; the stream below may be missing the mail this person was sent.'}\n </Typography>\n ) : null}\n {entries.length === 0 ? (\n campaigns.status === 'loading' ? (\n // The campaign history is one request on mount; an empty state\n // before it answers would read as \"none\" for a person who was mailed.\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'No history yet'}\n description={'What this person does on the site, the campaigns they are sent, and what you log about them, shows here.'}\n action={logButton}\n />\n )\n ) : (\n pages.slice(entries).map((entry) =>\n entry.kind === 'captured' ? (\n <CapturedRow\n key={entry.key}\n interaction={entry.interaction}\n nowMs={nowMs}\n href={interactionHref(entry.interaction)}\n />\n ) : entry.kind === 'campaign' ? (\n <CampaignEmailRow\n key={entry.key}\n email={entry.email}\n href={campaignHref ? campaignHref(entry.email) : null}\n nowMs={nowMs}\n />\n ) : (\n <ActivityRow\n key={entry.key}\n activity={entry.activity}\n scope={scope}\n onEdit={openEdit}\n subject={loggedChip}\n nowMs={nowMs}\n editable={!suiteLocked && canEdit(entry.activity)}\n expanded={expansion.isExpanded(entry.key)}\n onToggle={() => expansion.toggle(entry.key)}\n />\n ),\n )\n )}\n {entries.length ? <ListPagination {...pages.footer} /> : null}\n </Stack>\n </CardDisplay>\n <LogActivityDialog\n open={dialogOpen}\n onClose={close}\n scope={scope}\n link={link}\n activity={editing}\n />\n </>\n )\n}\nContactTimelineCard.displayName = 'ContactTimelineCard'\n\nexport default ContactTimelineCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","pluginDocsHelp","mdiAccountCircleOutline","mdiAccountEditOutline","mdiAccountPlusOutline","mdiApi","mdiBullhornOutline","mdiCalendarCheckOutline","mdiCartOutline","mdiEmailNewsletter","mdiFileImportOutline","mdiFormSelect","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","ListPagination","Button","Chip","Stack","Tooltip","Typography","useParams","useCallback","useMemo","useState","contactInteractionHref","INTERACTION_LINK_LABELS","contactPrimaryGroup","ActivityRow","useActivityScope","useActivityWindow","useCanEditActivity","TimelineEntry","TimelineExpandAllButton","useTimelineExpansion","useTimelinePages","CrmSuiteLockedButton","LogActivityDialog","useContactCampaignEmails","SOURCE_ICONS","form","member","order","booking","newsletter","api","manual","import","account","CapturedRow","props","interaction","nowMs","href","source","type","label","String","icon","when","Date","atMs","path","chips","size","variant","title","summary","meta","direction","spacing","sx","alignItems","flexWrap","rowGap","toLocaleString","color","activityTimeLabel","displayName","CampaignEmailRow","email","name","campaignName","subject","campaignEmailSummary","join","sentAtMs","alignSelf","ContactTimelineCard","hostId","org","contactId","contact","campaignHref","suiteLocked","scope","consentGroup","interactions","facet","readContactFacet","groupId","interactionsForGroup","hostIds","link","activities","params","siteContext","orgSlug","host","interactionHref","campaigns","canEdit","orgId","entries","mergeContactTimeline","rows","emails","now","dialogOpen","setDialogOpen","editing","setEditing","openNew","openEdit","activity","close","loggedChip","expansion","pages","length","hasMore","showMore","loading","status","logButton","onClick","disabled","ready","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","lookupFailed","compact","description","slice","map","entry","kind","key","onEdit","editable","expanded","isExpanded","onToggle","toggle","footer","open","onClose"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-timeline-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 type {\n ContactCampaignEmail,\n ContactInteraction,\n ContactSource,\n CrmActivityRow,\n} from '@aglyn/aglyn'\nimport { CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiAccountCircleOutline,\n mdiAccountEditOutline,\n mdiAccountPlusOutline,\n mdiApi,\n mdiBullhornOutline,\n mdiCalendarCheckOutline,\n mdiCartOutline,\n mdiEmailNewsletter,\n mdiFileImportOutline,\n mdiFormSelect,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { Button, Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport { useParams } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { contactInteractionHref, INTERACTION_LINK_LABELS } from '../model/contact-interaction-links'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport { ActivityRow } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n useCanEditActivity,\n} from './activity-queries'\nimport {\n TimelineEntry,\n TimelineExpandAllButton,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\nimport { LogActivityDialog } from './log-activity-dialog'\nimport { useContactCampaignEmails } from './use-contact-campaign-emails'\n\n/**\n * One glyph per capture door. Typed against the source union so a door added\n * to it cannot render as the placeholder glyph.\n */\nconst SOURCE_ICONS: Record<ContactSource, { path: string }> = {\n form: mdiFormSelect,\n member: mdiAccountPlusOutline,\n order: mdiCartOutline,\n booking: mdiCalendarCheckOutline,\n newsletter: mdiEmailNewsletter,\n api: mdiApi,\n manual: mdiAccountEditOutline,\n import: mdiFileImportOutline,\n account: mdiAccountCircleOutline,\n}\n\n/**\n * One thing the platform recorded: the door it came through, what the door\n * wrote, when, and the page the person was on.\n *\n * Read-only by nature — nobody logged it, so nobody edits it. The entry\n * point sits beside the timestamp rather than in the summary, the way the\n * contacts drawer draws it: the summary is the sentence the door wrote, the\n * path is a fact the door recorded, and a long path on the sentence's line\n * pushes out the thing the row is about.\n *\n * Everything it holds fits the collapsed row, so it has nothing to open.\n */\nfunction CapturedRow(props: {\n interaction: ContactInteraction\n nowMs: number\n /** The console page the door's record is read on, when it left one. */\n href: string | null\n}) {\n const { interaction, nowMs, href } = props\n const source = interaction.type\n const label = CONTACT_SOURCE_LABELS[source] ?? String(source)\n const icon = SOURCE_ICONS[source]\n const when = new Date(interaction.atMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={icon?.path} />}\n chips={\n <>\n <Chip label={label} size=\"small\" />\n <Chip label=\"Captured\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={interaction.summary ?? label}\n meta={\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap', rowGap: 0.25 }}\n >\n <Tooltip title={when.toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {interaction.path\n ? `${Aglyn.activityTimeLabel(interaction.atMs, nowMs)} · ${interaction.path}`\n : Aglyn.activityTimeLabel(interaction.atMs, nowMs)}\n </Typography>\n </Tooltip>\n {/*\n The record the door left, where the console reads it (AGL-2622):\n the submission in the Inbox reader, the order in its dialog. A door\n that left nothing to open — a newsletter opt-in, an import — has no\n link, and the caption is the whole entry.\n */}\n {href ? (\n <Typography variant=\"caption\">\n <AppLink href={href}>\n {INTERACTION_LINK_LABELS[source] ?? 'Open'}\n </AppLink>\n </Typography>\n ) : null}\n </Stack>\n }\n />\n )\n}\nCapturedRow.displayName = 'CapturedRow'\n\n/**\n * One campaign email the platform sent this person (AGL-2616): which email,\n * and what became of it — `sent · delivered · opened ×2 · clicked`.\n *\n * Read-only like a captured row, and drawn apart from BOTH the captured and\n * the logged rows: its glyph is the campaign's, never the envelope a logged\n * `email` activity carries, and its chips say \"Campaign\" and \"Sent\" where\n * the others say \"Captured\" and \"Logged\". The name links to the email's own\n * report when the page can address it; an email the team has deleted, or\n * one a sibling site sent, keeps its name and loses the link.\n *\n * The delivery line stays on the collapsed row beside the name, so it has\n * nothing to open.\n */\nfunction CampaignEmailRow(props: {\n email: ContactCampaignEmail\n href: string | null\n nowMs: number\n}) {\n const { email, href, nowMs } = props\n const name = email.campaignName ?? email.subject ?? 'Campaign email'\n const summary = Aglyn.campaignEmailSummary(email).join(' · ')\n const when = new Date(email.sentAtMs)\n return (\n <TimelineEntry\n icon={<MdiIcon path={mdiBullhornOutline.path} />}\n chips={\n <>\n <Chip label=\"Campaign\" size=\"small\" />\n <Chip label=\"Sent\" size=\"small\" variant=\"outlined\" />\n </>\n }\n title={\n <>\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n {` · ${summary}`}\n </>\n }\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {email.subject && email.subject !== name\n ? `${Aglyn.activityTimeLabel(email.sentAtMs, nowMs)} · ${email.subject}`\n : Aglyn.activityTimeLabel(email.sentAtMs, nowMs)}\n </Typography>\n </Tooltip>\n }\n />\n )\n}\nCampaignEmailRow.displayName = 'CampaignEmailRow'\n\nexport interface ContactTimelineCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n contactId: string\n /**\n * The RAW contact document, as the page's listener hands it back. The card\n * resolves the viewing group and reads that group's facet itself, so a\n * page cannot hand it another holder's timeline by flattening the row\n * through the wrong group.\n */\n contact: Record<string, unknown> | null | undefined\n /**\n * Where a campaign entry links to — the email's report — or `null` for\n * one the page cannot address. Handed in rather than built here because\n * the report lives on another surface whose path is the MOUNT's to know:\n * a site's record page reaches its own Emails hub, an org-level mount\n * holds the org's site list and can reach every site's. No builder means\n * no links, never a guessed one.\n */\n campaignHref?: (email: ContactCampaignEmail) => string | null\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The history still draws; logging an activity stands\n * locked, and one already logged cannot be edited.\n */\n suiteLocked?: boolean\n}\n\n/**\n * A person's whole history in one newest-first stream (AGL-2600): what the\n * platform CAPTURED — a form, an order, a booking, a sign-up — what a person\n * LOGGED beside it — a call, an email, a meeting, a note — and, since\n * AGL-2616, the campaign mail the platform SENT them and what became of it.\n *\n * ## Why one stream\n *\n * The histories answer one question, \"what has happened with this person\",\n * and a page showing them as separate lists asks the reader to interleave\n * them by eye. The merge is `mergeContactTimeline`, pure and specced: newest\n * first, stable at a tie, an undated entry last, a campaign at the instant\n * it was sent.\n *\n * Each entry says which it is. A captured entry carries the door's label and\n * a \"Captured\" chip and offers no controls; a logged entry carries its kind\n * and a \"Logged\" chip, and its author may edit or delete it; a campaign\n * entry carries \"Campaign\" and \"Sent\", links to the email's report, and is\n * nobody's to edit.\n *\n * ## What is read\n *\n * The captured side comes off the contact document the page already holds\n * — THIS group's facet, filtered to the sites the group may see, the same\n * projection the contacts list makes — and costs no read. The logged side is\n * the bounded listener every activity surface uses, filtered to this\n * contact, a hundred at a time. The campaign side is one request to\n * `crm/contact-email-history`, which reads down this person's own delivery\n * log, capped, and never a campaign's recipients. The facet's history is\n * capped at fifty by the platform, so past the window the stream is the\n * logged side alone.\n *\n * ## How it reads\n *\n * Every entry is a collapsed row that opens in place, paged by the\n * console's footer over the merged stream; a page turned past the logged\n * window widens it by the next hundred. **Expand all** and **Log activity**\n * sit in the card header, and edit and delete stay on the entry they act on.\n */\nexport function ContactTimelineCard(props: ContactTimelineCardProps) {\n const { hostId, org, contactId, contact, campaignHref, suiteLocked = false } = props\n const scope = useActivityScope(hostId, org)\n /*\n * THIS holder's captured history, and only the visits this group may see.\n * A read off the top of the document, or of another group's facet, would\n * be another business's records — the disclosure the facet shape ended.\n * At the organization level (AGL-2630) the holder is the person's own\n * primary one, the group the record page reads the profile through.\n */\n const consentGroup = useMemo(\n () =>\n scope.consentGroup ??\n contactPrimaryGroup(contact, (org ?? null) as Record<string, unknown> | null),\n [scope.consentGroup, contact, org],\n )\n const interactions = useMemo(() => {\n const facet = Aglyn.readContactFacet(contact, consentGroup.groupId)\n return Aglyn.interactionsForGroup(facet.interactions, consentGroup.hostIds)\n }, [contact, consentGroup])\n const link = useMemo<ActivityRecordLink>(() => ({ contactId }), [contactId])\n const activities = useActivityWindow(scope, link)\n /*\n * Where a captured entry's record is READ (AGL-2622). The site console's\n * two route params are already in the URL this page is on, so the link is\n * built from them rather than from two document reads per timeline. Only\n * an interaction on THIS site links: a sibling site's order is that\n * site's record, read on that site's console, and a link built with this\n * site's segment would 404 or — worse — open the wrong site's row under\n * the same id. An interaction with no site recorded predates the stamp\n * and is left unlinked rather than guessed at. Off a site — the org-level\n * mount — there is no segment to build with, and nothing links.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext = useMemo(\n () =>\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null,\n [params?.orgSlug, params?.host],\n )\n const interactionHref = useCallback(\n (interaction: ContactInteraction): string | null =>\n siteContext && interaction.hostId === hostId\n ? contactInteractionHref(interaction, siteContext)\n : null,\n [siteContext, hostId],\n )\n // The campaign history is asked of a site: the mounted one, or the holder's.\n const campaigns = useContactCampaignEmails(\n hostId ?? (consentGroup.hostId || null),\n contactId,\n )\n // One member read for the whole stream, however many rows it has.\n const canEdit = useCanEditActivity(scope.orgId)\n const entries = useMemo(\n () =>\n Aglyn.mergeContactTimeline(interactions, activities.rows, campaigns.emails),\n [interactions, activities.rows, campaigns.emails],\n )\n // One clock for every row of one paint, so two rows logged a second apart\n // cannot read \"just now\" and \"1 min ago\" from being rendered across a\n // minute boundary.\n const nowMs = Date.now()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editing, setEditing] = useState<CrmActivityRow | null>(null)\n const openNew = useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n const openEdit = useCallback((activity: CrmActivityRow) => {\n setEditing(activity)\n setDialogOpen(true)\n }, [])\n const close = useCallback(() => setDialogOpen(false), [])\n\n const loggedChip = useMemo(\n () => <Chip label=\"Logged\" size=\"small\" variant=\"outlined\" />,\n [],\n )\n const expansion = useTimelineExpansion()\n const pages = useTimelinePages(entries.length, {\n hasMore: activities.hasMore,\n showMore: activities.showMore,\n loading: activities.status === 'loading',\n })\n const logButton = suiteLocked ? (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'Log activity'}\n </CrmSuiteLockedButton>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n )\n\n return (\n <>\n <CardDisplay\n header={'Timeline'}\n help={pluginDocsHelp('contactActivities', { anchor: '#four-kinds-of-history' })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <TimelineExpandAllButton expansion={expansion} disabled={!entries.length} />\n {logButton}\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'Logged activity could not be loaded; what follows is what the platform captured.'}\n </Typography>\n ) : null}\n {campaigns.status === 'error' || campaigns.lookupFailed ? (\n // \"Unknown\", not \"none\": an empty campaign history and one that\n // could not be read lead a reader to opposite conclusions.\n <Typography variant=\"body2\" color=\"error\">\n {'Campaign email could not be loaded; the stream below may be missing the mail this person was sent.'}\n </Typography>\n ) : null}\n {entries.length === 0 ? (\n campaigns.status === 'loading' ? (\n // The campaign history is one request on mount; an empty state\n // before it answers would read as \"none\" for a person who was mailed.\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'No history yet'}\n description={'What this person does on the site, the campaigns they are sent, and what you log about them, shows here.'}\n action={logButton}\n />\n )\n ) : (\n pages.slice(entries).map((entry) =>\n entry.kind === 'captured' ? (\n <CapturedRow\n key={entry.key}\n interaction={entry.interaction}\n nowMs={nowMs}\n href={interactionHref(entry.interaction)}\n />\n ) : entry.kind === 'campaign' ? (\n <CampaignEmailRow\n key={entry.key}\n email={entry.email}\n href={campaignHref ? campaignHref(entry.email) : null}\n nowMs={nowMs}\n />\n ) : (\n <ActivityRow\n key={entry.key}\n activity={entry.activity}\n scope={scope}\n onEdit={openEdit}\n subject={loggedChip}\n nowMs={nowMs}\n editable={!suiteLocked && canEdit(entry.activity)}\n expanded={expansion.isExpanded(entry.key)}\n onToggle={() => expansion.toggle(entry.key)}\n />\n ),\n )\n )}\n {entries.length ? <ListPagination {...pages.footer} /> : null}\n </Stack>\n </CardDisplay>\n <LogActivityDialog\n open={dialogOpen}\n onClose={close}\n scope={scope}\n link={link}\n activity={editing}\n />\n </>\n )\n}\nContactTimelineCard.displayName = 'ContactTimelineCard'\n\nexport default ContactTimelineCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","pluginDocsHelp","mdiAccountCircleOutline","mdiAccountEditOutline","mdiAccountPlusOutline","mdiApi","mdiBullhornOutline","mdiCalendarCheckOutline","mdiCartOutline","mdiEmailNewsletter","mdiFileImportOutline","mdiFormSelect","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","ListPagination","Button","Chip","Stack","Tooltip","Typography","useParams","useCallback","useMemo","useState","contactInteractionHref","INTERACTION_LINK_LABELS","contactPrimaryGroup","ActivityRow","useActivityScope","useActivityWindow","useCanEditActivity","TimelineEntry","TimelineExpandAllButton","useTimelineExpansion","useTimelinePages","CrmSuiteLockedButton","LogActivityDialog","useContactCampaignEmails","SOURCE_ICONS","form","member","order","booking","newsletter","api","manual","import","account","CapturedRow","props","interaction","nowMs","href","source","type","label","String","icon","when","Date","atMs","path","chips","size","variant","title","summary","meta","useFlexGap","direction","spacing","sx","alignItems","flexWrap","rowGap","toLocaleString","color","activityTimeLabel","displayName","CampaignEmailRow","email","name","campaignName","subject","campaignEmailSummary","join","sentAtMs","alignSelf","ContactTimelineCard","hostId","org","contactId","contact","campaignHref","suiteLocked","scope","consentGroup","interactions","facet","readContactFacet","groupId","interactionsForGroup","hostIds","link","activities","params","siteContext","orgSlug","host","interactionHref","campaigns","canEdit","orgId","entries","mergeContactTimeline","rows","emails","now","dialogOpen","setDialogOpen","editing","setEditing","openNew","openEdit","activity","close","loggedChip","expansion","pages","length","hasMore","showMore","loading","status","logButton","onClick","disabled","ready","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","lookupFailed","compact","description","slice","map","entry","kind","key","onEdit","editable","expanded","isExpanded","onToggle","toggle","footer","open","onClose"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-glance-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Button, Stack, Typography } from '@mui/material'\nimport {\n getAggregateFromServer,\n getCountFromServer,\n query,\n sum,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useOrgDataScope,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmRoutes } from '../model/crm-routes'\nimport { openLeadsForScope } from './reports/lead-counts'\nimport { scopedCollection, visibleToClause } from './reports/report-scope'\nimport { ReportStatTile } from './reports/report-stat-tile'\nimport { useAggregateRead } from './reports/use-aggregate-read'\nimport { localDayBounds } from '../model/crm-reports'\n\nconst WEEK_MS = 7 * 24 * 60 * 60 * 1000\n\ninterface GlanceFigures {\n contacts: number\n newThisWeek: number\n pipelineCents: number\n tasksDue: number\n /** Open leads — new or being worked — on this site, or across the org's sites. */\n leadsToWork: number\n}\n\nexport interface CrmGlanceCardProps {\n /**\n * The site the card is on the dashboard of, or `null` beneath the org\n * hub's mount (AGL-2634), where the figures total every site: the\n * org-wide reads carry no scope clause, and the lead figure fans out\n * over the mount's site list.\n */\n hostId: string | null\n /** The org hub's own path, for the links when there is no site to derive them from. */\n basePath?: string\n}\n\n/**\n * CRM at a glance (AGL-2604): a `hostDashboard` widget with five numbers —\n * contacts, new this week, open pipeline value, tasks due today or overdue,\n * and leads still to work (AGL-2624) — each a link into the section that\n * explains it.\n *\n * Server aggregates and nothing else: a dashboard card mounts on every\n * visit to the site's front page, so it is the one surface on which a\n * bounded read of a thousand rows would be a cost paid for nothing. Each\n * number is a count or a sum the server takes over the same `visibleTo`\n * predicate the hub's sections query with, so the card never shows a figure\n * the section behind the link would not.\n *\n * The lead figure is two counts and a subtraction — every lead on the site,\n * less the closed ones — because a lead nobody has touched carries no status\n * field and Firestore cannot select on a field's absence; see\n * `openLeadsFromCounts`. Leads are host-scoped by path rather than by\n * `visibleTo`, so those two counts run on `hosts/{hostId}/leads` under the\n * site's own rules, the way the Leads section reads it.\n *\n * The widget is handed a host doc id and nothing else, so the org — needed\n * for the consent group whose tokens scope the reads — comes from\n * `useOrgPlan`, and the link base from `useConsoleHostRoute`, the way the\n * commerce glance card builds its store link. Nothing is read until the org\n * has settled: a first round of aggregates against the solo group, replaced\n * by a second against the real one, would be two reads for one answer.\n *\n * ## The organization level (AGL-2634)\n *\n * Mounted with no site beneath the org hub's mount, the card totals the\n * organization: the reads carry no scope clause — the rules admit an\n * org-wide member to every row — and the lead figure is the same two\n * counts per site, over every site the mount lists, summed. Nothing is\n * read until that list has settled, for the reason nothing is read under\n * a site until the org has. The org's dashboard has no widget slot yet, so\n * the mount is a capability rather than a page; what it needs from the\n * shell is a slot and the hub's `basePath`.\n */\nexport function CrmGlanceCard(props: CrmGlanceCardProps) {\n const { hostId, basePath } = props\n const mount = useCrmOrgMount()\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: planReady } = useOrgPlan(hostId ?? undefined)\n const { scope } = useOrgDataScope(hostId ? { hostId } : { orgId: mount?.orgId })\n const firestore = useFirestore()\n // Under a site, the site's group's tokens once the org has settled; at\n // the org level `null` — no clause — once the site list has.\n const tokens = useMemo(() => {\n if (!hostId) return null\n if (!planReady) return undefined\n const group = Aglyn.consentGroupForHost(org ?? null, hostId)\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...group.hostIds.map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, planReady, hostId])\n const leadSites = useMemo(\n () =>\n hostId\n ? [hostId]\n : mount?.hostsReady\n ? mount.hosts.map((host) => host.id)\n : null,\n [hostId, mount],\n )\n const leadSitesKey = leadSites?.join('\\n') ?? ''\n // Anchored once per mount so the queries are stable across renders.\n const nowMs = useMemo(() => Date.now(), [])\n\n const figures = useAggregateRead<GlanceFigures>(() => {\n if (!scope || tokens === undefined || !leadSites) return null\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const deals = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.deals)\n const tasks = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks)\n const day = localDayBounds(nowMs)\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n return Promise.all([\n countOf(query(contacts, ...visibleToClause(tokens))),\n countOf(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(nowMs - WEEK_MS)),\n ),\n ),\n getAggregateFromServer(\n query(deals, ...visibleToClause(tokens), where('status', '==', 'open')),\n { amountCents: sum('amountCents') },\n ).then((snapshot) => Number(snapshot.data().amountCents ?? 0)),\n // Due today OR overdue: everything open that is due before the day ends.\n countOf(\n query(\n tasks,\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n where('dueAtMs', '<', day.end),\n ),\n ),\n // One scoped count over the org collection (AGL-3275) — see the\n // function's note on why summing per site would double-count.\n openLeadsForScope(firestore, scope[1], tokens),\n ]).then(([contacts, newThisWeek, pipelineCents, tasksDue, leadsToWork]) => ({\n contacts,\n newThisWeek,\n pipelineCents,\n tasksDue,\n leadsToWork,\n }))\n }, [firestore, scope, tokens, nowMs, leadSitesKey])\n\n const routes = useMemo(\n () =>\n hostId\n ? consoleRoute.base\n ? crmRoutes(`${consoleRoute.base}/crm`)\n : null\n : basePath\n ? crmRoutes(basePath)\n : null,\n [hostId, consoleRoute.base, basePath],\n )\n const hubHref = hostId\n ? consoleRoute.base\n ? `${consoleRoute.base}/crm`\n : undefined\n : basePath\n const value = figures.value\n\n return (\n <CardDisplay\n header={'CRM at a glance'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#crm-at-a-glance',\n excerpt:\n 'Contacts, new contacts this week, the value of every open deal, ' +\n 'the tasks due today or overdue, and the leads still to work — ' +\n 'each counted on the server and each a link into the CRM.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={hubHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Open CRM'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Contacts'}\n value={value ? value.contacts.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'New this week'}\n value={value ? value.newThisWeek.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'Open pipeline'}\n value={value ? money(value.pipelineCents, 'usd') : null}\n href={routes?.section('deals')}\n />\n <ReportStatTile\n label={'Tasks due'}\n value={value ? value.tasksDue.toLocaleString() : null}\n note={'today or overdue'}\n color={value?.tasksDue ? 'warning.main' : undefined}\n href={routes?.section('tasks')}\n />\n <ReportStatTile\n label={'Leads to work'}\n value={value ? value.leadsToWork.toLocaleString() : null}\n note={hostId ? 'open on this site' : 'open across every site'}\n href={routes?.section('leads')}\n />\n </Stack>\n {figures.status === 'error' ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The CRM figures could not be read. Open the CRM for the full picture.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCrmGlanceCard.displayName = 'CrmGlanceCard'\n\nexport default CrmGlanceCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Button","Stack","Typography","getAggregateFromServer","getCountFromServer","query","sum","Timestamp","where","useMemo","useConsoleHostRoute","useFirestore","useOrgDataScope","useOrgPlan","useCrmOrgMount","crmRoutes","openLeadsForScope","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","localDayBounds","WEEK_MS","CrmGlanceCard","props","hostId","basePath","mount","consoleRoute","org","ready","planReady","undefined","scope","orgId","firestore","tokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","leadSites","hostsReady","hosts","host","leadSitesKey","join","nowMs","Date","now","figures","contacts","deals","CRM_COLLECTIONS","tasks","day","countOf","target","then","snapshot","data","count","Promise","all","fromMillis","amountCents","Number","end","newThisWeek","pipelineCents","tasksDue","leadsToWork","routes","base","hubHref","value","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","size","color","spacing","direction","sx","flexWrap","label","toLocaleString","section","note","status","variant","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,KAAK,QAAQ,6DAA4D;AAClF,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACzD,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,KAAK,EACLC,GAAG,EACHC,SAAS,EACTC,KAAK,QACA,qBAAoB;AAC3B,SAASC,OAAO,QAAQ,QAAO;AAC/B,SACEC,mBAAmB,EACnBC,YAAY,EACZC,eAAe,EACfC,UAAU,QACL,iCAAgC;AACvC,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,gBAAgB,EAAEC,eAAe,QAAQ,4BAAwB;AAC1E,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,gBAAgB,QAAQ,kCAA8B;AAC/D,SAASC,cAAc,QAAQ,0BAAsB;AAErD,MAAMC,UAAU,IAAI,KAAK,KAAK,KAAK;AAuBnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GACD,OAAO,SAASC,cAAcC,KAAyB;;IACrD,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,MAAMG,QAAQb;IACd,MAAMc,eAAelB,oBAAoBe;IACzC,MAAM,EAAEI,GAAG,EAAEC,OAAOC,SAAS,EAAE,GAAGlB,WAAWY,iBAAAA,SAAUO;IACvD,MAAM,EAAEC,KAAK,EAAE,GAAGrB,gBAAgBa,SAAS;QAAEA;IAAO,IAAI;QAAES,KAAK,EAAEP,yBAAAA,MAAOO,KAAK;IAAC;IAC9E,MAAMC,YAAYxB;IAClB,uEAAuE;IACvE,6DAA6D;IAC7D,MAAMyB,SAAS3B,QAAQ;QACrB,IAAI,CAACgB,QAAQ,OAAO;QACpB,IAAI,CAACM,WAAW,OAAOC;QACvB,MAAMK,QAAQzC,MAAM0C,mBAAmB,CAACT,cAAAA,MAAO,MAAMJ;QACrD,OAAO;YACL7B,MAAM2C,eAAe;eAClBF,MAAMG,OAAO,CAACC,GAAG,CAAC,CAACC,KAAO9C,MAAM+C,cAAc,CAACD;SACnD,CAACE,KAAK,CAAC,GAAGhD,MAAMiD,eAAe;IAClC,GAAG;QAAChB;QAAKE;QAAWN;KAAO;IAC3B,MAAMqB,YAAYrC,QAChB,IACEgB,SACI;YAACA;SAAO,GACRE,CAAAA,yBAAAA,MAAOoB,UAAU,IACfpB,MAAMqB,KAAK,CAACP,GAAG,CAAC,CAACQ,OAASA,KAAKP,EAAE,IACjC,MACR;QAACjB;QAAQE;KAAM;IAEjB,MAAMuB,uBAAeJ,6BAAAA,UAAWK,IAAI,CAAC,wBAAS;IAC9C,oEAAoE;IACpE,MAAMC,QAAQ3C,QAAQ,IAAM4C,KAAKC,GAAG,IAAI,EAAE;IAE1C,MAAMC,UAAUnC,iBAAgC;QAC9C,IAAI,CAACa,SAASG,WAAWJ,aAAa,CAACc,WAAW,OAAO;QACzD,MAAMU,WAAWvC,iBAAiBkB,WAAWF,OAAO;QACpD,MAAMwB,QAAQxC,iBAAiBkB,WAAWF,OAAOrC,MAAM8D,eAAe,CAACD,KAAK;QAC5E,MAAME,QAAQ1C,iBAAiBkB,WAAWF,OAAOrC,MAAM8D,eAAe,CAACC,KAAK;QAC5E,MAAMC,MAAMvC,eAAe+B;QAC3B,MAAMS,UAAU,CAACC,SACf1D,mBAAmB0D,QAAQC,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK;QACrE,OAAOC,QAAQC,GAAG,CAAC;YACjBP,QAAQxD,MAAMmD,aAAatC,gBAAgBkB;YAC3CyB,QACExD,MACEmD,aACGtC,gBAAgBkB,SACnB5B,MAAM,aAAa,MAAMD,UAAU8D,UAAU,CAACjB,QAAQ9B;YAG1DnB,uBACEE,MAAMoD,UAAUvC,gBAAgBkB,SAAS5B,MAAM,UAAU,MAAM,UAC/D;gBAAE8D,aAAahE,IAAI;YAAe,GAClCyD,IAAI,CAAC,CAACC;oBAAoBA;uBAAPO,QAAOP,6BAAAA,SAASC,IAAI,GAAGK,WAAW,YAA3BN,6BAA+B;;YAC3D,yEAAyE;YACzEH,QACExD,MACEsD,UACGzC,gBAAgBkB,SACnB5B,MAAM,UAAU,MAAM,SACtBA,MAAM,WAAW,KAAKoD,IAAIY,GAAG;YAGjC,gEAAgE;YAChE,8DAA8D;YAC9DxD,kBAAkBmB,WAAWF,KAAK,CAAC,EAAE,EAAEG;SACxC,EAAE2B,IAAI,CAAC,CAAC,CAACP,UAAUiB,aAAaC,eAAeC,UAAUC,YAAY,GAAM,CAAA;gBAC1EpB;gBACAiB;gBACAC;gBACAC;gBACAC;YACF,CAAA;IACF,GAAG;QAACzC;QAAWF;QAAOG;QAAQgB;QAAOF;KAAa;IAElD,MAAM2B,SAASpE,QACb,IACEgB,SACIG,aAAakD,IAAI,GACf/D,UAAU,GAAGa,aAAakD,IAAI,CAAC,IAAI,CAAC,IACpC,OACFpD,WACEX,UAAUW,YACV,MACR;QAACD;QAAQG,aAAakD,IAAI;QAAEpD;KAAS;IAEvC,MAAMqD,UAAUtD,SACZG,aAAakD,IAAI,GACf,GAAGlD,aAAakD,IAAI,CAAC,IAAI,CAAC,GAC1B9C,YACFN;IACJ,MAAMsD,QAAQzB,QAAQyB,KAAK;IAE3B,qBACE,KAACjF;QACCkF,QAAQ;QACRC,MAAMtF,MAAMuF,cAAc,CAAC,cAAc;YACvCC,QAAQ;YACRC,SACE,qEACA,mEACA;QACJ;QACAC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACzF;gBACC0F,WAAW5F;eACN;gBAAE6F,kBAAkB;gBAASC,cAAc;YAAM;gBACtDC,MAAMd;gBACNe,MAAK;gBACLC,OAAM;0BAEL;;QAGP;kBAEA,cAAA,MAAC9F;YAAM+F,SAAS;;8BACd,MAAC/F;oBAAMgG,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAChF;4BACCiF,OAAO;4BACPpB,OAAOA,QAAQA,MAAMxB,QAAQ,CAAC6C,cAAc,KAAK;4BACjDR,IAAI,EAAEhB,0BAAAA,OAAQyB,OAAO,CAAC;;sCAExB,KAACnF;4BACCiF,OAAO;4BACPpB,OAAOA,QAAQA,MAAMP,WAAW,CAAC4B,cAAc,KAAK;4BACpDR,IAAI,EAAEhB,0BAAAA,OAAQyB,OAAO,CAAC;;sCAExB,KAACnF;4BACCiF,OAAO;4BACPpB,OAAOA,QAAQnF,MAAMmF,MAAMN,aAAa,EAAE,SAAS;4BACnDmB,IAAI,EAAEhB,0BAAAA,OAAQyB,OAAO,CAAC;;sCAExB,KAACnF;4BACCiF,OAAO;4BACPpB,OAAOA,QAAQA,MAAML,QAAQ,CAAC0B,cAAc,KAAK;4BACjDE,MAAM;4BACNR,OAAOf,CAAAA,yBAAAA,MAAOL,QAAQ,IAAG,iBAAiB3C;4BAC1C6D,IAAI,EAAEhB,0BAAAA,OAAQyB,OAAO,CAAC;;sCAExB,KAACnF;4BACCiF,OAAO;4BACPpB,OAAOA,QAAQA,MAAMJ,WAAW,CAACyB,cAAc,KAAK;4BACpDE,MAAM9E,SAAS,sBAAsB;4BACrCoE,IAAI,EAAEhB,0BAAAA,OAAQyB,OAAO,CAAC;;;;gBAGzB/C,QAAQiD,MAAM,KAAK,wBAClB,KAACtG;oBAAWuG,SAAQ;oBAAUV,OAAM;8BACjC;qBAED;;;;AAIZ;AACAxE,cAAcmF,WAAW,GAAG;AAE5B,eAAenF,cAAa"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-glance-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Button, Stack, Typography } from '@mui/material'\nimport {\n getAggregateFromServer,\n getCountFromServer,\n query,\n sum,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useOrgDataScope,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmRoutes } from '../model/crm-routes'\nimport { openLeadsForScope } from './reports/lead-counts'\nimport { scopedCollection, visibleToClause } from './reports/report-scope'\nimport { ReportStatTile } from './reports/report-stat-tile'\nimport { useAggregateRead } from './reports/use-aggregate-read'\nimport { localDayBounds } from '../model/crm-reports'\n\nconst WEEK_MS = 7 * 24 * 60 * 60 * 1000\n\ninterface GlanceFigures {\n contacts: number\n newThisWeek: number\n pipelineCents: number\n tasksDue: number\n /** Open leads — new or being worked — on this site, or across the org's sites. */\n leadsToWork: number\n}\n\nexport interface CrmGlanceCardProps {\n /**\n * The site the card is on the dashboard of, or `null` beneath the org\n * hub's mount (AGL-2634), where the figures total every site: the\n * org-wide reads carry no scope clause, and the lead figure fans out\n * over the mount's site list.\n */\n hostId: string | null\n /** The org hub's own path, for the links when there is no site to derive them from. */\n basePath?: string\n}\n\n/**\n * CRM at a glance (AGL-2604): a `hostDashboard` widget with five numbers —\n * contacts, new this week, open pipeline value, tasks due today or overdue,\n * and leads still to work (AGL-2624) — each a link into the section that\n * explains it.\n *\n * Server aggregates and nothing else: a dashboard card mounts on every\n * visit to the site's front page, so it is the one surface on which a\n * bounded read of a thousand rows would be a cost paid for nothing. Each\n * number is a count or a sum the server takes over the same `visibleTo`\n * predicate the hub's sections query with, so the card never shows a figure\n * the section behind the link would not.\n *\n * The lead figure is two counts and a subtraction — every lead on the site,\n * less the closed ones — because a lead nobody has touched carries no status\n * field and Firestore cannot select on a field's absence; see\n * `openLeadsFromCounts`. Leads are host-scoped by path rather than by\n * `visibleTo`, so those two counts run on `hosts/{hostId}/leads` under the\n * site's own rules, the way the Leads section reads it.\n *\n * The widget is handed a host doc id and nothing else, so the org — needed\n * for the consent group whose tokens scope the reads — comes from\n * `useOrgPlan`, and the link base from `useConsoleHostRoute`, the way the\n * commerce glance card builds its store link. Nothing is read until the org\n * has settled: a first round of aggregates against the solo group, replaced\n * by a second against the real one, would be two reads for one answer.\n *\n * ## The organization level (AGL-2634)\n *\n * Mounted with no site beneath the org hub's mount, the card totals the\n * organization: the reads carry no scope clause — the rules admit an\n * org-wide member to every row — and the lead figure is the same two\n * counts per site, over every site the mount lists, summed. Nothing is\n * read until that list has settled, for the reason nothing is read under\n * a site until the org has. The org's dashboard has no widget slot yet, so\n * the mount is a capability rather than a page; what it needs from the\n * shell is a slot and the hub's `basePath`.\n */\nexport function CrmGlanceCard(props: CrmGlanceCardProps) {\n const { hostId, basePath } = props\n const mount = useCrmOrgMount()\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: planReady } = useOrgPlan(hostId ?? undefined)\n const { scope } = useOrgDataScope(hostId ? { hostId } : { orgId: mount?.orgId })\n const firestore = useFirestore()\n // Under a site, the site's group's tokens once the org has settled; at\n // the org level `null` — no clause — once the site list has.\n const tokens = useMemo(() => {\n if (!hostId) return null\n if (!planReady) return undefined\n const group = Aglyn.consentGroupForHost(org ?? null, hostId)\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...group.hostIds.map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, planReady, hostId])\n const leadSites = useMemo(\n () =>\n hostId\n ? [hostId]\n : mount?.hostsReady\n ? mount.hosts.map((host) => host.id)\n : null,\n [hostId, mount],\n )\n const leadSitesKey = leadSites?.join('\\n') ?? ''\n // Anchored once per mount so the queries are stable across renders.\n const nowMs = useMemo(() => Date.now(), [])\n\n const figures = useAggregateRead<GlanceFigures>(() => {\n if (!scope || tokens === undefined || !leadSites) return null\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const deals = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.deals)\n const tasks = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks)\n const day = localDayBounds(nowMs)\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n return Promise.all([\n countOf(query(contacts, ...visibleToClause(tokens))),\n countOf(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(nowMs - WEEK_MS)),\n ),\n ),\n getAggregateFromServer(\n query(deals, ...visibleToClause(tokens), where('status', '==', 'open')),\n { amountCents: sum('amountCents') },\n ).then((snapshot) => Number(snapshot.data().amountCents ?? 0)),\n // Due today OR overdue: everything open that is due before the day ends.\n countOf(\n query(\n tasks,\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n where('dueAtMs', '<', day.end),\n ),\n ),\n // One scoped count over the org collection (AGL-3275) — see the\n // function's note on why summing per site would double-count.\n openLeadsForScope(firestore, scope[1], tokens),\n ]).then(([contacts, newThisWeek, pipelineCents, tasksDue, leadsToWork]) => ({\n contacts,\n newThisWeek,\n pipelineCents,\n tasksDue,\n leadsToWork,\n }))\n }, [firestore, scope, tokens, nowMs, leadSitesKey])\n\n const routes = useMemo(\n () =>\n hostId\n ? consoleRoute.base\n ? crmRoutes(`${consoleRoute.base}/crm`)\n : null\n : basePath\n ? crmRoutes(basePath)\n : null,\n [hostId, consoleRoute.base, basePath],\n )\n const hubHref = hostId\n ? consoleRoute.base\n ? `${consoleRoute.base}/crm`\n : undefined\n : basePath\n const value = figures.value\n\n return (\n <CardDisplay\n header={'CRM at a glance'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#crm-at-a-glance',\n excerpt:\n 'Contacts, new contacts this week, the value of every open deal, ' +\n 'the tasks due today or overdue, and the leads still to work — ' +\n 'each counted on the server and each a link into the CRM.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={hubHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Open CRM'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Contacts'}\n value={value ? value.contacts.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'New this week'}\n value={value ? value.newThisWeek.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'Open pipeline'}\n value={value ? money(value.pipelineCents, 'usd') : null}\n href={routes?.section('deals')}\n />\n <ReportStatTile\n label={'Tasks due'}\n value={value ? value.tasksDue.toLocaleString() : null}\n note={'today or overdue'}\n color={value?.tasksDue ? 'warning.main' : undefined}\n href={routes?.section('tasks')}\n />\n <ReportStatTile\n label={'Leads to work'}\n value={value ? value.leadsToWork.toLocaleString() : null}\n note={hostId ? 'open on this site' : 'open across every site'}\n href={routes?.section('leads')}\n />\n </Stack>\n {figures.status === 'error' ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The CRM figures could not be read. Open the CRM for the full picture.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCrmGlanceCard.displayName = 'CrmGlanceCard'\n\nexport default CrmGlanceCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Button","Stack","Typography","getAggregateFromServer","getCountFromServer","query","sum","Timestamp","where","useMemo","useConsoleHostRoute","useFirestore","useOrgDataScope","useOrgPlan","useCrmOrgMount","crmRoutes","openLeadsForScope","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","localDayBounds","WEEK_MS","CrmGlanceCard","props","hostId","basePath","mount","consoleRoute","org","ready","planReady","undefined","scope","orgId","firestore","tokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","leadSites","hostsReady","hosts","host","leadSitesKey","join","nowMs","Date","now","figures","contacts","deals","CRM_COLLECTIONS","tasks","day","countOf","target","then","snapshot","data","count","Promise","all","fromMillis","amountCents","Number","end","newThisWeek","pipelineCents","tasksDue","leadsToWork","routes","base","hubHref","value","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","size","color","spacing","useFlexGap","direction","sx","flexWrap","label","toLocaleString","section","note","status","variant","displayName"],"mappings":"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"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
2
1
|
import type { GridColDef } from '@mui/x-data-grid';
|
|
3
2
|
/**
|
|
4
3
|
* The "Next activity" column the contacts, companies and deals lists share
|
|
@@ -15,15 +14,4 @@ import type { GridColDef } from '@mui/x-data-grid';
|
|
|
15
14
|
* the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
|
|
16
15
|
*/
|
|
17
16
|
export declare function nextActivityColumn(nowMs: number): GridColDef;
|
|
18
|
-
|
|
19
|
-
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
20
|
-
* Next activity column offers "is empty", which stores the clause
|
|
21
|
-
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
22
|
-
* reopens filtered the same way. Asked of the query as `nextTaskAtMs ==
|
|
23
|
-
* null` (AGL-3321): every contact, company and deal is created carrying
|
|
24
|
-
* `null`, and the task writers put `null` back when nothing is left
|
|
25
|
-
* scheduled.
|
|
26
|
-
*/
|
|
27
|
-
export declare const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField;
|
|
28
|
-
/** What the clause reads as on a chip. */
|
|
29
|
-
export declare const CRM_NEXT_ACTIVITY_FILTER_HEADER = "Next activity";
|
|
17
|
+
export { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, } from '../model/crm-next-activity';
|
|
@@ -69,24 +69,6 @@ import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "../model/crm-next-act
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
74
|
-
* Next activity column offers "is empty", which stores the clause
|
|
75
|
-
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
76
|
-
* reopens filtered the same way. Asked of the query as `nextTaskAtMs ==
|
|
77
|
-
* null` (AGL-3321): every contact, company and deal is created carrying
|
|
78
|
-
* `null`, and the task writers put `null` back when nothing is left
|
|
79
|
-
* scheduled.
|
|
80
|
-
*/ export const CRM_NEXT_ACTIVITY_FILTER_FIELD = {
|
|
81
|
-
column: CRM_NEXT_ACTIVITY_FIELD,
|
|
82
|
-
kind: 'date',
|
|
83
|
-
path: CRM_NEXT_ACTIVITY_FIELD,
|
|
84
|
-
presence: 'nullable',
|
|
85
|
-
storedAs: 'millis',
|
|
86
|
-
operators: [
|
|
87
|
-
'isEmpty'
|
|
88
|
-
]
|
|
89
|
-
};
|
|
90
|
-
/** What the clause reads as on a chip. */ export const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity';
|
|
72
|
+
export { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER } from "../model/crm-next-activity.js";
|
|
91
73
|
|
|
92
74
|
//# sourceMappingURL=crm-next-activity-column.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.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
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.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 { Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { TaskDueText } from './task-cells'\nimport { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from '../model/crm-next-activity'\n\n/**\n * The \"Next activity\" column the contacts, companies and deals lists share\n * (AGL-2661): when the record's earliest open task is due, off the\n * `nextTaskAtMs` every server-side task writer keeps on the row.\n *\n * Drawn the way the tasks list draws a due date — overdue in the warning\n * color, today emphasized — because it IS that date, seen from the record.\n * A dash for a record with nothing scheduled, and the same dash for one\n * written before the field existed: the reader's question is \"is anything\n * planned\", and to both the answer is no. Sorting is off for the reason\n * the custom columns' is — the value is on the row, and a sort over the\n * loaded window would look like a sort over the collection. Filtering is\n * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.\n */\nexport function nextActivityColumn(nowMs: number): GridColDef {\n return {\n field: CRM_NEXT_ACTIVITY_FIELD,\n headerName: 'Next activity',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: { nextTaskAtMs?: unknown }) => {\n const at = readNextTaskAtMs(row)\n return at === null ? null : new Date(at)\n },\n renderCell: ({ row }: { row: { nextTaskAtMs?: unknown } }) => {\n const at = readNextTaskAtMs(row)\n if (at === null) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n }\n return (\n <Tooltip title={new Date(at).toLocaleString()}>\n <span>\n <TaskDueText task={{ status: 'open', dueAtMs: at }} nowMs={nowMs} variant=\"caption\" />\n </span>\n </Tooltip>\n )\n },\n }\n}\n\nexport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n} from '../model/crm-next-activity'\n"],"names":["Tooltip","Typography","TaskDueText","CRM_NEXT_ACTIVITY_FIELD","readNextTaskAtMs","nextActivityColumn","nowMs","field","headerName","flex","minWidth","type","sortable","filterable","valueGetter","_value","row","at","Date","renderCell","variant","color","title","toLocaleString","span","task","status","dueAtMs","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEnD,SAASC,WAAW,QAAQ,kBAAc;AAC1C,SAASC,uBAAuB,EAAEC,gBAAgB,QAAQ,gCAA4B;AAEtF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOJ;QACPK,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKb,iBAAiBY;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKb,iBAAiBY;YAC5B,IAAIC,OAAO,MAAM;gBACf,qBACE,KAAChB;oBAAWmB,SAAQ;oBAAUC,OAAM;8BACjC;;YAGP;YACA,qBACE,KAACrB;gBAAQsB,OAAO,IAAIJ,KAAKD,IAAIM,cAAc;0BACzC,cAAA,KAACC;8BACC,cAAA,KAACtB;wBAAYuB,MAAM;4BAAEC,QAAQ;4BAAQC,SAASV;wBAAG;wBAAGX,OAAOA;wBAAOc,SAAQ;;;;QAIlF;IACF;AACF;AAEA,SACEQ,8BAA8B,EAC9BC,+BAA+B,QAC1B,gCAA4B"}
|
|
@@ -72,6 +72,7 @@ import { CrmRecordBookingZone } from "./crm-booking-zone.js";
|
|
|
72
72
|
contentGutterY: true,
|
|
73
73
|
HeaderProps: {
|
|
74
74
|
action: /*#__PURE__*/ _jsxs(Stack, {
|
|
75
|
+
useFlexGap: true,
|
|
75
76
|
direction: "row",
|
|
76
77
|
spacing: 1,
|
|
77
78
|
sx: {
|
|
@@ -104,6 +105,7 @@ import { CrmRecordBookingZone } from "./crm-booking-zone.js";
|
|
|
104
105
|
spacing: 2,
|
|
105
106
|
children: [
|
|
106
107
|
hasChipRow ? /*#__PURE__*/ _jsxs(Stack, {
|
|
108
|
+
useFlexGap: true,
|
|
107
109
|
direction: "row",
|
|
108
110
|
spacing: 1,
|
|
109
111
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-record-header.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 { PageHeaderRecord } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay, type CardDisplayProps } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { Button, Chip, type ChipProps, Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\nimport {\n CrmRecordBookingZone,\n type CrmRecordBookingZoneProps,\n} from './crm-booking-zone'\n\nexport interface CrmRecordHeaderProps {\n /** What the record is — the lead card's heading: Contact, Company, Deal, Lead. */\n kind: string\n /**\n * The record's own name, published to the page heading and the trail.\n * Undefined until it has loaded, so the route's heading stands meanwhile.\n */\n title: string | undefined\n /** The one line under the heading — an address, a domain, a pipeline and stage. */\n subtitle?: ReactNode\n /** The record's facts as chips: a stage, a status, an owner. */\n chips?: ReactNode\n /** The way back to the section the record lives in. */\n backHref: string\n backLabel: string\n /** The primary controls, beside the menu. */\n actions?: ReactNode\n /** Everything else, behind the overflow — the destructive act lives here. */\n menuItems?: RowActionsMenuItem[]\n help?: CardDisplayProps['help']\n /** The record is still on its way; the chip row says so rather than sitting empty. */\n loading?: boolean\n /** What the card holds under the chips — a properties list, or a refusal. */\n children?: ReactNode\n /**\n * The **Book a meeting** action (AGL-2660), for a contact, a lead or a\n * deal: the record the link carries, and the site whose services are\n * offered. Drawn beside the primary controls, and only where the site has\n * a booking door — the action renders nothing otherwise.\n */\n booking?: CrmRecordBookingZoneProps\n}\n\n/**\n * THE HEADER EVERY CRM RECORD PAGE OPENS ON (AGL-2614).\n *\n * A contact, a company, a deal and a lead each had a lead card of their own\n * and no two agreed: one kept its controls in the card header and one at the\n * card's foot, one put the way back in a caption under the properties, one\n * offered Delete as a red button beside Edit and one behind a menu, and the\n * page heading was published by the page on three of them and never on the\n * fourth. A reader moving between the four records of one sale was reading\n * four products.\n *\n * ## The grammar\n *\n * ```\n * [ page heading and trail: the record's name ]\n * Kind [ Back to … ] [ primary ] [ ⋮ ]\n * subtitle line\n * [ chip ] [ chip ] [ chip ]\n * children\n * ```\n *\n * The page heading is the record's NAME — published from here, once, by\n * whichever component loaded the record, so the shell's heading and the\n * card read one document. The card is then free to say what the record IS\n * (`kind`) rather than repeating the name, and its subtitle carries the one\n * fact that identifies the record beyond its name: the address, the domain,\n * the pipeline and stage.\n *\n * Controls sit in the card HEADER, never at its foot: the header is where a\n * reader looks for what they can do to the thing they just opened, and a\n * foot of controls scrolls away under a long properties list. The way back\n * leads, because the record is the trail's last crumb and the crumb is not a\n * link. The primary act — Edit, Convert, Add to list — is a button; every\n * other act, and the destructive one in particular, is a menu item, so that\n * a delete is never one mis-click from the button beside it.\n */\nexport function CrmRecordHeader(props: CrmRecordHeaderProps) {\n const {\n kind,\n title,\n subtitle,\n chips,\n backHref,\n backLabel,\n actions,\n menuItems,\n help,\n loading,\n children,\n booking,\n } = props\n const hasChipRow = Boolean(chips) || Boolean(loading)\n return (\n <>\n <PageHeaderRecord title={title} />\n <CardDisplay\n header={kind}\n subheader={subtitle}\n help={help}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={backHref}\n size=\"small\"\n color=\"primary\"\n >\n {backLabel}\n </Button>\n {actions}\n {booking ? <CrmRecordBookingZone {...booking} /> : null}\n {menuItems?.length ? (\n <RowActionsMenu label={title || kind} items={menuItems} />\n ) : null}\n </Stack>\n ),\n }}\n >\n {hasChipRow || children ? (\n <Stack spacing={2}>\n {hasChipRow ? (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n {chips}\n {loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n {children}\n </Stack>\n ) : null}\n </CardDisplay>\n </>\n )\n}\nCrmRecordHeader.displayName = 'CrmRecordHeader'\n\nexport interface CrmRecordChipProps {\n /** What the fact is — Owner, Industry, Amount. */\n label: string\n /** The fact. Nothing is rendered without one, so a caller can pass it blindly. */\n value?: ReactNode\n color?: ChipProps['color']\n variant?: ChipProps['variant']\n}\n\n/**\n * One fact on the chip row, in the `Label: value` form every record uses.\n *\n * A chip with no value renders nothing rather than a bare label, so a\n * record page lists every fact it might have and the row shows the ones\n * this record does.\n */\nexport function CrmRecordChip(props: CrmRecordChipProps) {\n const { label, value, color, variant = 'outlined' } = props\n if (value === null || value === undefined || value === '') return null\n return (\n <Chip\n size=\"small\"\n variant={variant}\n color={color}\n label={\n <>\n <Typography component=\"span\" variant=\"inherit\" color=\"text.secondary\">\n {`${label}: `}\n </Typography>\n {value}\n </>\n }\n />\n )\n}\nCrmRecordChip.displayName = 'CrmRecordChip'\n\nexport default CrmRecordHeader\n"],"names":["PageHeaderRecord","AppLink","CardDisplay","RowActionsMenu","Button","Chip","Stack","Typography","CrmRecordBookingZone","CrmRecordHeader","props","kind","title","subtitle","chips","backHref","backLabel","actions","menuItems","help","loading","children","booking","hasChipRow","Boolean","header","subheader","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","sx","alignItems","flexWrap","rowGap","component","componentVariant","nativeButton","href","size","color","length","label","items","variant","displayName","CrmRecordChip","value","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,QAAQ,eAAc;AAC/C,SAASC,OAAO,EAAEC,WAAW,QAA+B,uBAAsB;AAClF,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,MAAM,EAAEC,IAAI,EAAkBC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAE/E,SACEC,oBAAoB,QAEf,wBAAoB;AAmC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,MAAM,EACJC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,KAAK,EACLC,QAAQ,EACRC,SAAS,EACTC,OAAO,EACPC,SAAS,EACTC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,OAAO,EACR,GAAGZ;IACJ,MAAMa,aAAaC,QAAQV,UAAUU,QAAQJ;IAC7C,qBACE;;0BACE,KAACpB;gBAAiBY,OAAOA;;0BACzB,KAACV;gBACCuB,QAAQd;gBACRe,WAAWb;gBACXM,MAAMA;gBACNQ,cAAc;gBACdC,cAAc;gBACdC,aAAa;oBACXC,sBACE,MAACxB;wBACCyB,WAAU;wBACVC,SAAS;wBACTC,IAAI;4BAAEC,YAAY;4BAAUC,UAAU;4BAAQC,QAAQ;wBAAE;;0CAExD,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-record-header.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 { PageHeaderRecord } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay, type CardDisplayProps } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { Button, Chip, type ChipProps, Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\nimport {\n CrmRecordBookingZone,\n type CrmRecordBookingZoneProps,\n} from './crm-booking-zone'\n\nexport interface CrmRecordHeaderProps {\n /** What the record is — the lead card's heading: Contact, Company, Deal, Lead. */\n kind: string\n /**\n * The record's own name, published to the page heading and the trail.\n * Undefined until it has loaded, so the route's heading stands meanwhile.\n */\n title: string | undefined\n /** The one line under the heading — an address, a domain, a pipeline and stage. */\n subtitle?: ReactNode\n /** The record's facts as chips: a stage, a status, an owner. */\n chips?: ReactNode\n /** The way back to the section the record lives in. */\n backHref: string\n backLabel: string\n /** The primary controls, beside the menu. */\n actions?: ReactNode\n /** Everything else, behind the overflow — the destructive act lives here. */\n menuItems?: RowActionsMenuItem[]\n help?: CardDisplayProps['help']\n /** The record is still on its way; the chip row says so rather than sitting empty. */\n loading?: boolean\n /** What the card holds under the chips — a properties list, or a refusal. */\n children?: ReactNode\n /**\n * The **Book a meeting** action (AGL-2660), for a contact, a lead or a\n * deal: the record the link carries, and the site whose services are\n * offered. Drawn beside the primary controls, and only where the site has\n * a booking door — the action renders nothing otherwise.\n */\n booking?: CrmRecordBookingZoneProps\n}\n\n/**\n * THE HEADER EVERY CRM RECORD PAGE OPENS ON (AGL-2614).\n *\n * A contact, a company, a deal and a lead each had a lead card of their own\n * and no two agreed: one kept its controls in the card header and one at the\n * card's foot, one put the way back in a caption under the properties, one\n * offered Delete as a red button beside Edit and one behind a menu, and the\n * page heading was published by the page on three of them and never on the\n * fourth. A reader moving between the four records of one sale was reading\n * four products.\n *\n * ## The grammar\n *\n * ```\n * [ page heading and trail: the record's name ]\n * Kind [ Back to … ] [ primary ] [ ⋮ ]\n * subtitle line\n * [ chip ] [ chip ] [ chip ]\n * children\n * ```\n *\n * The page heading is the record's NAME — published from here, once, by\n * whichever component loaded the record, so the shell's heading and the\n * card read one document. The card is then free to say what the record IS\n * (`kind`) rather than repeating the name, and its subtitle carries the one\n * fact that identifies the record beyond its name: the address, the domain,\n * the pipeline and stage.\n *\n * Controls sit in the card HEADER, never at its foot: the header is where a\n * reader looks for what they can do to the thing they just opened, and a\n * foot of controls scrolls away under a long properties list. The way back\n * leads, because the record is the trail's last crumb and the crumb is not a\n * link. The primary act — Edit, Convert, Add to list — is a button; every\n * other act, and the destructive one in particular, is a menu item, so that\n * a delete is never one mis-click from the button beside it.\n */\nexport function CrmRecordHeader(props: CrmRecordHeaderProps) {\n const {\n kind,\n title,\n subtitle,\n chips,\n backHref,\n backLabel,\n actions,\n menuItems,\n help,\n loading,\n children,\n booking,\n } = props\n const hasChipRow = Boolean(chips) || Boolean(loading)\n return (\n <>\n <PageHeaderRecord title={title} />\n <CardDisplay\n header={kind}\n subheader={subtitle}\n help={help}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={backHref}\n size=\"small\"\n color=\"primary\"\n >\n {backLabel}\n </Button>\n {actions}\n {booking ? <CrmRecordBookingZone {...booking} /> : null}\n {menuItems?.length ? (\n <RowActionsMenu label={title || kind} items={menuItems} />\n ) : null}\n </Stack>\n ),\n }}\n >\n {hasChipRow || children ? (\n <Stack spacing={2}>\n {hasChipRow ? (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n {chips}\n {loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n {children}\n </Stack>\n ) : null}\n </CardDisplay>\n </>\n )\n}\nCrmRecordHeader.displayName = 'CrmRecordHeader'\n\nexport interface CrmRecordChipProps {\n /** What the fact is — Owner, Industry, Amount. */\n label: string\n /** The fact. Nothing is rendered without one, so a caller can pass it blindly. */\n value?: ReactNode\n color?: ChipProps['color']\n variant?: ChipProps['variant']\n}\n\n/**\n * One fact on the chip row, in the `Label: value` form every record uses.\n *\n * A chip with no value renders nothing rather than a bare label, so a\n * record page lists every fact it might have and the row shows the ones\n * this record does.\n */\nexport function CrmRecordChip(props: CrmRecordChipProps) {\n const { label, value, color, variant = 'outlined' } = props\n if (value === null || value === undefined || value === '') return null\n return (\n <Chip\n size=\"small\"\n variant={variant}\n color={color}\n label={\n <>\n <Typography component=\"span\" variant=\"inherit\" color=\"text.secondary\">\n {`${label}: `}\n </Typography>\n {value}\n </>\n }\n />\n )\n}\nCrmRecordChip.displayName = 'CrmRecordChip'\n\nexport default CrmRecordHeader\n"],"names":["PageHeaderRecord","AppLink","CardDisplay","RowActionsMenu","Button","Chip","Stack","Typography","CrmRecordBookingZone","CrmRecordHeader","props","kind","title","subtitle","chips","backHref","backLabel","actions","menuItems","help","loading","children","booking","hasChipRow","Boolean","header","subheader","contentGutterX","contentGutterY","HeaderProps","action","useFlexGap","direction","spacing","sx","alignItems","flexWrap","rowGap","component","componentVariant","nativeButton","href","size","color","length","label","items","variant","displayName","CrmRecordChip","value","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,QAAQ,eAAc;AAC/C,SAASC,OAAO,EAAEC,WAAW,QAA+B,uBAAsB;AAClF,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,MAAM,EAAEC,IAAI,EAAkBC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAE/E,SACEC,oBAAoB,QAEf,wBAAoB;AAmC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,MAAM,EACJC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,KAAK,EACLC,QAAQ,EACRC,SAAS,EACTC,OAAO,EACPC,SAAS,EACTC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,OAAO,EACR,GAAGZ;IACJ,MAAMa,aAAaC,QAAQV,UAAUU,QAAQJ;IAC7C,qBACE;;0BACE,KAACpB;gBAAiBY,OAAOA;;0BACzB,KAACV;gBACCuB,QAAQd;gBACRe,WAAWb;gBACXM,MAAMA;gBACNQ,cAAc;gBACdC,cAAc;gBACdC,aAAa;oBACXC,sBACE,MAACxB;wBACCyB,UAAU;wBACVC,WAAU;wBACVC,SAAS;wBACTC,IAAI;4BAAEC,YAAY;4BAAUC,UAAU;4BAAQC,QAAQ;wBAAE;;0CAExD,KAACjC;gCACCkC,WAAWrC;+BACN;gCAAEsC,kBAAkB;gCAASC,cAAc;4BAAM;gCACtDC,MAAM1B;gCACN2B,MAAK;gCACLC,OAAM;0CAEL3B;;4BAEFC;4BACAK,wBAAU,KAACd,mCAAyBc,YAAc;4BAClDJ,CAAAA,6BAAAA,UAAW0B,MAAM,kBAChB,KAACzC;gCAAe0C,OAAOjC,SAASD;gCAAMmC,OAAO5B;iCAC3C;;;gBAGV;0BAECK,cAAcF,yBACb,MAACf;oBAAM2B,SAAS;;wBACbV,2BACC,MAACjB;4BACCyB,UAAU;4BACVC,WAAU;4BACVC,SAAS;4BACTC,IAAI;gCAAEC,YAAY;gCAAUC,UAAU;gCAAQC,QAAQ;4BAAE;;gCAEvDvB;gCACAM,wBACC,KAACb;oCAAWwC,SAAQ;oCAAQJ,OAAM;8CAC/B;qCAED;;6BAEJ;wBACHtB;;qBAED;;;;AAIZ;AACAZ,gBAAgBuC,WAAW,GAAG;AAW9B;;;;;;CAMC,GACD,OAAO,SAASC,cAAcvC,KAAyB;IACrD,MAAM,EAAEmC,KAAK,EAAEK,KAAK,EAAEP,KAAK,EAAEI,UAAU,UAAU,EAAE,GAAGrC;IACtD,IAAIwC,UAAU,QAAQA,UAAUC,aAAaD,UAAU,IAAI,OAAO;IAClE,qBACE,KAAC7C;QACCqC,MAAK;QACLK,SAASA;QACTJ,OAAOA;QACPE,qBACE;;8BACE,KAACtC;oBAAW+B,WAAU;oBAAOS,SAAQ;oBAAUJ,OAAM;8BAClD,GAAGE,MAAM,EAAE,CAAC;;gBAEdK;;;;AAKX;AACAD,cAAcD,WAAW,GAAG;AAE5B,eAAevC,gBAAe"}
|