@aglyn/plugins-crm 1.0.0 → 1.1.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.
Files changed (154) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-link-fields.d.ts +34 -0
  3. package/src/lib/components/activity-link-fields.js +141 -0
  4. package/src/lib/components/activity-link-fields.js.map +1 -0
  5. package/src/lib/components/activity-list.d.ts +8 -0
  6. package/src/lib/components/activity-list.js +4 -2
  7. package/src/lib/components/activity-list.js.map +1 -1
  8. package/src/lib/components/activity-queries.d.ts +7 -2
  9. package/src/lib/components/activity-queries.js +7 -3
  10. package/src/lib/components/activity-queries.js.map +1 -1
  11. package/src/lib/components/company-contact-roles-card.d.ts +42 -0
  12. package/src/lib/components/company-contact-roles-card.js +135 -0
  13. package/src/lib/components/company-contact-roles-card.js.map +1 -0
  14. package/src/lib/components/company-detail-page.js +12 -1
  15. package/src/lib/components/company-detail-page.js.map +1 -1
  16. package/src/lib/components/company-edit-drawer.js +9 -6
  17. package/src/lib/components/company-edit-drawer.js.map +1 -1
  18. package/src/lib/components/company-picker.js +4 -2
  19. package/src/lib/components/company-picker.js.map +1 -1
  20. package/src/lib/components/contact-associations-card.js +15 -1
  21. package/src/lib/components/contact-associations-card.js.map +1 -1
  22. package/src/lib/components/contact-detail-page.js +13 -14
  23. package/src/lib/components/contact-detail-page.js.map +1 -1
  24. package/src/lib/components/contact-list-columns.js +5 -4
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.js +41 -20
  27. package/src/lib/components/contact-properties-card.js.map +1 -1
  28. package/src/lib/components/contact-roles-editor.d.ts +54 -0
  29. package/src/lib/components/contact-roles-editor.js +270 -0
  30. package/src/lib/components/contact-roles-editor.js.map +1 -0
  31. package/src/lib/components/contacts-section.js +6 -4
  32. package/src/lib/components/contacts-section.js.map +1 -1
  33. package/src/lib/components/crm-console-sections.js +1 -1
  34. package/src/lib/components/crm-console-sections.js.map +1 -1
  35. package/src/lib/components/crm-record-picker.d.ts +19 -0
  36. package/src/lib/components/crm-record-picker.js +40 -11
  37. package/src/lib/components/crm-record-picker.js.map +1 -1
  38. package/src/lib/components/crm-related-to-picker.d.ts +33 -0
  39. package/src/lib/components/crm-related-to-picker.js +135 -0
  40. package/src/lib/components/crm-related-to-picker.js.map +1 -0
  41. package/src/lib/components/crm-site-picker.d.ts +13 -1
  42. package/src/lib/components/crm-site-picker.js +15 -6
  43. package/src/lib/components/crm-site-picker.js.map +1 -1
  44. package/src/lib/components/crm-transfer-buttons.d.ts +5 -0
  45. package/src/lib/components/crm-transfer-buttons.js +9 -6
  46. package/src/lib/components/crm-transfer-buttons.js.map +1 -1
  47. package/src/lib/components/deal-contact-roles-card.js +21 -239
  48. package/src/lib/components/deal-contact-roles-card.js.map +1 -1
  49. package/src/lib/components/deal-detail-page.js +2 -1
  50. package/src/lib/components/deal-detail-page.js.map +1 -1
  51. package/src/lib/components/deal-edit-drawer.js +11 -7
  52. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  53. package/src/lib/components/deals-section.js +5 -4
  54. package/src/lib/components/deals-section.js.map +1 -1
  55. package/src/lib/components/log-activity-dialog.d.ts +27 -9
  56. package/src/lib/components/log-activity-dialog.js +96 -32
  57. package/src/lib/components/log-activity-dialog.js.map +1 -1
  58. package/src/lib/components/new-contact-drawer.d.ts +7 -3
  59. package/src/lib/components/new-contact-drawer.js +27 -15
  60. package/src/lib/components/new-contact-drawer.js.map +1 -1
  61. package/src/lib/components/pipeline-stages-dialog.js +5 -1
  62. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  63. package/src/lib/components/record-activity-card.d.ts +11 -2
  64. package/src/lib/components/record-activity-card.js +90 -5
  65. package/src/lib/components/record-activity-card.js.map +1 -1
  66. package/src/lib/components/settings-section.d.ts +4 -2
  67. package/src/lib/components/settings-section.js +7 -5
  68. package/src/lib/components/settings-section.js.map +1 -1
  69. package/src/lib/hooks/crm-activity-client-reads.d.ts +8 -0
  70. package/src/lib/hooks/crm-activity-client-reads.js +50 -0
  71. package/src/lib/hooks/crm-activity-client-reads.js.map +1 -0
  72. package/src/lib/hooks/use-campaign-filing-log.d.ts +2 -1
  73. package/src/lib/hooks/use-campaign-filing-log.js +10 -3
  74. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -1
  75. package/src/lib/hooks/use-crm-org-mount.d.ts +8 -6
  76. package/src/lib/hooks/use-crm-org-mount.js +2 -1
  77. package/src/lib/hooks/use-crm-org-mount.js.map +1 -1
  78. package/src/lib/hooks/use-crm-scope.d.ts +17 -5
  79. package/src/lib/hooks/use-crm-scope.js +12 -5
  80. package/src/lib/hooks/use-crm-scope.js.map +1 -1
  81. package/src/lib/model/contact-holder.d.ts +12 -0
  82. package/src/lib/model/contact-holder.js +20 -1
  83. package/src/lib/model/contact-holder.js.map +1 -1
  84. package/src/lib/model/contact-record.d.ts +5 -0
  85. package/src/lib/model/contact-record.js +7 -1
  86. package/src/lib/model/contact-record.js.map +1 -1
  87. package/src/lib/model/contact-update.d.ts +6 -0
  88. package/src/lib/model/contact-update.js +1 -0
  89. package/src/lib/model/contact-update.js.map +1 -1
  90. package/src/lib/model/crm-activity-link.d.ts +117 -0
  91. package/src/lib/model/crm-activity-link.js +113 -0
  92. package/src/lib/model/crm-activity-link.js.map +1 -0
  93. package/src/lib/model/crm-api.d.ts +4 -1
  94. package/src/lib/model/crm-api.js +9 -5
  95. package/src/lib/model/crm-api.js.map +1 -1
  96. package/src/lib/model/deal-board-model.js +5 -3
  97. package/src/lib/model/deal-board-model.js.map +1 -1
  98. package/src/lib/model/deal-contact-roles.d.ts +15 -1
  99. package/src/lib/model/deal-contact-roles.js +17 -3
  100. package/src/lib/model/deal-contact-roles.js.map +1 -1
  101. package/src/lib/model/settle-stage-names.d.ts +25 -0
  102. package/src/lib/model/settle-stage-names.js +29 -0
  103. package/src/lib/model/settle-stage-names.js.map +1 -0
  104. package/src/lib/native-nav.js +1 -1
  105. package/src/lib/native-nav.js.map +1 -1
  106. package/src/lib/server/add-contact-by-hand.d.ts +84 -0
  107. package/src/lib/server/add-contact-by-hand.js +212 -0
  108. package/src/lib/server/add-contact-by-hand.js.map +1 -0
  109. package/src/lib/server/api-v1/contacts.js +61 -23
  110. package/src/lib/server/api-v1/contacts.js.map +1 -1
  111. package/src/lib/server/api-v1/crm-activities.js +13 -3
  112. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  113. package/src/lib/server/api-v1/crm-pipelines.d.ts +1 -1
  114. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  115. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  116. package/src/lib/server/api-v1/crm-shared.d.ts +16 -9
  117. package/src/lib/server/api-v1/crm-shared.js +23 -11
  118. package/src/lib/server/api-v1/crm-shared.js.map +1 -1
  119. package/src/lib/server/api-v1/openapi.js +3 -3
  120. package/src/lib/server/api-v1/openapi.js.map +1 -1
  121. package/src/lib/server/automation-steps.js +11 -9
  122. package/src/lib/server/automation-steps.js.map +1 -1
  123. package/src/lib/server/campaign-filing-activity.js +7 -2
  124. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  125. package/src/lib/server/contact-update.d.ts +4 -0
  126. package/src/lib/server/contact-update.js +71 -10
  127. package/src/lib/server/contact-update.js.map +1 -1
  128. package/src/lib/server/convert-host-lead.js +13 -2
  129. package/src/lib/server/convert-host-lead.js.map +1 -1
  130. package/src/lib/server/convert-open-lead.js +8 -1
  131. package/src/lib/server/convert-open-lead.js.map +1 -1
  132. package/src/lib/server/crm-activity-company.d.ts +25 -0
  133. package/src/lib/server/crm-activity-company.js +34 -0
  134. package/src/lib/server/crm-activity-company.js.map +1 -0
  135. package/src/lib/server/crm-inbound-email.js +21 -15
  136. package/src/lib/server/crm-inbound-email.js.map +1 -1
  137. package/src/lib/server/crm-sharing.js +9 -2
  138. package/src/lib/server/crm-sharing.js.map +1 -1
  139. package/src/lib/server/email-send.js +10 -8
  140. package/src/lib/server/email-send.js.map +1 -1
  141. package/src/lib/server/merge-contacts.js +6 -2
  142. package/src/lib/server/merge-contacts.js.map +1 -1
  143. package/src/lib/server/org-caller.d.ts +8 -0
  144. package/src/lib/server/org-caller.js +10 -0
  145. package/src/lib/server/org-caller.js.map +1 -1
  146. package/src/lib/server/record-timeline.d.ts +3 -1
  147. package/src/lib/server/record-timeline.js +11 -3
  148. package/src/lib/server/record-timeline.js.map +1 -1
  149. package/src/lib/server.js +152 -43
  150. package/src/lib/server.js.map +1 -1
  151. package/src/lib/transfer/contacts.js +83 -48
  152. package/src/lib/transfer/contacts.js.map +1 -1
  153. package/src/lib/transfer/fields.js +1 -2
  154. package/src/lib/transfer/fields.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/log-activity-dialog.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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { crmVisibleToClause } from '../hooks/use-crm-scope'\nimport type { ActivityRecordLink, ActivityScope } from './activity-queries'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** The longest body a single activity stores — a call summary, not a document. */\nconst BODY_MAX = 4000\nconst OUTCOME_MAX = 200\nconst DURATION_MAX_MINUTES = 24 * 60\n\n/**\n * Epoch millis as the value a `datetime-local` input holds, in the LOCAL\n * zone — the input has no zone of its own, so an ISO string with a `Z`\n * would land the call an offset away from when it happened.\n */\nfunction toLocalInput(ms: number): string {\n if (!Number.isFinite(ms)) return ''\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-` +\n `${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`\n )\n}\n\n/** The inverse: a `datetime-local` value parsed as local time, or `null`. */\nfunction fromLocalInput(value: string): number | null {\n if (!value) return null\n const ms = new Date(value).getTime()\n return Number.isFinite(ms) ? ms : null\n}\n\ninterface Draft {\n kind: CrmActivityKind\n body: string\n /** The `datetime-local` string, not millis — what the input holds. */\n at: string\n outcome: string\n /** Free text until save, so a half-typed number does not snap to zero. */\n durationMinutes: string\n /**\n * Which way a call or an email went (AGL-3517) — Salesforce's Call Type\n * for a call — or `''` for not said. Kept across a kind change and\n * written only when the kind takes it.\n */\n direction: string\n}\n\n/** The kind a new activity opens on when the caller names none. */\nconst DEFAULT_KIND: CrmActivityKind = 'call'\n\nconst freshDraft = (kind: CrmActivityKind = DEFAULT_KIND): Draft => ({\n kind,\n body: '',\n at: toLocalInput(Date.now()),\n outcome: '',\n durationMinutes: '',\n // Most logged calls and emails are the team's own.\n direction: 'outbound',\n})\n\nconst draftFrom = (activity: CrmActivityRow): Draft => ({\n kind: Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other',\n body: activity.body ?? '',\n at: toLocalInput(activity.atMs),\n outcome: activity.outcome ?? '',\n durationMinutes:\n typeof activity.durationMinutes === 'number'\n ? String(activity.durationMinutes)\n : '',\n direction: Aglyn.crmActivityDirection(activity.kind, activity.direction) ?? '',\n})\n\nexport interface LogActivityDialogProps {\n open: boolean\n onClose: () => void\n /** Where the activity is written, from `useActivityScope`. */\n scope: ActivityScope\n /**\n * The record the activity is filed against, FIXED by the caller. A\n * contact's page logs against that contact; the dialog offers no picker,\n * because an activity logged from a record page is about that record and\n * a picker would be a way to file it somewhere the reader is not looking.\n */\n link: ActivityRecordLink\n /**\n * When set, the dialog EDITS this activity rather than logging a new one.\n * The link, the author and the scope stay what they were; only what was\n * said, when, and how it went can change.\n */\n activity?: CrmActivityRow | null\n /**\n * What a NEW activity opens as (AGL-2661) — the click-to-call button\n * opens the dialog already a call, so logging one is the button and a\n * sentence rather than the button, the kind and a sentence. Ignored for\n * an edit, whose kind is the activity's own.\n */\n kind?: CrmActivityKind\n}\n\n/**\n * Log a call, an email, a meeting, a note or something else against a\n * record (AGL-2600) — or correct one already logged.\n *\n * ## What is written\n *\n * A new activity carries the whole `CrmScoped` stamp: `visibleTo` from\n * `crmScopeTokens`, so it lands in exactly the scope a contact captured on\n * this site would; `hostId` as provenance; `byUid` and `byName` for the\n * author, the name denormalized because a colleague reading the log cannot\n * resolve a uid (see `CrmActivity.byName`); `atMs` for when it HAPPENED,\n * which the reader picks and which defaults to now; and the timestamps.\n *\n * An edit rewrites only the fields the dialog shows. `visibleTo` is never\n * touched — the rules refuse a scoped member changing it, and widening a\n * record is an act for an org-wide member on the record itself — and neither\n * is the author or the link. A kind that stops taking an outcome takes the\n * outcome and the duration off the document rather than leaving a stale\n * \"left a voicemail\" on what is now a note.\n *\n * ## Who may write\n *\n * The rules admit any member who may write scoped org data, on create and\n * on update alike; they do not distinguish the author. The card that opens\n * this dialog for an edit checks the author itself (`useCanEditActivity`),\n * so what reaches here has already passed the console's verdict.\n */\nexport function LogActivityDialog(props: LogActivityDialogProps) {\n const { open, onClose, scope, link, activity, kind: presetKind } = props\n const { firestore, dataScope, hostId, mountedHostId, readTokens, writeTokens } =\n scope\n const { data: user } = useUser()\n const authorName = useUserName()\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState<Draft>(freshDraft)\n const [saving, setSaving] = useState(false)\n\n /*\n * Seeded on OPEN, not on mount: the dialog stays mounted between uses so\n * its state survives a close, and \"log another call\" must start from now\n * rather than from the last call's timestamp.\n */\n useEffect(() => {\n if (open) setDraft(activity ? draftFrom(activity) : freshDraft(presetKind))\n }, [open, activity, presetKind])\n\n const hasOutcome = Aglyn.activityKindHasOutcome(draft.kind)\n const directions = Aglyn.CRM_ACTIVITY_DIRECTIONS[draft.kind] ?? []\n const direction = Aglyn.crmActivityDirection(draft.kind, draft.direction)\n const atMs = fromLocalInput(draft.at)\n const body = draft.body.trim()\n const duration = draft.durationMinutes.trim()\n ? Number(draft.durationMinutes)\n : null\n const durationValid =\n duration === null ||\n (Number.isInteger(duration) &&\n duration >= 0 &&\n duration <= DURATION_MAX_MINUTES)\n const canSave =\n Boolean(dataScope) &&\n Boolean(user?.uid) &&\n // A NEW activity is stamped with a site's scope and provenance; at the\n // organization level that is the picked site, and there is nothing to\n // write until there is one (AGL-2630). An edit never touches either.\n (Boolean(activity) || Boolean(hostId)) &&\n body.length > 0 &&\n atMs !== null &&\n durationValid &&\n !saving\n\n const handleSave = useCallback(async () => {\n // Every guard the button reads, again — a callback can outlive the\n // render that disabled it.\n if (!dataScope || !user?.uid || !body || atMs === null || !durationValid) {\n return\n }\n if (!activity && !hostId) return\n setSaving(true)\n try {\n const said = {\n kind: draft.kind,\n body: body.slice(0, BODY_MAX),\n atMs,\n }\n const outcome = hasOutcome ? draft.outcome.trim().slice(0, OUTCOME_MAX) : ''\n const durationMinutes = hasOutcome && duration !== null ? duration : null\n if (activity) {\n await updateDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n {\n ...said,\n // Cleared rather than left: a call turned into a note must not\n // keep the call's outcome, and Firestore refuses `undefined`.\n outcome: outcome ? outcome : deleteField(),\n direction: direction ?? deleteField(),\n durationMinutes:\n durationMinutes !== null ? durationMinutes : deleteField(),\n updatedAt: new Date(),\n },\n )\n enqueueSnackbar('Activity updated', { variant: 'success', persist: false })\n } else {\n /*\n * THE PER-RECORD CEILING (AGL-2611): one aggregate on the record\n * this dialog is filing under, before the write — the same count\n * the automation step and the REST create take, on the same link.\n * Scoped to the tokens this viewer may list, as every activity\n * listener is, so the rules admit the aggregate for a scoped\n * member exactly as they admit the timeline.\n */\n const lead = Aglyn.crmActivityCeilingLink(link)\n if (lead) {\n const logged = (\n await getCountFromServer(\n query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n ),\n ...crmVisibleToClause(readTokens),\n where(lead.field, '==', lead.id),\n ),\n )\n ).data().count\n if (!Aglyn.crmActivityLogHasRoom(logged)) {\n enqueueSnackbar(Aglyn.CRM_ACTIVITY_LOG_FULL_MESSAGE, {\n variant: 'warning',\n persist: false,\n })\n return\n }\n }\n await addDoc(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n ),\n {\n ...said,\n ...(outcome ? { outcome } : {}),\n ...(durationMinutes !== null ? { durationMinutes } : {}),\n ...(direction ? { direction } : {}),\n // Only the link the caller fixed. A key with no value is\n // `undefined`, which Firestore refuses, so each is spread in\n // rather than written blank.\n ...(link.contactId ? { contactId: link.contactId } : {}),\n ...(link.companyId ? { companyId: link.companyId } : {}),\n ...(link.dealId ? { dealId: link.dealId } : {}),\n ...(link.leadId ? { leadId: link.leadId } : {}),\n visibleTo: writeTokens,\n hostId,\n byUid: user.uid,\n // Absent rather than blank while the profile is still resolving:\n // the row reads a stored name over anything else, and an empty\n // string would be a name.\n ...(authorName ? { byName: authorName } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n )\n enqueueSnackbar('Activity logged', { variant: 'success', persist: false })\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }, [\n dataScope,\n user,\n body,\n atMs,\n durationValid,\n draft.kind,\n draft.outcome,\n direction,\n hasOutcome,\n duration,\n activity,\n firestore,\n enqueueSnackbar,\n link,\n readTokens,\n writeTokens,\n hostId,\n authorName,\n onClose,\n ])\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{activity ? 'Edit activity' : 'Log activity'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {/* Only for a new activity at the organization level — see `canSave`. */}\n {activity ? null : (\n <CrmSitePicker\n hostId={mountedHostId}\n disabled={saving}\n helperText=\"The site this activity is logged from — it decides which of your sites may see it.\"\n />\n )}\n <Stack direction=\"row\" spacing={1} sx={{ mt: 1 }}>\n <TextField\n select\n size=\"small\"\n label=\"Kind\"\n value={draft.kind}\n onChange={(event) =>\n setDraft((prev) => ({\n ...prev,\n kind: Aglyn.isCrmActivityKind(event.target.value)\n ? event.target.value\n : prev.kind,\n }))\n }\n sx={{ minWidth: 140 }}\n >\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <MenuItem key={kind} value={kind}>\n {Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </TextField>\n {directions.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Direction\"\n value={direction ?? ''}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, direction: String(event.target.value) }))\n }\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ minWidth: 130 }}\n >\n <MenuItem value=\"\">{'Not said'}</MenuItem>\n {directions.map((entry) => (\n <MenuItem key={entry} value={entry}>\n {Aglyn.CRM_ACTIVITY_DIRECTION_LABELS[entry]}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n size=\"small\"\n label=\"When\"\n type=\"datetime-local\"\n value={draft.at}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, at: event.target.value }))\n }\n error={draft.at.length > 0 && atMs === null}\n slotProps={{ inputLabel: { shrink: true } }}\n sx={{ flex: 1 }}\n />\n </Stack>\n <TextField\n size=\"small\"\n label=\"What happened\"\n value={draft.body}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, body: event.target.value }))\n }\n multiline\n minRows={3}\n autoFocus\n slotProps={{ htmlInput: { maxLength: BODY_MAX } }}\n />\n {hasOutcome ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n size=\"small\"\n label=\"Outcome\"\n placeholder=\"Left a voicemail\"\n value={draft.outcome}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, outcome: event.target.value }))\n }\n slotProps={{ htmlInput: { maxLength: OUTCOME_MAX } }}\n sx={{ flex: 1 }}\n />\n <TextField\n size=\"small\"\n label=\"Minutes\"\n type=\"number\"\n value={draft.durationMinutes}\n onChange={(event) =>\n setDraft((prev) => ({\n ...prev,\n durationMinutes: event.target.value,\n }))\n }\n error={!durationValid}\n slotProps={{\n htmlInput: { min: 0, max: DURATION_MAX_MINUTES, step: 1 },\n }}\n sx={{ width: 120 }}\n />\n </Stack>\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={saving}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSave}\n onClick={handleSave}\n >\n {activity ? 'Save' : 'Log'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLogActivityDialog.displayName = 'LogActivityDialog'\n\nexport default LogActivityDialog\n"],"names":["Aglyn","useSnackbar","useUser","useUserName","Button","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","addDoc","collection","deleteField","doc","getCountFromServer","query","updateDoc","where","useCallback","useEffect","useState","crmVisibleToClause","CrmSitePicker","BODY_MAX","OUTCOME_MAX","DURATION_MAX_MINUTES","toLocalInput","ms","Number","isFinite","date","Date","pad","value","String","padStart","getFullYear","getMonth","getDate","getHours","getMinutes","fromLocalInput","getTime","DEFAULT_KIND","freshDraft","kind","body","at","now","outcome","durationMinutes","direction","draftFrom","activity","isCrmActivityKind","atMs","crmActivityDirection","LogActivityDialog","props","open","onClose","scope","link","presetKind","firestore","dataScope","hostId","mountedHostId","readTokens","writeTokens","data","user","authorName","enqueueSnackbar","draft","setDraft","saving","setSaving","hasOutcome","activityKindHasOutcome","directions","CRM_ACTIVITY_DIRECTIONS","trim","duration","durationValid","isInteger","canSave","Boolean","uid","length","handleSave","said","slice","CRM_COLLECTIONS","activities","$id","updatedAt","variant","persist","lead","crmActivityCeilingLink","logged","field","id","count","crmActivityLogHasRoom","CRM_ACTIVITY_LOG_FULL_MESSAGE","contactId","companyId","dealId","leadId","visibleTo","byUid","byName","createdAt","error","console","allowDuplicate","maxWidth","fullWidth","sx","display","flexDirection","gap","disabled","helperText","spacing","mt","select","size","label","onChange","event","prev","target","minWidth","CRM_ACTIVITY_KINDS","map","CRM_ACTIVITY_KIND_LABELS","slotProps","displayEmpty","inputLabel","shrink","entry","CRM_ACTIVITY_DIRECTION_LABELS","type","flex","multiline","minRows","autoFocus","htmlInput","maxLength","placeholder","min","max","step","width","onClick","color","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/log-activity-dialog.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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n getCountFromServer,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { crmActivityClientReads } from '../hooks/crm-activity-client-reads'\nimport { crmVisibleToClause } from '../hooks/use-crm-scope'\nimport {\n crmActivityLinkFromPicks,\n crmActivityPicksEqual,\n crmActivityPicksFrom,\n type CrmActivityPicks,\n resolveCrmActivityCompany,\n} from '../model/crm-activity-link'\nimport { ActivityLinkFields } from './activity-link-fields'\nimport type { ActivityRecordLink, ActivityScope } from './activity-queries'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** The longest body a single activity stores — a call summary, not a document. */\nconst BODY_MAX = 4000\nconst OUTCOME_MAX = 200\nconst DURATION_MAX_MINUTES = 24 * 60\n\n/**\n * Epoch millis as the value a `datetime-local` input holds, in the LOCAL\n * zone — the input has no zone of its own, so an ISO string with a `Z`\n * would land the call an offset away from when it happened.\n */\nfunction toLocalInput(ms: number): string {\n if (!Number.isFinite(ms)) return ''\n const date = new Date(ms)\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-` +\n `${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`\n )\n}\n\n/** The inverse: a `datetime-local` value parsed as local time, or `null`. */\nfunction fromLocalInput(value: string): number | null {\n if (!value) return null\n const ms = new Date(value).getTime()\n return Number.isFinite(ms) ? ms : null\n}\n\ninterface Draft {\n kind: CrmActivityKind\n body: string\n /** The `datetime-local` string, not millis — what the input holds. */\n at: string\n outcome: string\n /** Free text until save, so a half-typed number does not snap to zero. */\n durationMinutes: string\n /**\n * Which way a call or an email went (AGL-3517) — Salesforce's Call Type\n * for a call — or `''` for not said. Kept across a kind change and\n * written only when the kind takes it.\n */\n direction: string\n /** **Name** and **Related To**, as the reader has left them. */\n picks: CrmActivityPicks\n}\n\n/** The kind a new activity opens on when the caller names none. */\nconst DEFAULT_KIND: CrmActivityKind = 'call'\n\nconst freshDraft = (\n kind: CrmActivityKind = DEFAULT_KIND,\n picks: CrmActivityPicks = crmActivityPicksFrom({}),\n): Draft => ({\n kind,\n body: '',\n at: toLocalInput(Date.now()),\n outcome: '',\n durationMinutes: '',\n // Most logged calls and emails are the team's own.\n direction: 'outbound',\n picks,\n})\n\nconst draftFrom = (activity: CrmActivityRow): Draft => ({\n kind: Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other',\n body: activity.body ?? '',\n at: toLocalInput(activity.atMs),\n outcome: activity.outcome ?? '',\n durationMinutes:\n typeof activity.durationMinutes === 'number'\n ? String(activity.durationMinutes)\n : '',\n direction: Aglyn.crmActivityDirection(activity.kind, activity.direction) ?? '',\n picks: crmActivityPicksFrom(activity),\n})\n\nexport interface LogActivityDialogProps {\n open: boolean\n onClose: () => void\n /** Where the activity is written, from `useActivityScope`. */\n scope: ActivityScope\n /**\n * The record the dialog was opened on, which prefills **Name** and\n * **Related To** (AGL-4123). A lead's own log offers neither: the lead is\n * the entry's only link until it converts.\n */\n link: ActivityRecordLink\n /**\n * The deal the dialog was opened on, as its page read it: **Name** opens on\n * its Primary and its contact roles lead the list.\n */\n deal?: { contactId?: unknown; contactRoles?: unknown } | null\n /**\n * When set, the dialog EDITS this activity rather than logging a new one.\n * The author and the scope stay what they were; what was said, when, how\n * it went, and who and what it is about can change.\n */\n activity?: CrmActivityRow | null\n /**\n * What a NEW activity opens as (AGL-2661) — the click-to-call button\n * opens the dialog already a call, so logging one is the button and a\n * sentence rather than the button, the kind and a sentence. Ignored for\n * an edit, whose kind is the activity's own.\n */\n kind?: CrmActivityKind\n}\n\n/**\n * Log a call, an email, a meeting, a note or something else against a\n * record (AGL-2600) — or correct one already logged.\n *\n * ## Who, what and which account\n *\n * **Name** and **Related To** are Salesforce's Who and What (AGL-4123), and\n * the company an entry rolls up to is stamped from them when it is saved —\n * `model/crm-activity-link.ts` has the rule, shared with every other writer.\n * The fields open on the record the dialog was opened from and the reader may\n * change them: an entry named for a deal shows on the deal, on the person's\n * timeline and on the company, whichever page it was logged from.\n *\n * ## What is written\n *\n * A new activity carries the whole `CrmScoped` stamp: `visibleTo` from\n * `crmScopeTokens`, so it lands in exactly the scope a contact captured on\n * this site would; `hostId` as provenance; `byUid` and `byName` for the\n * author, the name denormalized because a colleague reading the log cannot\n * resolve a uid (see `CrmActivity.byName`); `atMs` for when it HAPPENED,\n * which the reader picks and which defaults to now; and the timestamps.\n *\n * An edit rewrites only the fields the dialog shows. `visibleTo` is never\n * touched — the rules refuse a scoped member changing it, and widening a\n * record is an act for an org-wide member on the record itself — and neither\n * is the author. The links are rewritten only when **Name** or **Related To**\n * changed, and then the company is worked out again, so an edit that corrects\n * a typo cannot move an entry off a timeline it was filed on. A kind that\n * stops taking an outcome takes the outcome and the duration off the document\n * rather than leaving a stale \"left a voicemail\" on what is now a note.\n *\n * ## Who may write\n *\n * The rules admit any member who may write scoped org data, on create and\n * on update alike; they do not distinguish the author. The card that opens\n * this dialog for an edit checks the author itself (`useCanEditActivity`),\n * so what reaches here has already passed the console's verdict.\n */\nexport function LogActivityDialog(props: LogActivityDialogProps) {\n const { open, onClose, scope, link, deal, activity, kind: presetKind } = props\n const { firestore, dataScope, hostId, mountedHostId, readTokens, writeTokens, needsSite } =\n scope\n const { consentGroup, org } = scope\n const { data: user } = useUser()\n const authorName = useUserName()\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState<Draft>(freshDraft)\n const [saving, setSaving] = useState(false)\n\n /*\n * Seeded on OPEN, not on mount: the dialog stays mounted between uses so\n * its state survives a close, and \"log another call\" must start from now\n * rather than from the last call's timestamp. The record is read through a\n * ref, because a deal is a live document and a colleague's save to it must\n * not wipe what the reader has typed so far.\n */\n const openedOn = useRef({ link, deal })\n useEffect(() => {\n openedOn.current = { link, deal }\n }, [link, deal])\n useEffect(() => {\n if (!open) return\n const seed = openedOn.current\n const primary = seed.deal ? Aglyn.dealPrimaryContactId(Aglyn.dealContactRolesOf(seed.deal)) : null\n setDraft(\n activity\n ? draftFrom(activity)\n : freshDraft(presetKind, crmActivityPicksFrom(seed.link, primary)),\n )\n }, [open, activity, presetKind])\n\n const hasOutcome = Aglyn.activityKindHasOutcome(draft.kind)\n const directions = Aglyn.CRM_ACTIVITY_DIRECTIONS[draft.kind] ?? []\n const direction = Aglyn.crmActivityDirection(draft.kind, draft.direction)\n const atMs = fromLocalInput(draft.at)\n const body = draft.body.trim()\n /*\n * A lead's own log shows neither field: a lead is not a contact **Name**\n * could hold, and in Salesforce's model too it names no What. A contact\n * beside the lead means it converted, and the entry reads like any other.\n */\n const filedOn = activity ?? link\n const leadOnly = Boolean(filedOn.leadId) && !filedOn.contactId\n const { picks } = draft\n // An activity attached to nothing is a sentence nobody can find (AGL-2606).\n const linked = leadOnly || Boolean(picks.contactId || picks.related)\n const duration = draft.durationMinutes.trim()\n ? Number(draft.durationMinutes)\n : null\n const durationValid =\n duration === null ||\n (Number.isInteger(duration) &&\n duration >= 0 &&\n duration <= DURATION_MAX_MINUTES)\n const canSave =\n Boolean(dataScope) &&\n Boolean(user?.uid) &&\n // A NEW activity is stamped with a scope: the whole org where every site\n // sees the CRM's records, else the picked site's — and in an org that\n // keeps its sites apart there is nothing to write until one is picked\n // (AGL-2630). An edit never touches either.\n (Boolean(activity) || writeTokens.length > 0) &&\n body.length > 0 &&\n atMs !== null &&\n durationValid &&\n linked &&\n !saving\n\n const handleSave = useCallback(async () => {\n // Every guard the button reads, again — a callback can outlive the\n // render that disabled it.\n if (!dataScope || !user?.uid || !body || atMs === null || !durationValid || !linked) {\n return\n }\n if (!activity && !writeTokens.length) return\n setSaving(true)\n try {\n const named = crmActivityLinkFromPicks(picks, filedOn.leadId)\n const stamped = () =>\n resolveCrmActivityCompany(\n named,\n { groupId: consentGroup?.groupId, org },\n crmActivityClientReads(firestore, dataScope[1]),\n )\n const said = {\n kind: draft.kind,\n body: body.slice(0, BODY_MAX),\n atMs,\n }\n const outcome = hasOutcome ? draft.outcome.trim().slice(0, OUTCOME_MAX) : ''\n const durationMinutes = hasOutcome && duration !== null ? duration : null\n if (activity) {\n const relinked = crmActivityPicksEqual(picks, crmActivityPicksFrom(activity))\n ? null\n : await stamped()\n await updateDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n {\n ...said,\n // Cleared rather than left: an entry moved off a deal must stop\n // showing on it. A lead's own link is never the fields' to change.\n ...(relinked\n ? {\n contactId: relinked.contactId ?? deleteField(),\n dealId: relinked.dealId ?? deleteField(),\n companyId: relinked.companyId ?? deleteField(),\n }\n : {}),\n // Cleared rather than left: a call turned into a note must not\n // keep the call's outcome, and Firestore refuses `undefined`.\n outcome: outcome ? outcome : deleteField(),\n direction: direction ?? deleteField(),\n durationMinutes:\n durationMinutes !== null ? durationMinutes : deleteField(),\n updatedAt: new Date(),\n },\n )\n enqueueSnackbar('Activity updated', { variant: 'success', persist: false })\n } else {\n /*\n * THE PER-RECORD CEILING (AGL-2611): one aggregate on the record\n * this dialog is filing under, before the write — the same count\n * the automation step and the REST create take, on the same link.\n * Scoped to the tokens this viewer may list, as every activity\n * listener is, so the rules admit the aggregate for a scoped\n * member exactly as they admit the timeline.\n */\n const lead = Aglyn.crmActivityCeilingLink(named)\n if (lead) {\n const logged = (\n await getCountFromServer(\n query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n ),\n ...crmVisibleToClause(readTokens),\n where(lead.field, '==', lead.id),\n ),\n )\n ).data().count\n if (!Aglyn.crmActivityLogHasRoom(logged)) {\n enqueueSnackbar(Aglyn.CRM_ACTIVITY_LOG_FULL_MESSAGE, {\n variant: 'warning',\n persist: false,\n })\n return\n }\n }\n const links = await stamped()\n await addDoc(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n ),\n {\n ...said,\n ...(outcome ? { outcome } : {}),\n ...(durationMinutes !== null ? { durationMinutes } : {}),\n ...(direction ? { direction } : {}),\n // Only the links it has: `stampCrmActivityCompany` leaves no blank\n // key, and Firestore refuses `undefined`.\n ...links,\n visibleTo: writeTokens,\n hostId,\n byUid: user.uid,\n // Absent rather than blank while the profile is still resolving:\n // the row reads a stored name over anything else, and an empty\n // string would be a name.\n ...(authorName ? { byName: authorName } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n )\n enqueueSnackbar('Activity logged', { variant: 'success', persist: false })\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }, [\n dataScope,\n user,\n body,\n atMs,\n durationValid,\n draft.kind,\n draft.outcome,\n direction,\n hasOutcome,\n duration,\n activity,\n firestore,\n enqueueSnackbar,\n linked,\n picks,\n filedOn.leadId,\n consentGroup,\n org,\n readTokens,\n writeTokens,\n hostId,\n authorName,\n onClose,\n ])\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{activity ? 'Edit activity' : 'Log activity'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {/* Only for a new activity at the organization level — see `canSave`. */}\n {activity ? null : (\n <CrmSitePicker\n hostId={mountedHostId}\n disabled={saving}\n optional={needsSite === false}\n helperText={\n needsSite === false\n ? undefined\n : 'The site this activity is logged from — it decides which of your sites may see it.'\n }\n />\n )}\n <Stack direction=\"row\" spacing={1} sx={{ mt: 1 }}>\n <TextField\n select\n size=\"small\"\n label=\"Kind\"\n value={draft.kind}\n onChange={(event) =>\n setDraft((prev) => ({\n ...prev,\n kind: Aglyn.isCrmActivityKind(event.target.value)\n ? event.target.value\n : prev.kind,\n }))\n }\n sx={{ minWidth: 140 }}\n >\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <MenuItem key={kind} value={kind}>\n {Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </TextField>\n {directions.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Direction\"\n value={direction ?? ''}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, direction: String(event.target.value) }))\n }\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ minWidth: 130 }}\n >\n <MenuItem value=\"\">{'Not said'}</MenuItem>\n {directions.map((entry) => (\n <MenuItem key={entry} value={entry}>\n {Aglyn.CRM_ACTIVITY_DIRECTION_LABELS[entry]}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n size=\"small\"\n label=\"When\"\n type=\"datetime-local\"\n value={draft.at}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, at: event.target.value }))\n }\n error={draft.at.length > 0 && atMs === null}\n slotProps={{ inputLabel: { shrink: true } }}\n sx={{ flex: 1 }}\n />\n </Stack>\n <TextField\n size=\"small\"\n label=\"What happened\"\n value={draft.body}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, body: event.target.value }))\n }\n multiline\n minRows={3}\n autoFocus\n slotProps={{ htmlInput: { maxLength: BODY_MAX } }}\n />\n {hasOutcome ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n size=\"small\"\n label=\"Outcome\"\n placeholder=\"Left a voicemail\"\n value={draft.outcome}\n onChange={(event) =>\n setDraft((prev) => ({ ...prev, outcome: event.target.value }))\n }\n slotProps={{ htmlInput: { maxLength: OUTCOME_MAX } }}\n sx={{ flex: 1 }}\n />\n <TextField\n size=\"small\"\n label=\"Minutes\"\n type=\"number\"\n value={draft.durationMinutes}\n onChange={(event) =>\n setDraft((prev) => ({\n ...prev,\n durationMinutes: event.target.value,\n }))\n }\n error={!durationValid}\n slotProps={{\n htmlInput: { min: 0, max: DURATION_MAX_MINUTES, step: 1 },\n }}\n sx={{ width: 120 }}\n />\n </Stack>\n ) : null}\n {leadOnly ? null : (\n <ActivityLinkFields\n scope={scope}\n picks={picks}\n onChange={(next) => setDraft((prev) => ({ ...prev, picks: next }))}\n link={link}\n deal={deal}\n disabled={saving}\n />\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={saving}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSave}\n onClick={handleSave}\n >\n {activity ? 'Save' : 'Log'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLogActivityDialog.displayName = 'LogActivityDialog'\n\nexport default LogActivityDialog\n"],"names":["Aglyn","useSnackbar","useUser","useUserName","Button","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","addDoc","collection","deleteField","doc","getCountFromServer","query","updateDoc","where","useCallback","useEffect","useRef","useState","crmActivityClientReads","crmVisibleToClause","crmActivityLinkFromPicks","crmActivityPicksEqual","crmActivityPicksFrom","resolveCrmActivityCompany","ActivityLinkFields","CrmSitePicker","BODY_MAX","OUTCOME_MAX","DURATION_MAX_MINUTES","toLocalInput","ms","Number","isFinite","date","Date","pad","value","String","padStart","getFullYear","getMonth","getDate","getHours","getMinutes","fromLocalInput","getTime","DEFAULT_KIND","freshDraft","kind","picks","body","at","now","outcome","durationMinutes","direction","draftFrom","activity","isCrmActivityKind","atMs","crmActivityDirection","LogActivityDialog","props","open","onClose","scope","link","deal","presetKind","firestore","dataScope","hostId","mountedHostId","readTokens","writeTokens","needsSite","consentGroup","org","data","user","authorName","enqueueSnackbar","draft","setDraft","saving","setSaving","openedOn","current","seed","primary","dealPrimaryContactId","dealContactRolesOf","hasOutcome","activityKindHasOutcome","directions","CRM_ACTIVITY_DIRECTIONS","trim","filedOn","leadOnly","Boolean","leadId","contactId","linked","related","duration","durationValid","isInteger","canSave","uid","length","handleSave","named","stamped","groupId","said","slice","relinked","CRM_COLLECTIONS","activities","$id","dealId","companyId","updatedAt","variant","persist","lead","crmActivityCeilingLink","logged","field","id","count","crmActivityLogHasRoom","CRM_ACTIVITY_LOG_FULL_MESSAGE","links","visibleTo","byUid","byName","createdAt","error","console","allowDuplicate","maxWidth","fullWidth","sx","display","flexDirection","gap","disabled","optional","helperText","undefined","spacing","mt","select","size","label","onChange","event","prev","target","minWidth","CRM_ACTIVITY_KINDS","map","CRM_ACTIVITY_KIND_LABELS","slotProps","displayEmpty","inputLabel","shrink","entry","CRM_ACTIVITY_DIRECTION_LABELS","type","flex","multiline","minRows","autoFocus","htmlInput","maxLength","placeholder","min","max","step","width","next","onClick","color","displayName"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { type AglynOrgBilling, type AglynPostalAddress, type ContactLifecycleSta
2
2
  import type { OrgMemberOption } from './use-org-members';
3
3
  /** What the drawer hands back — already normalized where the route would. */
4
4
  export interface NewContactValues {
5
+ /** Normalized, or `''` for a person with no address yet (AGL-4122). */
5
6
  email: string;
6
7
  /** The name as typed, or the first and last names' composition when either was given. */
7
8
  name: string;
@@ -36,8 +37,9 @@ export interface NewContactDrawerProps {
36
37
  /**
37
38
  * The site the contact is being added from — what scopes the company list
38
39
  * and the controller the consent checkbox records a basis for. `null` at
39
- * the organization level (AGL-2630), where the drawer asks which site
40
- * with a picker and holds its submit until one is named.
40
+ * the organization level (AGL-2630), where the drawer asks which site with
41
+ * a picker. An org whose records belong to every site may leave it empty,
42
+ * and one that keeps them apart holds the submit until one is named.
41
43
  */
42
44
  hostId: string | null;
43
45
  /** The org document the shell passed, for the company picker's scope. */
@@ -65,7 +67,9 @@ export interface NewContactDrawerProps {
65
67
  * The two fields a person can mistype in a way the record cannot hold — the
66
68
  * email and the phone number — are checked before the request leaves, with
67
69
  * the same normalizers the route runs, so the refusal lands under the field
68
- * rather than as a sentence from the server. Everything else is the route's
70
+ * rather than as a sentence from the server. Neither is required: a contact
71
+ * needs a name, an email or a number, because the team works a deal with the
72
+ * person in front of them and the address can come later (AGL-4122). Everything else is the route's
69
73
  * to decide: whether the address already belongs to somebody (it merges),
70
74
  * whether the plan's band has room (it refuses, and its sentence is shown
71
75
  * above the form unchanged).
@@ -41,7 +41,9 @@ import { CrmPicklistSelect } from "./picklist-select.js";
41
41
  * The two fields a person can mistype in a way the record cannot hold — the
42
42
  * email and the phone number — are checked before the request leaves, with
43
43
  * the same normalizers the route runs, so the refusal lands under the field
44
- * rather than as a sentence from the server. Everything else is the route's
44
+ * rather than as a sentence from the server. Neither is required: a contact
45
+ * needs a name, an email or a number, because the team works a deal with the
46
+ * person in front of them and the address can come later (AGL-4122). Everything else is the route's
45
47
  * to decide: whether the address already belongs to somebody (it merges),
46
48
  * whether the plan's band has room (it refuses, and its sentence is shown
47
49
  * above the form unchanged).
@@ -71,7 +73,7 @@ import { CrmPicklistSelect } from "./picklist-select.js";
71
73
  * site's group, or at the organization level the picked site's — the one
72
74
  * the route will capture the contact under. `null` until a site is
73
75
  * picked, and the submit waits on it.
74
- */ const { createHostId, createGroup, orgId } = useCrmScope({
76
+ */ const { createHostId, createGroup, createNeedsSite, orgId } = useCrmScope({
75
77
  hostId,
76
78
  org
77
79
  });
@@ -105,6 +107,7 @@ import { CrmPicklistSelect } from "./picklist-select.js";
105
107
  const [marketingConsent, setMarketingConsent] = useState(false);
106
108
  const [emailError, setEmailError] = useState('');
107
109
  const [phoneError, setPhoneError] = useState('');
110
+ const [identityError, setIdentityError] = useState('');
108
111
  /*
109
112
  * A fresh form on every opening. The drawer stays mounted behind the list
110
113
  * between uses, and a person typed and then abandoned must not reappear
@@ -130,24 +133,28 @@ import { CrmPicklistSelect } from "./picklist-select.js";
130
133
  setMarketingConsent(false);
131
134
  setEmailError('');
132
135
  setPhoneError('');
136
+ setIdentityError('');
133
137
  }, [
134
138
  open
135
139
  ]);
136
140
  const handleSubmit = ()=>{
137
141
  var _ref;
138
- const normalizedEmail = normalizeContactEmail(email);
142
+ const trimmedEmail = email.trim();
143
+ const normalizedEmail = trimmedEmail ? normalizeContactEmail(trimmedEmail) : '';
139
144
  const trimmedPhone = phone.trim();
140
145
  const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : '';
141
- setEmailError(normalizedEmail ? '' : 'Enter a valid email address.');
146
+ setEmailError(trimmedEmail && !normalizedEmail ? 'Enter a valid email address.' : '');
142
147
  setPhoneError(trimmedPhone && !normalizedPhone ? 'Enter it with its country code, like +1 512 555 0107.' : '');
143
148
  const trimmedMobile = mobilePhone.trim();
144
149
  const normalizedMobile = trimmedMobile ? normalizePhone(trimmedMobile) : '';
145
150
  setMobileError(trimmedMobile && !normalizedMobile ? 'Enter it with its country code, like +1 512 555 0107.' : '');
146
- if (!normalizedEmail || trimmedPhone && !normalizedPhone || trimmedMobile && !normalizedMobile) {
151
+ const identified = Boolean(trimmedEmail || trimmedPhone || trimmedMobile || composedName || name.trim());
152
+ setIdentityError(identified ? '' : 'Enter a name, an email or a phone number.');
153
+ if (!identified || trimmedEmail && !normalizedEmail || trimmedPhone && !normalizedPhone || trimmedMobile && !normalizedMobile) {
147
154
  return;
148
155
  }
149
156
  onSubmit({
150
- email: normalizedEmail,
157
+ email: normalizedEmail != null ? normalizedEmail : '',
151
158
  name: composedName || name.trim().slice(0, 120),
152
159
  salutation,
153
160
  firstName: firstName.trim(),
@@ -163,11 +170,12 @@ import { CrmPicklistSelect } from "./picklist-select.js";
163
170
  lifecycleStage,
164
171
  tags: parseContactTags(tags),
165
172
  address: normalizeAddress(address),
166
- marketingConsent,
173
+ marketingConsent: marketingConsent && Boolean(normalizedEmail),
167
174
  disclosedConsentGroup: consentGroup ? consentGroupDisclosureKey(consentGroup) : null
168
175
  });
169
176
  };
170
177
  const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null;
178
+ const hasEmail = Boolean(email.trim());
171
179
  // While a first or last name is typed, the name is theirs (AGL-3515).
172
180
  const composedName = composeContactName(firstName, lastName);
173
181
  return /*#__PURE__*/ _jsx(NavigationDrawerComponent, {
@@ -226,10 +234,15 @@ import { CrmPicklistSelect } from "./picklist-select.js";
226
234
  severity: "warning",
227
235
  children: error
228
236
  }) : null,
237
+ identityError ? /*#__PURE__*/ _jsx(Alert, {
238
+ severity: "warning",
239
+ children: identityError
240
+ }) : null,
229
241
  /*#__PURE__*/ _jsx(CrmSitePicker, {
230
242
  hostId: hostId,
243
+ optional: !createNeedsSite,
231
244
  disabled: Boolean(busy),
232
- helperText: "The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records."
245
+ helperText: createNeedsSite ? 'The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.' : 'Where they came from, if anywhere. Every site in the organization can see them either way.'
233
246
  }),
234
247
  /*#__PURE__*/ _jsx(TextField, {
235
248
  size: "small",
@@ -238,8 +251,7 @@ import { CrmPicklistSelect } from "./picklist-select.js";
238
251
  value: email,
239
252
  onChange: (event)=>setEmail(event.target.value),
240
253
  error: Boolean(emailError),
241
- helperText: emailError || 'The one field that is required — it is what makes two captures one person.',
242
- required: true,
254
+ helperText: emailError || 'Optional. You can add it later, and an address already on a contact merges into them.',
243
255
  fullWidth: true,
244
256
  autoFocus: true
245
257
  }),
@@ -417,22 +429,22 @@ import { CrmPicklistSelect } from "./picklist-select.js";
417
429
  children: [
418
430
  /*#__PURE__*/ _jsx(FormControlLabel, {
419
431
  control: /*#__PURE__*/ _jsx(Checkbox, {
420
- checked: marketingConsent,
432
+ checked: marketingConsent && hasEmail,
433
+ disabled: !hasEmail,
421
434
  onChange: (event)=>setMarketingConsent(event.target.checked)
422
435
  }),
423
436
  label: "This person opted in to marketing email"
424
437
  }),
425
438
  /*#__PURE__*/ _jsx(FormHelperText, {
426
- children: (disclosure ? `${disclosure} ` : '') + 'Tick it only if they agreed. Adding a contact is never itself consent.'
439
+ children: !hasEmail ? 'Add an email first: there is nowhere to send marketing without one.' : (disclosure ? `${disclosure} ` : '') + (createHostId ? '' : 'With no source site, this records it on every site. ') + 'Tick it only if they agreed. Adding a contact is never itself consent.'
427
440
  })
428
441
  ]
429
442
  }),
430
443
  /*#__PURE__*/ _jsx(Button, {
431
444
  variant: "contained",
432
445
  color: "primary",
433
- // Held until the capturing site is known: the route resolves the
434
- // org from it and stamps the record with it.
435
- disabled: Boolean(busy) || !createHostId,
446
+ // Held only where the site decides who may see the record.
447
+ disabled: Boolean(busy) || createNeedsSite && !createHostId,
436
448
  onClick: handleSubmit,
437
449
  children: busy ? 'Adding…' : 'Add contact'
438
450
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n composeContactName,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_NAME_PART_MAX,\n CRM_SALUTATION_PICKLIST,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n consentGroupDisclosureKey,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CrmPicklistSelect } from './picklist-select'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n email: string\n /** The name as typed, or the first and last names' composition when either was given. */\n name: string\n /** Salesforce's standard contact fields (AGL-3515), `''` (or `false`) for none. */\n salutation: string\n firstName: string\n lastName: string\n mobilePhone: string\n department: string\n doNotCall: boolean\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n /**\n * The key of the consent-group sentence shown beside the box, when one was\n * (AGL-3320) — what lets the route record the opt-in for the whole group\n * the sentence named. `null` when the site sends on its own.\n */\n disclosedConsentGroup: string | null\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site\n * with a picker and holds its submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup, orgId } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, open ? orgId : null)\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [salutation, setSalutation] = useState('')\n const [firstName, setFirstName] = useState('')\n const [lastName, setLastName] = useState('')\n const [mobilePhone, setMobilePhone] = useState('')\n const [department, setDepartment] = useState('')\n const [doNotCall, setDoNotCall] = useState(false)\n const [mobileError, setMobileError] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setSalutation('')\n setFirstName('')\n setLastName('')\n setMobilePhone('')\n setDepartment('')\n setDoNotCall(false)\n setMobileError('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n const trimmedMobile = mobilePhone.trim()\n const normalizedMobile = trimmedMobile ? normalizePhone(trimmedMobile) : ''\n setMobileError(\n trimmedMobile && !normalizedMobile\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n if (\n !normalizedEmail ||\n (trimmedPhone && !normalizedPhone) ||\n (trimmedMobile && !normalizedMobile)\n ) {\n return\n }\n onSubmit({\n email: normalizedEmail,\n name: composedName || name.trim().slice(0, 120),\n salutation,\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n mobilePhone: normalizedMobile ?? '',\n department: department.trim().slice(0, 120),\n doNotCall,\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent,\n disclosedConsentGroup: consentGroup\n ? consentGroupDisclosureKey(consentGroup)\n : null,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n // While a first or last name is typed, the name is theirs (AGL-3515).\n const composedName = composeContactName(firstName, lastName)\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/*\n First, because everything below it is filed under the answer: the\n site decides which of the org's sites may see the person and whose\n consent the checkbox at the foot records. Renders nothing under a\n site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — it is what makes two captures one person.'\n }\n required\n fullWidth\n autoFocus\n />\n <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n onChange={setSalutation}\n disabled={Boolean(busy)}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => setFirstName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => setLastName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Stack>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || name}\n onChange={(event) => setName(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 : 'Or the whole name in one field.'\n }\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Mobile phone\"\n value={mobilePhone}\n onChange={(event) => setMobilePhone(event.target.value)}\n error={Boolean(mobileError)}\n helperText={mobileError || undefined}\n fullWidth\n />\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => setDoNotCall(event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => setDepartment(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\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 <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.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 {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {(disclosure ? `${disclosure} ` : '') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the capturing site is known: the route resolves the\n // org from it and stamps the record with it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["composeContactName","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_NAME_PART_MAX","CRM_SALUTATION_PICKLIST","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","consentGroupDisclosureKey","normalizeAddress","normalizeContactEmail","normalizePhone","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmPicklist","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","CrmPicklistSelect","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","orgId","consentGroup","salutations","companies","enabled","createCompany","email","setEmail","name","setName","salutation","setSalutation","firstName","setFirstName","lastName","setLastName","mobilePhone","setMobilePhone","department","setDepartment","doNotCall","setDoNotCall","mobileError","setMobileError","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","handleSubmit","normalizedEmail","trimmedPhone","trim","normalizedPhone","trimmedMobile","normalizedMobile","composedName","slice","companyName","companyId","id","disclosedConsentGroup","disclosure","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","picklistId","picklist","direction","slotProps","htmlInput","maxLength","input","readOnly","undefined","control","checked","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n composeContactName,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_NAME_PART_MAX,\n CRM_SALUTATION_PICKLIST,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n consentGroupDisclosureKey,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CrmPicklistSelect } from './picklist-select'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n /** Normalized, or `''` for a person with no address yet (AGL-4122). */\n email: string\n /** The name as typed, or the first and last names' composition when either was given. */\n name: string\n /** Salesforce's standard contact fields (AGL-3515), `''` (or `false`) for none. */\n salutation: string\n firstName: string\n lastName: string\n mobilePhone: string\n department: string\n doNotCall: boolean\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n /**\n * The key of the consent-group sentence shown beside the box, when one was\n * (AGL-3320) — what lets the route record the opt-in for the whole group\n * the sentence named. `null` when the site sends on its own.\n */\n disclosedConsentGroup: string | null\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site with\n * a picker. An org whose records belong to every site may leave it empty,\n * and one that keeps them apart holds the submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Neither is required: a contact\n * needs a name, an email or a number, because the team works a deal with the\n * person in front of them and the address can come later (AGL-4122). Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup, createNeedsSite, orgId } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, open ? orgId : null)\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [salutation, setSalutation] = useState('')\n const [firstName, setFirstName] = useState('')\n const [lastName, setLastName] = useState('')\n const [mobilePhone, setMobilePhone] = useState('')\n const [department, setDepartment] = useState('')\n const [doNotCall, setDoNotCall] = useState(false)\n const [mobileError, setMobileError] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n const [identityError, setIdentityError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setSalutation('')\n setFirstName('')\n setLastName('')\n setMobilePhone('')\n setDepartment('')\n setDoNotCall(false)\n setMobileError('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n setIdentityError('')\n }, [open])\n\n const handleSubmit = () => {\n const trimmedEmail = email.trim()\n const normalizedEmail = trimmedEmail ? normalizeContactEmail(trimmedEmail) : ''\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(trimmedEmail && !normalizedEmail ? 'Enter a valid email address.' : '')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n const trimmedMobile = mobilePhone.trim()\n const normalizedMobile = trimmedMobile ? normalizePhone(trimmedMobile) : ''\n setMobileError(\n trimmedMobile && !normalizedMobile\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n const identified = Boolean(\n trimmedEmail || trimmedPhone || trimmedMobile || composedName || name.trim(),\n )\n setIdentityError(identified ? '' : 'Enter a name, an email or a phone number.')\n if (\n !identified ||\n (trimmedEmail && !normalizedEmail) ||\n (trimmedPhone && !normalizedPhone) ||\n (trimmedMobile && !normalizedMobile)\n ) {\n return\n }\n onSubmit({\n email: normalizedEmail ?? '',\n name: composedName || name.trim().slice(0, 120),\n salutation,\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n mobilePhone: normalizedMobile ?? '',\n department: department.trim().slice(0, 120),\n doNotCall,\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent: marketingConsent && Boolean(normalizedEmail),\n disclosedConsentGroup: consentGroup\n ? consentGroupDisclosureKey(consentGroup)\n : null,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n const hasEmail = Boolean(email.trim())\n // While a first or last name is typed, the name is theirs (AGL-3515).\n const composedName = composeContactName(firstName, lastName)\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {identityError ? <Alert severity=\"warning\">{identityError}</Alert> : null}\n {/*\n First, because the consent at the foot is recorded against the\n answer. Renders nothing under a site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n optional={!createNeedsSite}\n disabled={Boolean(busy)}\n helperText={\n createNeedsSite\n ? 'The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.'\n : 'Where they came from, if anywhere. Every site in the organization can see them either way.'\n }\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'Optional. You can add it later, and an address already on a contact merges into them.'\n }\n fullWidth\n autoFocus\n />\n <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n onChange={setSalutation}\n disabled={Boolean(busy)}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => setFirstName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => setLastName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Stack>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || name}\n onChange={(event) => setName(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 : 'Or the whole name in one field.'\n }\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Mobile phone\"\n value={mobilePhone}\n onChange={(event) => setMobilePhone(event.target.value)}\n error={Boolean(mobileError)}\n helperText={mobileError || undefined}\n fullWidth\n />\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => setDoNotCall(event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => setDepartment(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\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 <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.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 {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent && hasEmail}\n disabled={!hasEmail}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {!hasEmail\n ? 'Add an email first: there is nowhere to send marketing without one.'\n : (disclosure ? `${disclosure} ` : '') +\n (createHostId ? '' : 'With no source site, this records it on every site. ') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held only where the site decides who may see the record.\n disabled={Boolean(busy) || (createNeedsSite && !createHostId)}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["composeContactName","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_NAME_PART_MAX","CRM_SALUTATION_PICKLIST","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","consentGroupDisclosureKey","normalizeAddress","normalizeContactEmail","normalizePhone","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmPicklist","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","CrmPicklistSelect","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","createNeedsSite","orgId","consentGroup","salutations","companies","enabled","createCompany","email","setEmail","name","setName","salutation","setSalutation","firstName","setFirstName","lastName","setLastName","mobilePhone","setMobilePhone","department","setDepartment","doNotCall","setDoNotCall","mobileError","setMobileError","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","identityError","setIdentityError","handleSubmit","trimmedEmail","trim","normalizedEmail","trimmedPhone","normalizedPhone","trimmedMobile","normalizedMobile","identified","Boolean","composedName","slice","companyName","companyId","id","disclosedConsentGroup","disclosure","hasEmail","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","optional","disabled","helperText","size","label","type","value","onChange","event","target","fullWidth","autoFocus","picklistId","picklist","direction","slotProps","htmlInput","maxLength","input","readOnly","undefined","control","checked","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder"],"mappings":"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"}
@@ -25,6 +25,7 @@ import { collection, doc, getCountFromServer, query, updateDoc, where } from "fi
25
25
  import { useCallback, useEffect, useState } from "react";
26
26
  import { crmVisibleToClause } from "../hooks/use-crm-scope.js";
27
27
  import { addStage, moveStage, removeStage, renameStage, setStageForecastCategory, setStageProbability, sortedStages, stageRemovalRefusal, stagesProblem } from "../model/deal-board-model.js";
28
+ import { settleStageNames } from "../model/settle-stage-names.js";
28
29
  /**
29
30
  * The pipeline's stages, edited as one draft and saved as one write.
30
31
  *
@@ -116,7 +117,7 @@ import { addStage, moveStage, removeStage, renameStage, setStageForecastCategory
116
117
  fromCache
117
118
  }, async ()=>{
118
119
  await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.pipelines, pipeline.$id), {
119
- stages,
120
+ stages: settleStageNames(stages),
120
121
  updatedAt: new Date()
121
122
  });
122
123
  });
@@ -242,6 +243,7 @@ import { addStage, moveStage, removeStage, renameStage, setStageForecastCategory
242
243
  children: /*#__PURE__*/ _jsx("span", {
243
244
  children: /*#__PURE__*/ _jsx(IconButton, {
244
245
  size: "small",
246
+ "aria-label": `Move ${stage.name} up`,
245
247
  disabled: first,
246
248
  onClick: ()=>setStages((current)=>moveStage(current, stage.id, 'up')),
247
249
  children: /*#__PURE__*/ _jsx(MdiIcon, {
@@ -256,6 +258,7 @@ import { addStage, moveStage, removeStage, renameStage, setStageForecastCategory
256
258
  children: /*#__PURE__*/ _jsx("span", {
257
259
  children: /*#__PURE__*/ _jsx(IconButton, {
258
260
  size: "small",
261
+ "aria-label": `Move ${stage.name} down`,
259
262
  disabled: lastOpen,
260
263
  onClick: ()=>setStages((current)=>moveStage(current, stage.id, 'down')),
261
264
  children: /*#__PURE__*/ _jsx(MdiIcon, {
@@ -271,6 +274,7 @@ import { addStage, moveStage, removeStage, renameStage, setStageForecastCategory
271
274
  children: /*#__PURE__*/ _jsx(IconButton, {
272
275
  size: "small",
273
276
  color: "error",
277
+ "aria-label": `Remove ${stage.name}`,
274
278
  disabled: counting === stage.id || busy,
275
279
  onClick: ()=>void handleRemove(stage.id),
276
280
  children: /*#__PURE__*/ _jsx(MdiIcon, {