@aglyn/plugins-crm 1.0.0-beta.293 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/activity-list.js +6 -1
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/contact-properties-card.js +8 -0
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/deal-board.d.ts +10 -0
- package/src/lib/components/deal-board.js +119 -61
- package/src/lib/components/deal-board.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +11 -7
- package/src/lib/components/deal-stage-card.js +99 -23
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +26 -14
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +7 -4
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -1
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/hooks/use-scroll-edges.d.ts +38 -0
- package/src/lib/hooks/use-scroll-edges.js +75 -0
- package/src/lib/hooks/use-scroll-edges.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.js +2 -8
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +7 -5
- package/src/lib/server/task-reminders-route.js +82 -15
- package/src/lib/server/task-reminders-route.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0
|
|
28
|
+
"@aglyn/aglyn": "1.0.0",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
-
import { mdiAccountGroupOutline, mdiDeleteOutline, mdiDotsHorizontalCircleOutline, mdiEmailOutline, mdiEmailReceiveOutline, mdiNoteTextOutline, mdiPencilOutline, mdiPhoneOutline } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { mdiAccountGroupOutline, mdiDeleteOutline, mdiDotsHorizontalCircleOutline, mdiEmailOutline, mdiEmailReceiveOutline, mdiFacebookMessenger, mdiInstagram, mdiLinkedin, mdiMessageTextOutline, mdiNoteTextOutline, mdiPencilOutline, mdiPhoneOutline, mdiWhatsapp } from "@aglyn/shared-data-mdi";
|
|
21
21
|
import { MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
23
23
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
@@ -36,6 +36,11 @@ import { TimelineEntry, useTimelineExpansion, useTimelinePages } from "./activit
|
|
|
36
36
|
*/ const KIND_ICONS = {
|
|
37
37
|
call: mdiPhoneOutline,
|
|
38
38
|
email: mdiEmailOutline,
|
|
39
|
+
text: mdiMessageTextOutline,
|
|
40
|
+
whatsapp: mdiWhatsapp,
|
|
41
|
+
messenger: mdiFacebookMessenger,
|
|
42
|
+
instagram: mdiInstagram,
|
|
43
|
+
linkedin: mdiLinkedin,
|
|
39
44
|
meeting: mdiAccountGroupOutline,
|
|
40
45
|
note: mdiNoteTextOutline,
|
|
41
46
|
other: mdiDotsHorizontalCircleOutline
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-list.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 { CrmActivityKind, CrmActivityRow } from '@aglyn/aglyn'\nimport {\n mdiAccountGroupOutline,\n mdiDeleteOutline,\n mdiDotsHorizontalCircleOutline,\n mdiEmailOutline,\n mdiEmailReceiveOutline,\n mdiNoteTextOutline,\n mdiPencilOutline,\n mdiPhoneOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } 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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { Chip, IconButton, Stack, Tooltip, Typography } from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { type ActivityScope, useCanEditActivity } from './activity-queries'\nimport {\n TimelineEntry,\n type TimelineExpansion,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\n\n/**\n * One glyph per kind. Typed against the union so a kind added to the list\n * cannot render with no icon — `MdiIcon` would fall back to its default\n * glyph silently, and a log where every note looks like a placeholder is\n * worse than a build error.\n */\nconst KIND_ICONS: Record<CrmActivityKind, { path: string }> = {\n call: mdiPhoneOutline,\n email: mdiEmailOutline,\n meeting: mdiAccountGroupOutline,\n note: mdiNoteTextOutline,\n other: mdiDotsHorizontalCircleOutline,\n}\n\n/** The icon for a kind — a stored kind outside the union draws as `other`. */\nexport function ActivityKindIcon(props: { kind: string; size?: number }) {\n const { kind, size = 1 } = props\n const icon = Aglyn.isCrmActivityKind(kind) ? KIND_ICONS[kind] : KIND_ICONS.other\n return <MdiIcon path={icon.path} size={size} />\n}\nActivityKindIcon.displayName = 'ActivityKindIcon'\n\n/**\n * The author's name for a row (AGL-2600).\n *\n * The denormalized `byName` leads: it is the only name a scoped colleague\n * can read, since a member document is readable by its subject and by\n * org-wide members alone. The signed-in user's OWN rows fall back to their\n * live name — the one case where a lookup is possible — and a row that\n * carries neither (written by a door that did not stamp one) reads as a\n * team member rather than as a uid.\n */\nexport function useActivityAuthorName(): (\n activity: Pick<CrmActivityRow, 'byUid' | 'byName'>,\n) => string {\n const { data: user } = useUser()\n const ownName = useUserName()\n const uid = user?.uid\n return useCallback(\n (activity) => {\n if (activity.byName) return activity.byName\n if (uid && activity.byUid === uid && ownName) return ownName\n return 'A team member'\n },\n [uid, ownName],\n )\n}\n\n/**\n * How long a note's own words may run on a collapsed row before it opens to\n * the rest; a single line under this reads whole, with nothing to open.\n */\nconst ACTIVITY_HEADING_MAX = 80\n\n/**\n * What a collapsed activity reads as, and whether opening it shows more.\n *\n * A subject is the heading and the body is what the row opens to. With no\n * subject the body's first line is the heading, and a body that runs past\n * that line, or past what one line holds, opens to the whole text.\n */\nexport function activityHeading(activity: Pick<CrmActivityRow, 'subject' | 'body'>): {\n heading: string\n opens: boolean\n} {\n const body = typeof activity.body === 'string' ? activity.body.trim() : ''\n const subject = typeof activity.subject === 'string' ? activity.subject.trim() : ''\n if (subject) return { heading: subject, opens: body !== '' }\n const firstLine = body.split('\\n').find((line) => line.trim() !== '')?.trim() ?? ''\n return {\n heading: firstLine,\n opens: body !== firstLine || firstLine.length > ACTIVITY_HEADING_MAX,\n }\n}\n\n/** \"45 min\" or \"2 h 05 min\" — a duration a manager reads at a glance. */\nfunction durationLabel(minutes: number): string {\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n return rest ? `${hours} h ${String(rest).padStart(2, '0')} min` : `${hours} h`\n}\n\nexport interface ActivityRowProps {\n activity: CrmActivityRow\n /** Where the delete is written, from `useActivityScope`. */\n scope: ActivityScope\n /** Opens the dialog on this activity. Absent, the row offers no edit. */\n onEdit?: (activity: CrmActivityRow) => void\n /**\n * What sits after the kind, before the body — a feed puts the record the\n * activity is about here, a record page needs nothing.\n */\n subject?: ReactNode\n /** The clock the relative time is read against; `Date.now()` when absent. */\n nowMs?: number\n /**\n * Whether THIS reader may edit and delete the row — the author, or an\n * org-wide member. Decided by whoever draws the rows, through\n * `useCanEditActivity`, once for the whole list: a row that asked for\n * itself would read the member document once per row.\n */\n editable?: boolean\n /**\n * Whether the row is open to its body. The list holds it so the card's\n * \"Expand all\" reaches every row; a row drawn alone holds its own.\n */\n expanded?: boolean\n onToggle?: () => void\n}\n\n/**\n * One logged activity: the kind's icon and label, what was said, how it\n * went, who logged it and how long ago — with edit and delete for whoever\n * may (AGL-2600).\n *\n * Collapsed, the row is its chips, its heading and its caption: the kind,\n * the direction and delivery state, how a call went, the subject or the\n * note's first line, and who, to or from whom, and when. Opened, it shows\n * the body — an email's whole text, a note's every line.\n *\n * Edit and delete appear only when `editable` says this reader may — the\n * author or an org-wide member. That is the console's verdict and not the\n * rules' — see `useCanEditActivity` for why the rules admit more — so the\n * controls are hidden rather than disabled: a disabled button asks \"why\n * not?\", and the honest answer would be \"you could, through the API\".\n */\nexport function ActivityRow(props: ActivityRowProps) {\n const { activity, scope, onEdit, subject, nowMs, editable } = props\n const [ownExpanded, setOwnExpanded] = useState(false)\n const expanded = props.expanded ?? ownExpanded\n const onToggle = props.onToggle ?? (() => setOwnExpanded((open) => !open))\n const { firestore, dataScope } = scope\n const authorName = useActivityAuthorName()\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n\n const handleDelete = useCallback(async () => {\n if (!dataScope) return\n const confirmed = await confirm({\n title: 'Delete this activity?',\n description:\n `This ${Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ].toLowerCase()} is removed from the record it was logged against. ` +\n 'Nothing the platform captured — a form, an order, a booking — is ' +\n 'affected.',\n confirmationText: 'Delete activity',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n )\n enqueueSnackbar('Activity deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [dataScope, confirm, activity, firestore, enqueueSnackbar])\n\n const label =\n Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ]\n /*\n * A message the platform SENT (AGL-2615) carries a subject, an address\n * and a delivery state the webhook advances; the state is the chip beside\n * the kind, red once the message did not land. What was sent is a record\n * of a fact and offers no edit — a rewritten body would misstate what\n * left — though whoever may delete a row may still delete this one.\n */\n const captured = typeof activity.messageId === 'string' && activity.messageId !== ''\n const deliveryState = Aglyn.isCrmEmailDeliveryState(activity.deliveryState)\n ? activity.deliveryState\n : null\n // A hand-logged email or call carries a direction too (AGL-3517); only a\n // message the platform carried or captured is \"sent\" or \"received\" here.\n const sent = activity.direction === 'outbound' && (deliveryState !== null || captured)\n /*\n * A message CAPTURED from a mailbox (AGL-2657) carries the provider's\n * Message-ID and, when a correspondent wrote it, the `inbound` direction.\n * It is drawn as its own thing — \"Received\" beside the kind, the sender\n * where a sent row shows the recipient, no author because nobody on the\n * team logged it — and a member's copied send reads \"Sent\" without a\n * delivery chip, since the platform did not carry it. Neither takes the\n * caller's \"Logged\" chip: nobody logged them.\n */\n const received = activity.direction === 'inbound' && captured\n /*\n * A hand-logged call or email names its direction in the kind's own chip\n * — \"Inbound call\", \"Outbound email\" (AGL-3517); a sent or captured\n * message says it with the chips below instead.\n */\n const kindChip =\n sent || received || captured\n ? label\n : Aglyn.crmActivityKindTitle(\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other',\n activity.direction,\n )\n const when = new Date(activity.atMs)\n const detail = [\n activity.outcome ? activity.outcome : null,\n typeof activity.durationMinutes === 'number' && activity.durationMinutes > 0\n ? durationLabel(activity.durationMinutes)\n : null,\n ].filter(Boolean)\n\n const { heading, opens } = activityHeading(activity)\n const hasSubject = typeof activity.subject === 'string' && activity.subject.trim() !== ''\n\n return (\n <TimelineEntry\n icon={\n received ? (\n <MdiIcon path={mdiEmailReceiveOutline.path} />\n ) : (\n <ActivityKindIcon kind={activity.kind} />\n )\n }\n label={heading || label.toLowerCase()}\n expanded={expanded}\n onToggle={onToggle}\n chips={\n <>\n <Chip label={kindChip} size=\"small\" />\n {received ? (\n <Chip\n label=\"Received\"\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n data-testid=\"activity-direction\"\n />\n ) : captured ? (\n <Chip\n label=\"Sent\"\n size=\"small\"\n variant=\"outlined\"\n data-testid=\"activity-direction\"\n />\n ) : null}\n {deliveryState ? (\n <Tooltip\n title={\n typeof activity.deliveryAtMs === 'number'\n ? new Date(activity.deliveryAtMs).toLocaleString()\n : ''\n }\n >\n <Chip\n label={Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState]}\n size=\"small\"\n variant=\"outlined\"\n color={Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default'}\n data-testid=\"activity-delivery-state\"\n />\n </Tooltip>\n ) : null}\n {captured ? null : subject}\n {detail.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {detail.join(' · ')}\n </Typography>\n ) : null}\n </>\n }\n // With no subject the heading IS the body's first line, so an open\n // row shows the body alone rather than its first line twice.\n title={hasSubject || !opens || !expanded ? heading : null}\n titleVariant={hasSubject ? 'subtitle2' : 'body2'}\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {[\n received ? null : authorName(activity),\n received && activity.from ? `from ${activity.from}` : null,\n sent && activity.to ? `to ${activity.to}` : null,\n Aglyn.activityTimeLabel(activity.atMs, nowMs ?? Date.now()),\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n </Tooltip>\n }\n body={\n opens ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>\n {activity.body}\n </Typography>\n ) : undefined\n }\n actions={\n editable ? (\n <>\n {onEdit && !sent && !received ? (\n <IconButton\n size=\"small\"\n aria-label=\"Edit activity\"\n onClick={() => onEdit(activity)}\n >\n <MdiIcon path={mdiPencilOutline.path} size={0.8} />\n </IconButton>\n ) : null}\n <IconButton\n size=\"small\"\n aria-label=\"Delete activity\"\n onClick={handleDelete}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </>\n ) : null\n }\n />\n )\n}\nActivityRow.displayName = 'ActivityRow'\n\nexport interface ActivityListProps {\n /** Newest-first, as the listener hands them back. */\n rows: readonly CrmActivityRow[]\n scope: ActivityScope\n onEdit?: (activity: CrmActivityRow) => void\n /** What the list says when there is nothing in it. */\n emptyText?: string\n /** The way out of an empty list — the record's \"Log activity\" button. */\n emptyAction?: ReactNode\n /** The listener holds less than exists; `onShowMore` widens it. */\n hasMore?: boolean\n onShowMore?: () => void\n /** The listener is reading, so an empty window is not yet an empty log. */\n loading?: boolean\n /** The record each row is about, for a list that spans records. */\n subjectFor?: (activity: CrmActivityRow) => ReactNode\n /**\n * No controls on any row, whoever is reading — and no member read to\n * decide them. A feed that spans records is a place to see what happened,\n * not to rewrite it; the record's own page is where an activity is\n * corrected, beside everything else about it.\n */\n readOnly?: boolean\n /**\n * Which rows are open, held by the card so its header's \"Expand all\"\n * reaches them. A list with no header control holds its own.\n */\n expansion?: TimelineExpansion\n}\n\n/**\n * A newest-first list of logged activities, collapsed to one entry each and\n * paged by the console's shared footer (AGL-2600).\n *\n * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the\n * list does not sort them again; a second sort here is a second place for\n * the order to be defined. The footer pages the window the listener holds,\n * and a page turned past it widens the listener while the probe row says\n * more exists (`useTimelinePages`).\n *\n * Who may edit is decided here, once, and handed to every row: the verdict\n * reads the member document, and the list is the one place that can ask\n * for it a single time however many rows it draws.\n */\nexport function ActivityList(props: ActivityListProps) {\n const {\n rows,\n scope,\n onEdit,\n emptyText = 'Nothing logged yet.',\n emptyAction,\n hasMore,\n onShowMore,\n loading,\n subjectFor,\n readOnly,\n } = props\n const canEdit = useCanEditActivity(scope.orgId, !readOnly)\n const ownExpansion = useTimelineExpansion()\n const expansion = props.expansion ?? ownExpansion\n const pages = useTimelinePages(rows.length, { hasMore, showMore: onShowMore, loading })\n // One clock for every row of one paint.\n const nowMs = Date.now()\n if (!rows.length) {\n return loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'Nothing logged yet'}\n description={emptyText}\n action={emptyAction}\n />\n )\n }\n return (\n <Stack spacing={2}>\n {pages.slice(rows).map((activity) => (\n <ActivityRow\n key={activity.$id}\n activity={activity}\n scope={scope}\n onEdit={onEdit}\n subject={subjectFor?.(activity)}\n nowMs={nowMs}\n editable={canEdit(activity)}\n expanded={expansion.isExpanded(activity.$id)}\n onToggle={() => expansion.toggle(activity.$id)}\n />\n ))}\n <ListPagination {...pages.footer} />\n </Stack>\n )\n}\nActivityList.displayName = 'ActivityList'\n\nexport default ActivityList\n"],"names":["Aglyn","mdiAccountGroupOutline","mdiDeleteOutline","mdiDotsHorizontalCircleOutline","mdiEmailOutline","mdiEmailReceiveOutline","mdiNoteTextOutline","mdiPencilOutline","mdiPhoneOutline","MdiIcon","useConfirmationContext","EmptyStateComponent","ListPagination","useSnackbar","useUser","useUserName","Chip","IconButton","Stack","Tooltip","Typography","deleteDoc","doc","useCallback","useState","useCanEditActivity","TimelineEntry","useTimelineExpansion","useTimelinePages","KIND_ICONS","call","email","meeting","note","other","ActivityKindIcon","props","kind","size","icon","isCrmActivityKind","path","displayName","useActivityAuthorName","data","user","ownName","uid","activity","byName","byUid","ACTIVITY_HEADING_MAX","activityHeading","body","trim","subject","heading","opens","firstLine","split","find","line","length","durationLabel","minutes","hours","Math","floor","rest","String","padStart","ActivityRow","scope","onEdit","nowMs","editable","ownExpanded","setOwnExpanded","expanded","onToggle","open","firestore","dataScope","authorName","confirm","enqueueSnackbar","handleDelete","confirmed","title","description","CRM_ACTIVITY_KIND_LABELS","toLowerCase","confirmationText","confirmationButtonProps","color","then","catch","CRM_COLLECTIONS","activities","$id","variant","persist","error","console","allowDuplicate","label","captured","messageId","deliveryState","isCrmEmailDeliveryState","sent","direction","received","kindChip","crmActivityKindTitle","when","Date","atMs","detail","outcome","durationMinutes","filter","Boolean","hasSubject","chips","data-testid","deliveryAtMs","toLocaleString","CRM_EMAIL_DELIVERY_STATE_LABELS","isCrmEmailDeliveryFailure","join","titleVariant","meta","sx","alignSelf","from","to","activityTimeLabel","now","whiteSpace","overflowWrap","undefined","actions","aria-label","onClick","ActivityList","rows","emptyText","emptyAction","hasMore","onShowMore","loading","subjectFor","readOnly","canEdit","orgId","ownExpansion","expansion","pages","showMore","compact","action","spacing","slice","map","isExpanded","toggle","footer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-list.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 { CrmActivityKind, CrmActivityRow } from '@aglyn/aglyn'\nimport {\n mdiAccountGroupOutline,\n mdiDeleteOutline,\n mdiDotsHorizontalCircleOutline,\n mdiEmailOutline,\n mdiEmailReceiveOutline,\n mdiFacebookMessenger,\n mdiInstagram,\n mdiLinkedin,\n mdiMessageTextOutline,\n mdiNoteTextOutline,\n mdiPencilOutline,\n mdiPhoneOutline,\n mdiWhatsapp,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } 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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { Chip, IconButton, Stack, Tooltip, Typography } from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { type ActivityScope, useCanEditActivity } from './activity-queries'\nimport {\n TimelineEntry,\n type TimelineExpansion,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\n\n/**\n * One glyph per kind. Typed against the union so a kind added to the list\n * cannot render with no icon — `MdiIcon` would fall back to its default\n * glyph silently, and a log where every note looks like a placeholder is\n * worse than a build error.\n */\nconst KIND_ICONS: Record<CrmActivityKind, { path: string }> = {\n call: mdiPhoneOutline,\n email: mdiEmailOutline,\n text: mdiMessageTextOutline,\n whatsapp: mdiWhatsapp,\n messenger: mdiFacebookMessenger,\n instagram: mdiInstagram,\n linkedin: mdiLinkedin,\n meeting: mdiAccountGroupOutline,\n note: mdiNoteTextOutline,\n other: mdiDotsHorizontalCircleOutline,\n}\n\n/** The icon for a kind — a stored kind outside the union draws as `other`. */\nexport function ActivityKindIcon(props: { kind: string; size?: number }) {\n const { kind, size = 1 } = props\n const icon = Aglyn.isCrmActivityKind(kind) ? KIND_ICONS[kind] : KIND_ICONS.other\n return <MdiIcon path={icon.path} size={size} />\n}\nActivityKindIcon.displayName = 'ActivityKindIcon'\n\n/**\n * The author's name for a row (AGL-2600).\n *\n * The denormalized `byName` leads: it is the only name a scoped colleague\n * can read, since a member document is readable by its subject and by\n * org-wide members alone. The signed-in user's OWN rows fall back to their\n * live name — the one case where a lookup is possible — and a row that\n * carries neither (written by a door that did not stamp one) reads as a\n * team member rather than as a uid.\n */\nexport function useActivityAuthorName(): (\n activity: Pick<CrmActivityRow, 'byUid' | 'byName'>,\n) => string {\n const { data: user } = useUser()\n const ownName = useUserName()\n const uid = user?.uid\n return useCallback(\n (activity) => {\n if (activity.byName) return activity.byName\n if (uid && activity.byUid === uid && ownName) return ownName\n return 'A team member'\n },\n [uid, ownName],\n )\n}\n\n/**\n * How long a note's own words may run on a collapsed row before it opens to\n * the rest; a single line under this reads whole, with nothing to open.\n */\nconst ACTIVITY_HEADING_MAX = 80\n\n/**\n * What a collapsed activity reads as, and whether opening it shows more.\n *\n * A subject is the heading and the body is what the row opens to. With no\n * subject the body's first line is the heading, and a body that runs past\n * that line, or past what one line holds, opens to the whole text.\n */\nexport function activityHeading(activity: Pick<CrmActivityRow, 'subject' | 'body'>): {\n heading: string\n opens: boolean\n} {\n const body = typeof activity.body === 'string' ? activity.body.trim() : ''\n const subject = typeof activity.subject === 'string' ? activity.subject.trim() : ''\n if (subject) return { heading: subject, opens: body !== '' }\n const firstLine = body.split('\\n').find((line) => line.trim() !== '')?.trim() ?? ''\n return {\n heading: firstLine,\n opens: body !== firstLine || firstLine.length > ACTIVITY_HEADING_MAX,\n }\n}\n\n/** \"45 min\" or \"2 h 05 min\" — a duration a manager reads at a glance. */\nfunction durationLabel(minutes: number): string {\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n return rest ? `${hours} h ${String(rest).padStart(2, '0')} min` : `${hours} h`\n}\n\nexport interface ActivityRowProps {\n activity: CrmActivityRow\n /** Where the delete is written, from `useActivityScope`. */\n scope: ActivityScope\n /** Opens the dialog on this activity. Absent, the row offers no edit. */\n onEdit?: (activity: CrmActivityRow) => void\n /**\n * What sits after the kind, before the body — a feed puts the record the\n * activity is about here, a record page needs nothing.\n */\n subject?: ReactNode\n /** The clock the relative time is read against; `Date.now()` when absent. */\n nowMs?: number\n /**\n * Whether THIS reader may edit and delete the row — the author, or an\n * org-wide member. Decided by whoever draws the rows, through\n * `useCanEditActivity`, once for the whole list: a row that asked for\n * itself would read the member document once per row.\n */\n editable?: boolean\n /**\n * Whether the row is open to its body. The list holds it so the card's\n * \"Expand all\" reaches every row; a row drawn alone holds its own.\n */\n expanded?: boolean\n onToggle?: () => void\n}\n\n/**\n * One logged activity: the kind's icon and label, what was said, how it\n * went, who logged it and how long ago — with edit and delete for whoever\n * may (AGL-2600).\n *\n * Collapsed, the row is its chips, its heading and its caption: the kind,\n * the direction and delivery state, how a call went, the subject or the\n * note's first line, and who, to or from whom, and when. Opened, it shows\n * the body — an email's whole text, a note's every line.\n *\n * Edit and delete appear only when `editable` says this reader may — the\n * author or an org-wide member. That is the console's verdict and not the\n * rules' — see `useCanEditActivity` for why the rules admit more — so the\n * controls are hidden rather than disabled: a disabled button asks \"why\n * not?\", and the honest answer would be \"you could, through the API\".\n */\nexport function ActivityRow(props: ActivityRowProps) {\n const { activity, scope, onEdit, subject, nowMs, editable } = props\n const [ownExpanded, setOwnExpanded] = useState(false)\n const expanded = props.expanded ?? ownExpanded\n const onToggle = props.onToggle ?? (() => setOwnExpanded((open) => !open))\n const { firestore, dataScope } = scope\n const authorName = useActivityAuthorName()\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n\n const handleDelete = useCallback(async () => {\n if (!dataScope) return\n const confirmed = await confirm({\n title: 'Delete this activity?',\n description:\n `This ${Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ].toLowerCase()} is removed from the record it was logged against. ` +\n 'Nothing the platform captured — a form, an order, a booking — is ' +\n 'affected.',\n confirmationText: 'Delete activity',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n )\n enqueueSnackbar('Activity deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [dataScope, confirm, activity, firestore, enqueueSnackbar])\n\n const label =\n Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ]\n /*\n * A message the platform SENT (AGL-2615) carries a subject, an address\n * and a delivery state the webhook advances; the state is the chip beside\n * the kind, red once the message did not land. What was sent is a record\n * of a fact and offers no edit — a rewritten body would misstate what\n * left — though whoever may delete a row may still delete this one.\n */\n const captured = typeof activity.messageId === 'string' && activity.messageId !== ''\n const deliveryState = Aglyn.isCrmEmailDeliveryState(activity.deliveryState)\n ? activity.deliveryState\n : null\n // A hand-logged email or call carries a direction too (AGL-3517); only a\n // message the platform carried or captured is \"sent\" or \"received\" here.\n const sent = activity.direction === 'outbound' && (deliveryState !== null || captured)\n /*\n * A message CAPTURED from a mailbox (AGL-2657) carries the provider's\n * Message-ID and, when a correspondent wrote it, the `inbound` direction.\n * It is drawn as its own thing — \"Received\" beside the kind, the sender\n * where a sent row shows the recipient, no author because nobody on the\n * team logged it — and a member's copied send reads \"Sent\" without a\n * delivery chip, since the platform did not carry it. Neither takes the\n * caller's \"Logged\" chip: nobody logged them.\n */\n const received = activity.direction === 'inbound' && captured\n /*\n * A hand-logged call or email names its direction in the kind's own chip\n * — \"Inbound call\", \"Outbound email\" (AGL-3517); a sent or captured\n * message says it with the chips below instead.\n */\n const kindChip =\n sent || received || captured\n ? label\n : Aglyn.crmActivityKindTitle(\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other',\n activity.direction,\n )\n const when = new Date(activity.atMs)\n const detail = [\n activity.outcome ? activity.outcome : null,\n typeof activity.durationMinutes === 'number' && activity.durationMinutes > 0\n ? durationLabel(activity.durationMinutes)\n : null,\n ].filter(Boolean)\n\n const { heading, opens } = activityHeading(activity)\n const hasSubject = typeof activity.subject === 'string' && activity.subject.trim() !== ''\n\n return (\n <TimelineEntry\n icon={\n received ? (\n <MdiIcon path={mdiEmailReceiveOutline.path} />\n ) : (\n <ActivityKindIcon kind={activity.kind} />\n )\n }\n label={heading || label.toLowerCase()}\n expanded={expanded}\n onToggle={onToggle}\n chips={\n <>\n <Chip label={kindChip} size=\"small\" />\n {received ? (\n <Chip\n label=\"Received\"\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n data-testid=\"activity-direction\"\n />\n ) : captured ? (\n <Chip\n label=\"Sent\"\n size=\"small\"\n variant=\"outlined\"\n data-testid=\"activity-direction\"\n />\n ) : null}\n {deliveryState ? (\n <Tooltip\n title={\n typeof activity.deliveryAtMs === 'number'\n ? new Date(activity.deliveryAtMs).toLocaleString()\n : ''\n }\n >\n <Chip\n label={Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState]}\n size=\"small\"\n variant=\"outlined\"\n color={Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default'}\n data-testid=\"activity-delivery-state\"\n />\n </Tooltip>\n ) : null}\n {captured ? null : subject}\n {detail.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {detail.join(' · ')}\n </Typography>\n ) : null}\n </>\n }\n // With no subject the heading IS the body's first line, so an open\n // row shows the body alone rather than its first line twice.\n title={hasSubject || !opens || !expanded ? heading : null}\n titleVariant={hasSubject ? 'subtitle2' : 'body2'}\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {[\n received ? null : authorName(activity),\n received && activity.from ? `from ${activity.from}` : null,\n sent && activity.to ? `to ${activity.to}` : null,\n Aglyn.activityTimeLabel(activity.atMs, nowMs ?? Date.now()),\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n </Tooltip>\n }\n body={\n opens ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>\n {activity.body}\n </Typography>\n ) : undefined\n }\n actions={\n editable ? (\n <>\n {onEdit && !sent && !received ? (\n <IconButton\n size=\"small\"\n aria-label=\"Edit activity\"\n onClick={() => onEdit(activity)}\n >\n <MdiIcon path={mdiPencilOutline.path} size={0.8} />\n </IconButton>\n ) : null}\n <IconButton\n size=\"small\"\n aria-label=\"Delete activity\"\n onClick={handleDelete}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </>\n ) : null\n }\n />\n )\n}\nActivityRow.displayName = 'ActivityRow'\n\nexport interface ActivityListProps {\n /** Newest-first, as the listener hands them back. */\n rows: readonly CrmActivityRow[]\n scope: ActivityScope\n onEdit?: (activity: CrmActivityRow) => void\n /** What the list says when there is nothing in it. */\n emptyText?: string\n /** The way out of an empty list — the record's \"Log activity\" button. */\n emptyAction?: ReactNode\n /** The listener holds less than exists; `onShowMore` widens it. */\n hasMore?: boolean\n onShowMore?: () => void\n /** The listener is reading, so an empty window is not yet an empty log. */\n loading?: boolean\n /** The record each row is about, for a list that spans records. */\n subjectFor?: (activity: CrmActivityRow) => ReactNode\n /**\n * No controls on any row, whoever is reading — and no member read to\n * decide them. A feed that spans records is a place to see what happened,\n * not to rewrite it; the record's own page is where an activity is\n * corrected, beside everything else about it.\n */\n readOnly?: boolean\n /**\n * Which rows are open, held by the card so its header's \"Expand all\"\n * reaches them. A list with no header control holds its own.\n */\n expansion?: TimelineExpansion\n}\n\n/**\n * A newest-first list of logged activities, collapsed to one entry each and\n * paged by the console's shared footer (AGL-2600).\n *\n * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the\n * list does not sort them again; a second sort here is a second place for\n * the order to be defined. The footer pages the window the listener holds,\n * and a page turned past it widens the listener while the probe row says\n * more exists (`useTimelinePages`).\n *\n * Who may edit is decided here, once, and handed to every row: the verdict\n * reads the member document, and the list is the one place that can ask\n * for it a single time however many rows it draws.\n */\nexport function ActivityList(props: ActivityListProps) {\n const {\n rows,\n scope,\n onEdit,\n emptyText = 'Nothing logged yet.',\n emptyAction,\n hasMore,\n onShowMore,\n loading,\n subjectFor,\n readOnly,\n } = props\n const canEdit = useCanEditActivity(scope.orgId, !readOnly)\n const ownExpansion = useTimelineExpansion()\n const expansion = props.expansion ?? ownExpansion\n const pages = useTimelinePages(rows.length, { hasMore, showMore: onShowMore, loading })\n // One clock for every row of one paint.\n const nowMs = Date.now()\n if (!rows.length) {\n return loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'Nothing logged yet'}\n description={emptyText}\n action={emptyAction}\n />\n )\n }\n return (\n <Stack spacing={2}>\n {pages.slice(rows).map((activity) => (\n <ActivityRow\n key={activity.$id}\n activity={activity}\n scope={scope}\n onEdit={onEdit}\n subject={subjectFor?.(activity)}\n nowMs={nowMs}\n editable={canEdit(activity)}\n expanded={expansion.isExpanded(activity.$id)}\n onToggle={() => expansion.toggle(activity.$id)}\n />\n ))}\n <ListPagination {...pages.footer} />\n </Stack>\n )\n}\nActivityList.displayName = 'ActivityList'\n\nexport default ActivityList\n"],"names":["Aglyn","mdiAccountGroupOutline","mdiDeleteOutline","mdiDotsHorizontalCircleOutline","mdiEmailOutline","mdiEmailReceiveOutline","mdiFacebookMessenger","mdiInstagram","mdiLinkedin","mdiMessageTextOutline","mdiNoteTextOutline","mdiPencilOutline","mdiPhoneOutline","mdiWhatsapp","MdiIcon","useConfirmationContext","EmptyStateComponent","ListPagination","useSnackbar","useUser","useUserName","Chip","IconButton","Stack","Tooltip","Typography","deleteDoc","doc","useCallback","useState","useCanEditActivity","TimelineEntry","useTimelineExpansion","useTimelinePages","KIND_ICONS","call","email","text","whatsapp","messenger","instagram","linkedin","meeting","note","other","ActivityKindIcon","props","kind","size","icon","isCrmActivityKind","path","displayName","useActivityAuthorName","data","user","ownName","uid","activity","byName","byUid","ACTIVITY_HEADING_MAX","activityHeading","body","trim","subject","heading","opens","firstLine","split","find","line","length","durationLabel","minutes","hours","Math","floor","rest","String","padStart","ActivityRow","scope","onEdit","nowMs","editable","ownExpanded","setOwnExpanded","expanded","onToggle","open","firestore","dataScope","authorName","confirm","enqueueSnackbar","handleDelete","confirmed","title","description","CRM_ACTIVITY_KIND_LABELS","toLowerCase","confirmationText","confirmationButtonProps","color","then","catch","CRM_COLLECTIONS","activities","$id","variant","persist","error","console","allowDuplicate","label","captured","messageId","deliveryState","isCrmEmailDeliveryState","sent","direction","received","kindChip","crmActivityKindTitle","when","Date","atMs","detail","outcome","durationMinutes","filter","Boolean","hasSubject","chips","data-testid","deliveryAtMs","toLocaleString","CRM_EMAIL_DELIVERY_STATE_LABELS","isCrmEmailDeliveryFailure","join","titleVariant","meta","sx","alignSelf","from","to","activityTimeLabel","now","whiteSpace","overflowWrap","undefined","actions","aria-label","onClick","ActivityList","rows","emptyText","emptyAction","hasMore","onShowMore","loading","subjectFor","readOnly","canEdit","orgId","ownExpansion","expansion","pages","showMore","compact","action","spacing","slice","map","isExpanded","toggle","footer"],"mappings":"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"}
|
|
@@ -334,6 +334,14 @@ export function ContactPropertiesCard(props) {
|
|
|
334
334
|
});
|
|
335
335
|
// The custom values are stored; the controls read the record again.
|
|
336
336
|
setCustom({});
|
|
337
|
+
if (stageChanged && !siteHostId) {
|
|
338
|
+
// The stage lives on a site's facet, and this person has none. Never
|
|
339
|
+
// "Contact saved" over a change that went nowhere (AGL-4120).
|
|
340
|
+
return void enqueueSnackbar('Saved, but the stage could not be changed: no site has captured this contact yet.', {
|
|
341
|
+
variant: 'warning',
|
|
342
|
+
persist: false
|
|
343
|
+
});
|
|
344
|
+
}
|
|
337
345
|
if (stageChanged && siteHostId) {
|
|
338
346
|
try {
|
|
339
347
|
await setContactStage(user, siteHostId, record.$id, lifecycleStage || null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-properties-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 {\n type AglynOrgBilling,\n composeContactName,\n CONTACT_BIRTHDATE_REFUSAL,\n CONTACT_EXTRA_PHONE_FIELDS,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_NAME_PART_MAX,\n CONTACT_PHONE_FIELD_LABELS,\n type ConsentGroup,\n type ContactLifecycleStage,\n CRM_SALUTATION_PICKLIST,\n crmTelHref,\n normalizeContactBirthdate,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n type CrmCustomDraft,\n crmCustomDraftChanges,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n} from '../model/crm-custom-draft'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { ContactReportsToField } from './contact-reports-to-field'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmPicklistSelect } from './picklist-select'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport { LeadSourceSelect } from './lead-source-select'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /**\n * The org the record belongs to, as the page resolved it — for the org's\n * lead source values (AGL-3298) and its custom contact fields (AGL-2601).\n * Null while it settles, when the select offers the starter list, no\n * custom field is drawn yet, and the save still sends only what changed.\n */\n orgId?: string | null\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too. The\n * custom fields are the CRM's as well, and are not drawn.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save in its header (AGL-2596).\n *\n * ## Custom fields sit with the built-in ones\n *\n * The org's own contact fields (AGL-2601) are drawn under **More fields**,\n * after the built-in properties, the way Salesforce keeps custom fields in\n * the record's Details beside the standard ones — and they save with them.\n * An org with no active field draws no subsection at all.\n *\n * A custom value is THIS holder's (`facets.{group}.custom.{key}`), so the\n * draft holds only the keys the reader touched and the save sends only the\n * keys that changed: the route writes each at its own dotted path, and a\n * `custom` map sent whole would take every key this card did not touch out\n * with it. A cleared control sends `null`, the explicit \"cleared\" the model\n * keeps the key present with, and a required field left empty is refused\n * before anything is sent.\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Salesforce's sections (AGL-3515)\n *\n * The fields are grouped the way Salesforce's contact Details groups them —\n * Contact information, Phones, Addresses, Additional information — and every\n * one is still one draft and one Save. While the holder keeps a first or\n * last name the Name field shows their composition and is read-only; the\n * save sends the parts, and the route composes the name from them. The\n * salutation and the reports-to are sent only when they change, for the\n * lead source's reason: the route judges a sent one. Do not call is a hint\n * on every phone's call link, never a block.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\n/** The phone fields the card edits, in the order the Phones section lists them. */\nconst PHONE_FIELDS = ['phone', ...CONTACT_EXTRA_PHONE_FIELDS] as const\ntype PhoneField = (typeof PHONE_FIELDS)[number]\ntype PhoneDrafts = Record<PhoneField, string>\n\nconst phoneDraftsFrom = (record: ContactRecord): PhoneDrafts =>\n Object.fromEntries(\n PHONE_FIELDS.map((field) => [field, String(record[field] ?? '')]),\n ) as PhoneDrafts\n\n/** The phones as the route stores them, or the fields that cannot be read as numbers. */\nfunction readPhones(\n drafts: PhoneDrafts,\n): { ok: true; phones: PhoneDrafts } | { ok: false; errors: Partial<PhoneDrafts> } {\n const phones = {} as PhoneDrafts\n const errors: Partial<PhoneDrafts> = {}\n for (const field of PHONE_FIELDS) {\n const typed = drafts[field].trim()\n const normalized = typed ? normalizePhone(typed) : ''\n if (normalized === null) errors[field] = PHONE_HELP_ERROR\n else phones[field] = normalized\n }\n return Object.keys(errors).length ? { ok: false, errors } : { ok: true, phones }\n}\n\nconst PHONE_HELP = 'With the country code, like +1 512 555 0107'\nconst PHONE_HELP_ERROR = 'Enter it with its country code, like +1 512 555 0107.'\n\n/** A section heading inside the card — Salesforce's Details groups its fields the same way. */\nfunction SectionHeading(props: { children: string }) {\n return (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\" color=\"text.secondary\">\n {props.children}\n </Typography>\n </Grid>\n )\n}\n\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n orgId = null,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [salutation, setSalutation] = useState(record.salutation ?? '')\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, orgId)\n const [firstName, setFirstName] = useState(record.firstName ?? '')\n const [lastName, setLastName] = useState(record.lastName ?? '')\n const [phones, setPhones] = useState<PhoneDrafts>(() => phoneDraftsFrom(record))\n const [doNotCall, setDoNotCall] = useState(record.doNotCall === true)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [department, setDepartment] = useState(record.department ?? '')\n const [birthdate, setBirthdate] = useState(record.birthdate ?? '')\n const [assistantName, setAssistantName] = useState(record.assistantName ?? '')\n const [reportsTo, setReportsTo] = useState(record.reportsToContactId ?? '')\n // Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.\n const [leadSource, setLeadSource] = useState(record.leadSource)\n const leadSources = useLeadSourcePicklist(orgId)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [otherAddress, setOtherAddress] = useState<AddressDraft>(\n addressDraftFrom(record.otherAddress),\n )\n const [phoneErrors, setPhoneErrors] = useState<Partial<PhoneDrafts>>({})\n const [birthdateError, setBirthdateError] = useState('')\n const [saving, setSaving] = useState(false)\n /** The custom values the reader touched; every other key reads from the record. */\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /** Something was edited since the card was seeded or last saved. */\n const [edited, setEdited] = useState(false)\n const fields = useContactFieldDefinitions(suiteLocked ? null : orgId)\n // The custom fields are the CRM's; a plan without it draws and sends none.\n const customFields = suiteLocked ? [] : fields.active\n const storedCustom = useMemo(() => record.custom ?? {}, [record.custom])\n const customChanges = useMemo(\n () => crmCustomDraftChanges(storedCustom, custom),\n [storedCustom, custom],\n )\n const clearingRequired =\n crmCustomDraftMissingRequired(customFields, storedCustom, custom, 'edit').length > 0\n /*\n * The name the first and last names make (AGL-3515). While there is one,\n * it IS the name — the Name field shows it, read-only, and the save sends\n * the parts rather than a name the route would have to reconcile.\n */\n const composedName = composeContactName(firstName, lastName)\n\n /** One field's edit: the value, and the card now has something to save. */\n const change = useCallback(<T,>(set: (value: T) => void, value: T) => {\n set(value)\n setEdited(true)\n }, [])\n\n /** Every draft back to what the record holds — the seed, and Discard. */\n const reseed = () => {\n setNameOverride(record.nameOverride)\n setSalutation(record.salutation ?? '')\n setFirstName(record.firstName ?? '')\n setLastName(record.lastName ?? '')\n setPhones(phoneDraftsFrom(record))\n setDoNotCall(record.doNotCall === true)\n setJobTitle(record.jobTitle)\n setDepartment(record.department ?? '')\n setBirthdate(record.birthdate ?? '')\n setAssistantName(record.assistantName ?? '')\n setReportsTo(record.reportsToContactId ?? '')\n setLeadSource(record.leadSource)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setOtherAddress(addressDraftFrom(record.otherAddress))\n setPhoneErrors({})\n setBirthdateError('')\n setCustom({})\n setEdited(false)\n }\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n reseed()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n if (clearingRequired) return\n const readPhonesResult = readPhones(phones)\n const storedBirthdate = normalizeContactBirthdate(birthdate)\n setPhoneErrors('errors' in readPhonesResult ? readPhonesResult.errors : {})\n setBirthdateError(storedBirthdate === null ? CONTACT_BIRTHDATE_REFUSAL : '')\n if ('errors' in readPhonesResult || storedBirthdate === null) return\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n // The name is the parts' while there are parts — see the note above.\n ...(composedName ? {} : { name: nameOverride.trim().slice(0, 120) }),\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n ...readPhonesResult.phones,\n // Sent only when it moves: the route stores the flag as a fact about\n // the person, and an untouched card should not re-assert it.\n ...(doNotCall !== (record.doNotCall === true) ? { doNotCall } : {}),\n jobTitle: jobTitle.trim().slice(0, 120),\n department: department.trim().slice(0, 120),\n birthdate: storedBirthdate,\n assistantName: assistantName.trim().slice(0, 120),\n // Sent only when changed: the route judges a sent value against the\n // org's list, and an untouched one needs no judging. The reports-to\n // likewise, because the route checks a sent one for a loop.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n ...(salutation !== (record.salutation ?? '') ? { salutation } : {}),\n ...(reportsTo !== (record.reportsToContactId ?? '')\n ? { reportsToContactId: reportsTo || null }\n : {}),\n address,\n otherAddress,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n // Only the custom keys that changed — see the note above.\n ...(!suiteLocked && customChanges.length\n ? { custom: Object.fromEntries(customChanges) }\n : {}),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n // The custom values are stored; the controls read the record again.\n setCustom({})\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n setEdited(false)\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n assistantName,\n birthdate,\n clearingRequired,\n companyId,\n companyName,\n composedName,\n contactUpdate,\n customChanges,\n department,\n doNotCall,\n enqueueSnackbar,\n firstName,\n siteHostId,\n jobTitle,\n lastName,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n otherAddress,\n ownerUid,\n phones,\n record.$id,\n record.doNotCall,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n record.reportsToContactId,\n record.salutation,\n reportsTo,\n salutation,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** One phone field, with click-to-call off what is IN the box (AGL-2661). */\n const phoneField = (field: PhoneField) => {\n const value = phones[field]\n const telHref = crmTelHref(value)\n const callLabel = doNotCall\n ? `Call ${value.trim()} — marked do not call`\n : `Call ${value.trim()}`\n return (\n <Grid key={field} size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label={CONTACT_PHONE_FIELD_LABELS[field]}\n value={value}\n onChange={(event) => change(setPhones, { ...phones, [field]: event.target.value })}\n error={Boolean(phoneErrors[field])}\n helperText={phoneErrors[field] || (field === 'phone' ? PHONE_HELP : undefined)}\n fullWidth\n slotProps={{\n input: {\n /*\n * The number on screen is the one a reader means to ring, and\n * `crmTelHref` withholds the link from anything half-typed. A\n * do-not-call person keeps the link — the rep decides — with\n * the request said in the tooltip and the icon's color.\n */\n endAdornment:\n telHref && field !== 'fax' ? (\n <InputAdornment position=\"end\">\n <Tooltip title={callLabel}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n color={doNotCall ? 'warning' : 'default'}\n aria-label={callLabel}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n )\n }\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#contact-properties' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {edited ? (\n <Button size=\"small\" disabled={saving} onClick={reseed}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving || !edited || clearingRequired}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Grid container spacing={2}>\n <SectionHeading>{'Contact information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n stored={record.salutation ?? ''}\n onChange={(value) => change(setSalutation, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => change(setFirstName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => change(setLastName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || nameOverride}\n onChange={(event) => change(setNameOverride, event.target.value)}\n slotProps={{\n htmlInput: { maxLength: 120 },\n input: { readOnly: Boolean(composedName) },\n }}\n helperText={\n composedName\n ? 'Made of the first and last name.'\n : record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => change(setJobTitle, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => change(setDepartment, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n change(setCompanyId, id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={leadSource}\n stored={record.leadSource}\n onChange={(value) => change(setLeadSource, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n change(setLifecycleStage, event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => change(setOwnerUid, event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => change(setTags, event.target.value)}\n fullWidth\n />\n </Grid>\n\n <SectionHeading>{'Phones'}</SectionHeading>\n {PHONE_FIELDS.filter((field) => field !== 'assistantPhone').map(phoneField)}\n <Grid size={{ xs: 12, sm: 6 }}>\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => change(setDoNotCall, event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <FormHelperText>\n {'They asked not to be phoned. Every number still dials; the Call button says so.'}\n </FormHelperText>\n </Grid>\n\n <SectionHeading>{'Addresses'}</SectionHeading>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields\n value={address}\n onChange={(value) => change(setAddress, value)}\n />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Other address'}</Typography>\n <ContactAddressFields\n value={otherAddress}\n onChange={(value) => change(setOtherAddress, value)}\n />\n </Stack>\n </Grid>\n\n <SectionHeading>{'Additional information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n type=\"date\"\n label=\"Birthdate\"\n value={birthdate}\n onChange={(event) => change(setBirthdate, event.target.value)}\n error={Boolean(birthdateError)}\n helperText={birthdateError || undefined}\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <ContactReportsToField\n hostId={hostId}\n org={org}\n contactId={record.$id}\n groupId={consentGroup.groupId}\n value={reportsTo}\n onChange={(value) => change(setReportsTo, value)}\n disabled={saving}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Assistant\"\n value={assistantName}\n onChange={(event) => change(setAssistantName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n {phoneField('assistantPhone')}\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => change(setNotes, event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n {/*\n The org's own fields (AGL-2601), after the built-in ones and saved\n with them. Absent, not an empty box, while the org defines none.\n */}\n {customFields.length ? (\n <>\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n </Grid>\n {customFields.map((definition) => (\n <Grid key={definition.$id} size={{ xs: 12, sm: 6 }}>\n <CrmCustomFieldControl\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n change(setCustom, { ...custom, [definition.key]: value })\n }\n disabled={saving}\n />\n </Grid>\n ))}\n {clearingRequired ? (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"caption\" color=\"error\">\n {'A required field cannot be left empty.'}\n </Typography>\n </Grid>\n ) : null}\n </>\n ) : null}\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","composeContactName","CONTACT_BIRTHDATE_REFUSAL","CONTACT_EXTRA_PHONE_FIELDS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_NAME_PART_MAX","CONTACT_PHONE_FIELD_LABELS","CRM_SALUTATION_PICKLIST","crmTelHref","normalizeContactBirthdate","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Checkbox","FormControlLabel","FormHelperText","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useContactUpdate","useCrmActivityLogger","useCrmPicklist","useLeadSourcePicklist","parseContactTags","setContactStage","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","ContactReportsToField","CrmCustomFieldControl","CrmPicklistSelect","crmSuiteLockedReason","LeadSourceSelect","PHONE_FIELDS","phoneDraftsFrom","record","Object","fromEntries","map","field","String","readPhones","drafts","phones","errors","typed","trim","normalized","PHONE_HELP_ERROR","keys","length","ok","PHONE_HELP","SectionHeading","props","size","xs","variant","color","children","ContactPropertiesCard","hostId","org","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","salutation","setSalutation","salutations","firstName","setFirstName","lastName","setLastName","setPhones","doNotCall","setDoNotCall","jobTitle","setJobTitle","department","setDepartment","birthdate","setBirthdate","assistantName","setAssistantName","reportsTo","setReportsTo","reportsToContactId","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","otherAddress","setOtherAddress","phoneErrors","setPhoneErrors","birthdateError","setBirthdateError","saving","setSaving","custom","setCustom","edited","setEdited","fields","customFields","active","storedCustom","customChanges","clearingRequired","composedName","change","set","value","reseed","recordId","$id","handleSave","readPhonesResult","storedBirthdate","stageChanged","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","message","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","phoneField","telHref","callLabel","sm","label","onChange","event","target","Boolean","helperText","undefined","fullWidth","slotProps","input","endAdornment","position","title","component","href","edge","aria-label","path","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","disabled","onClick","container","readOnly","picklistId","picklist","stored","htmlInput","maxLength","canonicalName","ready","truncated","company","onCreate","fallbackName","select","stage","memberName","owner","crmMemberPickerLabel","placeholder","filter","control","checked","inputLabel","shrink","contactId","groupId","multiline","minRows","definition","key","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-properties-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 {\n type AglynOrgBilling,\n composeContactName,\n CONTACT_BIRTHDATE_REFUSAL,\n CONTACT_EXTRA_PHONE_FIELDS,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_NAME_PART_MAX,\n CONTACT_PHONE_FIELD_LABELS,\n type ConsentGroup,\n type ContactLifecycleStage,\n CRM_SALUTATION_PICKLIST,\n crmTelHref,\n normalizeContactBirthdate,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n type CrmCustomDraft,\n crmCustomDraftChanges,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n} from '../model/crm-custom-draft'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { ContactReportsToField } from './contact-reports-to-field'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmPicklistSelect } from './picklist-select'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport { LeadSourceSelect } from './lead-source-select'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /**\n * The org the record belongs to, as the page resolved it — for the org's\n * lead source values (AGL-3298) and its custom contact fields (AGL-2601).\n * Null while it settles, when the select offers the starter list, no\n * custom field is drawn yet, and the save still sends only what changed.\n */\n orgId?: string | null\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too. The\n * custom fields are the CRM's as well, and are not drawn.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save in its header (AGL-2596).\n *\n * ## Custom fields sit with the built-in ones\n *\n * The org's own contact fields (AGL-2601) are drawn under **More fields**,\n * after the built-in properties, the way Salesforce keeps custom fields in\n * the record's Details beside the standard ones — and they save with them.\n * An org with no active field draws no subsection at all.\n *\n * A custom value is THIS holder's (`facets.{group}.custom.{key}`), so the\n * draft holds only the keys the reader touched and the save sends only the\n * keys that changed: the route writes each at its own dotted path, and a\n * `custom` map sent whole would take every key this card did not touch out\n * with it. A cleared control sends `null`, the explicit \"cleared\" the model\n * keeps the key present with, and a required field left empty is refused\n * before anything is sent.\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Salesforce's sections (AGL-3515)\n *\n * The fields are grouped the way Salesforce's contact Details groups them —\n * Contact information, Phones, Addresses, Additional information — and every\n * one is still one draft and one Save. While the holder keeps a first or\n * last name the Name field shows their composition and is read-only; the\n * save sends the parts, and the route composes the name from them. The\n * salutation and the reports-to are sent only when they change, for the\n * lead source's reason: the route judges a sent one. Do not call is a hint\n * on every phone's call link, never a block.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\n/** The phone fields the card edits, in the order the Phones section lists them. */\nconst PHONE_FIELDS = ['phone', ...CONTACT_EXTRA_PHONE_FIELDS] as const\ntype PhoneField = (typeof PHONE_FIELDS)[number]\ntype PhoneDrafts = Record<PhoneField, string>\n\nconst phoneDraftsFrom = (record: ContactRecord): PhoneDrafts =>\n Object.fromEntries(\n PHONE_FIELDS.map((field) => [field, String(record[field] ?? '')]),\n ) as PhoneDrafts\n\n/** The phones as the route stores them, or the fields that cannot be read as numbers. */\nfunction readPhones(\n drafts: PhoneDrafts,\n): { ok: true; phones: PhoneDrafts } | { ok: false; errors: Partial<PhoneDrafts> } {\n const phones = {} as PhoneDrafts\n const errors: Partial<PhoneDrafts> = {}\n for (const field of PHONE_FIELDS) {\n const typed = drafts[field].trim()\n const normalized = typed ? normalizePhone(typed) : ''\n if (normalized === null) errors[field] = PHONE_HELP_ERROR\n else phones[field] = normalized\n }\n return Object.keys(errors).length ? { ok: false, errors } : { ok: true, phones }\n}\n\nconst PHONE_HELP = 'With the country code, like +1 512 555 0107'\nconst PHONE_HELP_ERROR = 'Enter it with its country code, like +1 512 555 0107.'\n\n/** A section heading inside the card — Salesforce's Details groups its fields the same way. */\nfunction SectionHeading(props: { children: string }) {\n return (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\" color=\"text.secondary\">\n {props.children}\n </Typography>\n </Grid>\n )\n}\n\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n orgId = null,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [salutation, setSalutation] = useState(record.salutation ?? '')\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, orgId)\n const [firstName, setFirstName] = useState(record.firstName ?? '')\n const [lastName, setLastName] = useState(record.lastName ?? '')\n const [phones, setPhones] = useState<PhoneDrafts>(() => phoneDraftsFrom(record))\n const [doNotCall, setDoNotCall] = useState(record.doNotCall === true)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [department, setDepartment] = useState(record.department ?? '')\n const [birthdate, setBirthdate] = useState(record.birthdate ?? '')\n const [assistantName, setAssistantName] = useState(record.assistantName ?? '')\n const [reportsTo, setReportsTo] = useState(record.reportsToContactId ?? '')\n // Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.\n const [leadSource, setLeadSource] = useState(record.leadSource)\n const leadSources = useLeadSourcePicklist(orgId)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [otherAddress, setOtherAddress] = useState<AddressDraft>(\n addressDraftFrom(record.otherAddress),\n )\n const [phoneErrors, setPhoneErrors] = useState<Partial<PhoneDrafts>>({})\n const [birthdateError, setBirthdateError] = useState('')\n const [saving, setSaving] = useState(false)\n /** The custom values the reader touched; every other key reads from the record. */\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /** Something was edited since the card was seeded or last saved. */\n const [edited, setEdited] = useState(false)\n const fields = useContactFieldDefinitions(suiteLocked ? null : orgId)\n // The custom fields are the CRM's; a plan without it draws and sends none.\n const customFields = suiteLocked ? [] : fields.active\n const storedCustom = useMemo(() => record.custom ?? {}, [record.custom])\n const customChanges = useMemo(\n () => crmCustomDraftChanges(storedCustom, custom),\n [storedCustom, custom],\n )\n const clearingRequired =\n crmCustomDraftMissingRequired(customFields, storedCustom, custom, 'edit').length > 0\n /*\n * The name the first and last names make (AGL-3515). While there is one,\n * it IS the name — the Name field shows it, read-only, and the save sends\n * the parts rather than a name the route would have to reconcile.\n */\n const composedName = composeContactName(firstName, lastName)\n\n /** One field's edit: the value, and the card now has something to save. */\n const change = useCallback(<T,>(set: (value: T) => void, value: T) => {\n set(value)\n setEdited(true)\n }, [])\n\n /** Every draft back to what the record holds — the seed, and Discard. */\n const reseed = () => {\n setNameOverride(record.nameOverride)\n setSalutation(record.salutation ?? '')\n setFirstName(record.firstName ?? '')\n setLastName(record.lastName ?? '')\n setPhones(phoneDraftsFrom(record))\n setDoNotCall(record.doNotCall === true)\n setJobTitle(record.jobTitle)\n setDepartment(record.department ?? '')\n setBirthdate(record.birthdate ?? '')\n setAssistantName(record.assistantName ?? '')\n setReportsTo(record.reportsToContactId ?? '')\n setLeadSource(record.leadSource)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setOtherAddress(addressDraftFrom(record.otherAddress))\n setPhoneErrors({})\n setBirthdateError('')\n setCustom({})\n setEdited(false)\n }\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n reseed()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n if (clearingRequired) return\n const readPhonesResult = readPhones(phones)\n const storedBirthdate = normalizeContactBirthdate(birthdate)\n setPhoneErrors('errors' in readPhonesResult ? readPhonesResult.errors : {})\n setBirthdateError(storedBirthdate === null ? CONTACT_BIRTHDATE_REFUSAL : '')\n if ('errors' in readPhonesResult || storedBirthdate === null) return\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n // The name is the parts' while there are parts — see the note above.\n ...(composedName ? {} : { name: nameOverride.trim().slice(0, 120) }),\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n ...readPhonesResult.phones,\n // Sent only when it moves: the route stores the flag as a fact about\n // the person, and an untouched card should not re-assert it.\n ...(doNotCall !== (record.doNotCall === true) ? { doNotCall } : {}),\n jobTitle: jobTitle.trim().slice(0, 120),\n department: department.trim().slice(0, 120),\n birthdate: storedBirthdate,\n assistantName: assistantName.trim().slice(0, 120),\n // Sent only when changed: the route judges a sent value against the\n // org's list, and an untouched one needs no judging. The reports-to\n // likewise, because the route checks a sent one for a loop.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n ...(salutation !== (record.salutation ?? '') ? { salutation } : {}),\n ...(reportsTo !== (record.reportsToContactId ?? '')\n ? { reportsToContactId: reportsTo || null }\n : {}),\n address,\n otherAddress,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n // Only the custom keys that changed — see the note above.\n ...(!suiteLocked && customChanges.length\n ? { custom: Object.fromEntries(customChanges) }\n : {}),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n // The custom values are stored; the controls read the record again.\n setCustom({})\n if (stageChanged && !siteHostId) {\n // The stage lives on a site's facet, and this person has none. Never\n // \"Contact saved\" over a change that went nowhere (AGL-4120).\n return void enqueueSnackbar(\n 'Saved, but the stage could not be changed: no site has captured this contact yet.',\n { variant: 'warning', persist: false },\n )\n }\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n setEdited(false)\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n assistantName,\n birthdate,\n clearingRequired,\n companyId,\n companyName,\n composedName,\n contactUpdate,\n customChanges,\n department,\n doNotCall,\n enqueueSnackbar,\n firstName,\n siteHostId,\n jobTitle,\n lastName,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n otherAddress,\n ownerUid,\n phones,\n record.$id,\n record.doNotCall,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n record.reportsToContactId,\n record.salutation,\n reportsTo,\n salutation,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** One phone field, with click-to-call off what is IN the box (AGL-2661). */\n const phoneField = (field: PhoneField) => {\n const value = phones[field]\n const telHref = crmTelHref(value)\n const callLabel = doNotCall\n ? `Call ${value.trim()} — marked do not call`\n : `Call ${value.trim()}`\n return (\n <Grid key={field} size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label={CONTACT_PHONE_FIELD_LABELS[field]}\n value={value}\n onChange={(event) => change(setPhones, { ...phones, [field]: event.target.value })}\n error={Boolean(phoneErrors[field])}\n helperText={phoneErrors[field] || (field === 'phone' ? PHONE_HELP : undefined)}\n fullWidth\n slotProps={{\n input: {\n /*\n * The number on screen is the one a reader means to ring, and\n * `crmTelHref` withholds the link from anything half-typed. A\n * do-not-call person keeps the link — the rep decides — with\n * the request said in the tooltip and the icon's color.\n */\n endAdornment:\n telHref && field !== 'fax' ? (\n <InputAdornment position=\"end\">\n <Tooltip title={callLabel}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n color={doNotCall ? 'warning' : 'default'}\n aria-label={callLabel}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n )\n }\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#contact-properties' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {edited ? (\n <Button size=\"small\" disabled={saving} onClick={reseed}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving || !edited || clearingRequired}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Grid container spacing={2}>\n <SectionHeading>{'Contact information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n stored={record.salutation ?? ''}\n onChange={(value) => change(setSalutation, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => change(setFirstName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => change(setLastName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || nameOverride}\n onChange={(event) => change(setNameOverride, event.target.value)}\n slotProps={{\n htmlInput: { maxLength: 120 },\n input: { readOnly: Boolean(composedName) },\n }}\n helperText={\n composedName\n ? 'Made of the first and last name.'\n : record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => change(setJobTitle, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => change(setDepartment, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n change(setCompanyId, id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={leadSource}\n stored={record.leadSource}\n onChange={(value) => change(setLeadSource, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n change(setLifecycleStage, event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => change(setOwnerUid, event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => change(setTags, event.target.value)}\n fullWidth\n />\n </Grid>\n\n <SectionHeading>{'Phones'}</SectionHeading>\n {PHONE_FIELDS.filter((field) => field !== 'assistantPhone').map(phoneField)}\n <Grid size={{ xs: 12, sm: 6 }}>\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => change(setDoNotCall, event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <FormHelperText>\n {'They asked not to be phoned. Every number still dials; the Call button says so.'}\n </FormHelperText>\n </Grid>\n\n <SectionHeading>{'Addresses'}</SectionHeading>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields\n value={address}\n onChange={(value) => change(setAddress, value)}\n />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Other address'}</Typography>\n <ContactAddressFields\n value={otherAddress}\n onChange={(value) => change(setOtherAddress, value)}\n />\n </Stack>\n </Grid>\n\n <SectionHeading>{'Additional information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n type=\"date\"\n label=\"Birthdate\"\n value={birthdate}\n onChange={(event) => change(setBirthdate, event.target.value)}\n error={Boolean(birthdateError)}\n helperText={birthdateError || undefined}\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <ContactReportsToField\n hostId={hostId}\n org={org}\n contactId={record.$id}\n groupId={consentGroup.groupId}\n value={reportsTo}\n onChange={(value) => change(setReportsTo, value)}\n disabled={saving}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Assistant\"\n value={assistantName}\n onChange={(event) => change(setAssistantName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n {phoneField('assistantPhone')}\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => change(setNotes, event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n {/*\n The org's own fields (AGL-2601), after the built-in ones and saved\n with them. Absent, not an empty box, while the org defines none.\n */}\n {customFields.length ? (\n <>\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n </Grid>\n {customFields.map((definition) => (\n <Grid key={definition.$id} size={{ xs: 12, sm: 6 }}>\n <CrmCustomFieldControl\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n change(setCustom, { ...custom, [definition.key]: value })\n }\n disabled={saving}\n />\n </Grid>\n ))}\n {clearingRequired ? (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"caption\" color=\"error\">\n {'A required field cannot be left empty.'}\n </Typography>\n </Grid>\n ) : null}\n </>\n ) : null}\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","composeContactName","CONTACT_BIRTHDATE_REFUSAL","CONTACT_EXTRA_PHONE_FIELDS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_NAME_PART_MAX","CONTACT_PHONE_FIELD_LABELS","CRM_SALUTATION_PICKLIST","crmTelHref","normalizeContactBirthdate","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Checkbox","FormControlLabel","FormHelperText","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useContactUpdate","useCrmActivityLogger","useCrmPicklist","useLeadSourcePicklist","parseContactTags","setContactStage","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","ContactReportsToField","CrmCustomFieldControl","CrmPicklistSelect","crmSuiteLockedReason","LeadSourceSelect","PHONE_FIELDS","phoneDraftsFrom","record","Object","fromEntries","map","field","String","readPhones","drafts","phones","errors","typed","trim","normalized","PHONE_HELP_ERROR","keys","length","ok","PHONE_HELP","SectionHeading","props","size","xs","variant","color","children","ContactPropertiesCard","hostId","org","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","salutation","setSalutation","salutations","firstName","setFirstName","lastName","setLastName","setPhones","doNotCall","setDoNotCall","jobTitle","setJobTitle","department","setDepartment","birthdate","setBirthdate","assistantName","setAssistantName","reportsTo","setReportsTo","reportsToContactId","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","otherAddress","setOtherAddress","phoneErrors","setPhoneErrors","birthdateError","setBirthdateError","saving","setSaving","custom","setCustom","edited","setEdited","fields","customFields","active","storedCustom","customChanges","clearingRequired","composedName","change","set","value","reseed","recordId","$id","handleSave","readPhonesResult","storedBirthdate","stageChanged","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","message","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","phoneField","telHref","callLabel","sm","label","onChange","event","target","Boolean","helperText","undefined","fullWidth","slotProps","input","endAdornment","position","title","component","href","edge","aria-label","path","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","disabled","onClick","container","readOnly","picklistId","picklist","stored","htmlInput","maxLength","canonicalName","ready","truncated","company","onCreate","fallbackName","select","stage","memberName","owner","crmMemberPickerLabel","placeholder","filter","control","checked","inputLabel","shrink","contactId","groupId","multiline","minRows","definition","key","displayName"],"mappings":"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"}
|
|
@@ -45,6 +45,16 @@ export interface DealBoardProps {
|
|
|
45
45
|
* two closed columns start collapsed to a count-free stub and expand on
|
|
46
46
|
* request, which is also when their rows are read — a collapsed column
|
|
47
47
|
* costs no listener.
|
|
48
|
+
*
|
|
49
|
+
* ## Empty stages fold too, and the board says when there is more
|
|
50
|
+
*
|
|
51
|
+
* A pipeline with Salesforce's ten stages is wider than the card, and the
|
|
52
|
+
* stages with deals in them were off the edge with nothing to say so. An
|
|
53
|
+
* open stage with no deals folds to the same narrow stub, labelled and still
|
|
54
|
+
* a drop target, so the stages that hold work get the room. When the row is
|
|
55
|
+
* still wider than the card, chevrons at either end page it sideways the way
|
|
56
|
+
* MUI's scrollable Tabs do — the scrollbar alone stays hidden on a trackpad
|
|
57
|
+
* until the reader already knows to scroll.
|
|
48
58
|
*/
|
|
49
59
|
export declare function DealBoard(props: DealBoardProps): import("react").JSX.Element;
|
|
50
60
|
export declare namespace DealBoard {
|