@aglyn/plugins-crm 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +6 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +6 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +28 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +36 -7
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +37 -0
- package/src/lib/declarations.console-server.js +103 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +6 -5
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +185 -0
- package/src/lib/model/crm-digest.js +272 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +832 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +2 -1
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +107 -0
- package/src/lib/server/daily-digest-route.js +588 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +257 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +497 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-send-email-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 {\n type AglynOrgBilling,\n CRM_EMAIL_BODY_MAX,\n CRM_EMAIL_SUBJECT_MAX,\n normalizeContactEmail,\n consentGroupForHost,\n CRM_COLLECTIONS,\n createResourceUid,\n type CrmEmailTemplateRow,\n type CrmMergeContext,\n type CrmMergeFieldGroup,\n crmMergeUnresolvedMessage,\n hasCrmMergeFields,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport type { ConsoleRecordEmailDraft } from '@aglyn/aglyn/plugin-manager/record-zone-props'\nimport { useSendingApi } from '@aglyn/tenant-feature-instance/hooks/use-sending-identity-api'\nimport { AppLink, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useOrgDataScope,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n InputAdornment,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, getDoc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { CrmRecordBookingZone } from './crm-booking-zone'\nimport { insertLinkAtCaret } from './insert-link-at-caret'\nimport {\n CAPTURE_ADDRESS_HELPER,\n CopyCaptureAddressButton,\n} from './copy-capture-address-button'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CrmEmailCheckAlert, useCrmEmailCheck } from './crm-email-check'\nimport { useCrmApi } from './use-crm-api'\nimport { useCrmInboundAddress } from './use-crm-inbound-address'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport {\n useCrmEmailTemplates,\n useCrmEmailTemplateScope,\n} from '../hooks/use-crm-email-templates'\nimport {\n CrmInsertMenu,\n CrmSaveAsTemplateDialog,\n CrmTemplatePicker,\n} from './crm-email-template-tools'\n\n/**\n * The documents a merge-field preview reads (AGL-2658), fetched once per\n * open and only once the draft names a field: a letter with no fields\n * costs no reads. `site` is the sending site's name; `loaded` says the\n * fetch has answered, so the helper text can tell \"no value\" from \"not\n * yet read\".\n */\ninterface MergeRecords {\n contact: Record<string, unknown> | null\n deal: Record<string, unknown> | null\n lead: Record<string, unknown> | null\n site: string | null\n loaded: boolean\n}\n\nconst NO_MERGE_RECORDS: MergeRecords = {\n contact: null,\n deal: null,\n lead: null,\n site: null,\n loaded: false,\n}\n\n/**\n * What the sending-identity route said about this site, as the dialog\n * needs it: an address to print, or the reason there is none — or, at the\n * organization level, that no site has been picked to ask about.\n */\ntype IdentityState =\n | { status: 'loading' }\n | { status: 'site' }\n | { status: 'ready'; from: string }\n | { status: 'refused'; message: string; canManage: boolean }\n | { status: 'error'; message: string }\n\nexport interface CrmSendEmailDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The site the message leaves from — passed in, never assumed from the\n * URL. At the organization level the record's own capturing site, or\n * `null` for a record no site has captured (AGL-2634): the dialog then\n * offers the org's sites to send from, defaulting to the reader's pick.\n */\n hostId: string | null\n /** The org the shell passed; lets a deal's contact be read without a lookup. */\n org?: Partial<AglynOrgBilling> | null\n contactId?: string\n leadId?: string\n dealId?: string\n /**\n * The recipient as the record shows it. A deal's page holds no address,\n * so a caller with only a `contactId` may leave this out and the dialog\n * reads it off the contact when it opens.\n */\n email?: string | null\n /** What the dialog says it is writing to, beside the address. */\n name?: string | null\n /** Told once the route has accepted the message. */\n onSent?: (result: { activityId: string }) => void\n}\n\n/**\n * One email to one person, from their record (AGL-2615).\n *\n * ## What the reader sees, and what they cannot change\n *\n * To is the record's address and is read-only: the route resolves the\n * recipient off the record again, so an editable field here would be a\n * promise the server does not keep. From is the site's sending identity as\n * the sending-identity route reports it — the same answer the campaign\n * composer prints — and when that route refuses, the dialog says why and\n * points at **Emails › Sending** rather than offering a Send button that\n * 409s. Reply-to is the signed-in user's own address, because a reply to a\n * relationship email belongs in the inbox of the person who wrote it, not\n * the site's shared mailbox. Subject and Body are the whole of what a\n * person writes; the body is plain paragraphs, and the email design system\n * is deliberately not here — this is a letter, not a campaign.\n *\n * ## A refusal stays on screen\n *\n * The route's refusals — the daily cap, a suppressed address, a stated\n * refusal, the ceiling — are answers the rep should read, so they render\n * inside the dialog with the draft intact rather than as a toast over a\n * closed one. Only a send the provider accepted closes it.\n *\n * Mounted on open by its button, so the identity request and the contact\n * read happen when somebody asks and not on every record page paint.\n */\nexport function CrmSendEmailDialog(props: CrmSendEmailDialogProps) {\n const { open, onClose, hostId, org, contactId, leadId, dealId, onSent } = props\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const { orgId } = useOrgDataScope({\n hostId: hostId ?? undefined,\n orgId: org?.$id ?? mount?.orgId,\n })\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const sendingApi = useSendingApi()\n /*\n * The site the message LEAVES FROM: the record's own, or at the\n * organization level — for a record no site captured — the site the\n * reader picked (AGL-2634). The identity, the suppression list and the\n * person's stated refusal are each that site's, which is why the org\n * level cannot do without one.\n */\n const sendHostId = hostId ?? mount?.createHostId ?? null\n const crmApi = useCrmApi(sendHostId)\n const emailsHub = useEmailsHubPath(sendHostId)\n // The capture address (AGL-2657), for the reply this send will get: the\n // console logs what IT sends, so the address is shown to be forwarded\n // to or copied from a mailbox, never put on this message.\n const capture = useCrmInboundAddress(sendHostId, { enabled: open })\n // The Sending section of the Emails console — the page that fixes a\n // missing identity. `null` on a surface that cannot name the site's hub,\n // so the refusal prints the section's name instead of a link to nowhere.\n const sendingPath = emailsHub ? `${emailsHub}/sending` : null\n\n const [identity, setIdentity] = useState<IdentityState>({ status: 'loading' })\n const [recipient, setRecipient] = useState<string | null>(\n normalizeContactEmail(props.email) ?? null,\n )\n const [subject, setSubject] = useState('')\n const [body, setBody] = useState('')\n const [sending, setSending] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n /*\n * TEMPLATES, SNIPPETS AND MERGE FIELDS (AGL-2658). The listener is the\n * dialog's own scope — the mounted site's tokens, or none at the org\n * level — and what a saved template is stamped with follows the same\n * rule. The message field's element is held so a snippet lands at the\n * caret rather than at the end.\n */\n const { confirm } = useConfirmationContext()\n const templateScope = useCrmEmailTemplateScope({ hostId, org })\n const { templates } = useCrmEmailTemplates({\n scope: templateScope.scope,\n visibleTo: templateScope.visibleTo,\n uid: user?.uid,\n })\n const pickable = useMemo(() => templates.filter((row) => row.kind === 'template'), [templates])\n const snippets = useMemo(() => templates.filter((row) => row.kind === 'snippet'), [templates])\n const [appliedTemplate, setAppliedTemplate] = useState<CrmEmailTemplateRow | null>(null)\n const [saveOpen, setSaveOpen] = useState(false)\n const [records, setRecords] = useState<MergeRecords>(NO_MERGE_RECORDS)\n const messageRef = useRef<HTMLTextAreaElement | null>(null)\n const draftHasFields = hasCrmMergeFields(`${subject}\\n${body}`)\n // The groups the Insert menu offers: what the record can answer.\n const fieldGroups = useMemo<CrmMergeFieldGroup[]>(\n () => [\n ...(contactId || dealId ? (['Contact'] as const) : []),\n ...(leadId && !contactId ? (['Lead'] as const) : []),\n ...(dealId ? (['Deal'] as const) : []),\n 'You',\n 'Site',\n ],\n [contactId, dealId, leadId],\n )\n\n /*\n * The identity, asked for on open. The route is the send path's own\n * resolver, so what it prints here is what the message will leave as; a\n * dialog that derived \"verified\" from the domain list would be a second\n * opinion that drifts from the send.\n */\n useEffect(() => {\n if (!open) return\n let cancelled = false\n setError(null)\n // The draft survives a change of sending site — the site is not the\n // letter — and is cleared only when the dialog opens.\n if (!sendHostId) {\n setIdentity({ status: 'site' })\n return undefined\n }\n setIdentity({ status: 'loading' })\n void (async () => {\n const { response, payload } = await sendingApi({\n path: 'sending-identity',\n method: 'GET',\n query: { hostId: sendHostId },\n })\n if (cancelled) return\n if (!response.ok) {\n setIdentity({\n status: 'error',\n message: String(payload['error'] ?? 'The sending identity could not be read.'),\n })\n return\n }\n const refusal = payload['refusal'] as { message?: string } | null\n if (refusal) {\n setIdentity({\n status: 'refused',\n message: String(refusal.message ?? 'This site has no verified sending identity.'),\n canManage: payload['canManage'] === true,\n })\n return\n }\n const senders = Array.isArray(payload['senders'])\n ? (payload['senders'] as { isDefault?: boolean; from?: string | null }[])\n : []\n const from =\n senders.find((sender) => sender.isDefault && sender.from)?.from ??\n senders.find((sender) => sender.from)?.from ??\n null\n setIdentity(\n from\n ? { status: 'ready', from }\n : {\n status: 'error',\n message: String(payload['identity'] ?? 'No sending address is in effect.'),\n },\n )\n })()\n return () => {\n cancelled = true\n }\n }, [open, sendHostId, sendingApi])\n useEffect(() => {\n if (!open) return\n setSubject('')\n setBody('')\n setAppliedTemplate(null)\n setSaveOpen(false)\n setRecords(NO_MERGE_RECORDS)\n }, [open])\n\n /*\n * The documents a preview reads, fetched the first time the draft names\n * a merge field and kept for the rest of the open: the contact, the\n * deal, the lead, and the sending site's name — off the org mount when\n * it knows the site, off the host document otherwise. The route reads\n * the same documents again for the send; these are what the rep sees.\n */\n useEffect(() => {\n if (!open || !draftHasFields || records.loaded) return\n let cancelled = false\n const read = async (path: string[] | null): Promise<Record<string, unknown> | null> => {\n if (!path) return null\n const snapshot = await getDoc(doc(firestore, path[0], ...path.slice(1)))\n return snapshot.exists() ? ((snapshot.data() ?? null) as Record<string, unknown> | null) : null\n }\n const mountedSite = sendHostId\n ? (mount?.hosts ?? []).find((host) => host.id === sendHostId)\n : undefined\n void Promise.all([\n read(contactId && orgId ? ['orgs', orgId, 'contacts', contactId] : null),\n read(dealId && orgId ? ['orgs', orgId, CRM_COLLECTIONS.deals, dealId] : null),\n // One org row (AGL-3275); the site still decides WHICH lead is offered,\n // but not where it lives.\n read(leadId && orgId ? ['orgs', orgId, 'leads', leadId] : null),\n mountedSite || !sendHostId ? null : read(['hosts', sendHostId]),\n ])\n .then(([contact, deal, lead, host]) => {\n if (cancelled) return\n // A deal's contact, when the page named only the deal.\n const dealContactId = String(deal?.['contactId'] ?? '').trim()\n const withContact =\n !contact && dealContactId && orgId\n ? read(['orgs', orgId, 'contacts', dealContactId])\n : Promise.resolve(contact)\n return withContact.then((resolvedContact) => {\n if (cancelled) return\n setRecords({\n contact: resolvedContact,\n deal,\n lead,\n site: mountedSite?.name ?? (host ? String(host['displayName'] ?? '') : null),\n loaded: true,\n })\n })\n })\n .catch((cause) => {\n console.error(cause)\n if (!cancelled) setRecords({ ...NO_MERGE_RECORDS, loaded: true })\n })\n return () => {\n cancelled = true\n }\n }, [\n open,\n draftHasFields,\n records.loaded,\n firestore,\n orgId,\n contactId,\n dealId,\n leadId,\n sendHostId,\n mount?.hosts,\n ])\n\n const mergeContext = useMemo<CrmMergeContext>(\n () => ({\n contact: records.contact,\n contactGroupId: sendHostId ? consentGroupForHost(org ?? null, sendHostId).groupId : null,\n deal: records.deal,\n lead: records.lead,\n sender: { name: user?.displayName ?? '', email: user?.email ?? '' },\n site: { name: records.site ?? '' },\n }),\n [records, sendHostId, org, user?.displayName, user?.email],\n )\n const preview = useMemo(\n () =>\n draftHasFields && records.loaded\n ? {\n subject: resolveCrmMergeFields(subject, mergeContext),\n body: resolveCrmMergeFields(body, mergeContext),\n }\n : null,\n [draftHasFields, records.loaded, subject, body, mergeContext],\n )\n const unresolved = useMemo(\n () =>\n preview\n ? [...new Set([...preview.subject.unresolved, ...preview.body.unresolved])]\n : [],\n [preview],\n )\n\n /** A template into both fields — asking first when a message is already written. */\n const applyTemplate = useCallback(\n async (template: CrmEmailTemplateRow | null) => {\n if (!template) {\n setAppliedTemplate(null)\n return\n }\n const written = body.trim().length > 0 && body !== appliedTemplate?.body\n if (written) {\n const accepted = await confirm({\n title: 'Replace the message?',\n description: `\"${template.name}\" replaces what you have written in the subject and the message.`,\n confirmationText: 'Replace',\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n if (template.subject) setSubject(template.subject)\n setBody(template.body)\n setAppliedTemplate(template)\n },\n [body, appliedTemplate, confirm],\n )\n\n /*\n * The `recordEmail` zone (AGL-2917), hosted through the shell's renderer: a\n * widget there proposes a subject and a message, which land in the fields\n * above like a template does, asking first when a message is already\n * written. Send is still the only write, and the zone reads the record the\n * message is written from: the deal over its contact, a lead on its own site,\n * and at the organization level a contact or a deal the org reads.\n */\n const EmailDraftSlot = useConsoleWidgetSlot()\n const proposeDraft = useCallback(\n async (draft: ConsoleRecordEmailDraft) => {\n const next = { subject: draft.subject.slice(0, CRM_EMAIL_SUBJECT_MAX), body: draft.body.slice(0, CRM_EMAIL_BODY_MAX) }\n if (body.trim() && body !== next.body) {\n const accepted = await confirm({\n title: 'Replace the message?',\n description: 'The draft replaces what you have written in the subject and the message.',\n confirmationText: 'Replace',\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setSubject(next.subject)\n setBody(next.body)\n setAppliedTemplate(null)\n },\n [body, confirm],\n )\n const draftRecord = dealId\n ? { kind: 'deal', id: dealId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : contactId\n ? { kind: 'contact', id: contactId }\n : null\n\n /** A snippet or a field where the caret is, the caret moved past it. */\n const insertAtCaret = useCallback(\n (text: string) => {\n const element = messageRef.current\n const start = element?.selectionStart ?? body.length\n const end = element?.selectionEnd ?? body.length\n const next = `${body.slice(0, start)}${text}${body.slice(end)}`.slice(0, CRM_EMAIL_BODY_MAX)\n setBody(next)\n const caret = Math.min(start + text.length, next.length)\n window.setTimeout(() => {\n element?.focus()\n element?.setSelectionRange(caret, caret)\n }, 0)\n },\n [body],\n )\n\n /**\n * The draft kept as a template, stamped as this surface's scope stamps\n * every CRM record — the site's tokens under a site, the org token from\n * the organization's hub — and, when personal, owned by the writer.\n */\n const saveAsTemplate = useCallback(\n async (draft: { name: string; visibility: 'shared' | 'personal' }) => {\n const scope = templateScope.scope\n const uid = user?.uid\n if (!scope || !uid || templateScope.createTokens.length === 0) {\n throw new Error('a template needs an organization and a signed-in writer')\n }\n const now = Date.now()\n await setDoc(\n doc(collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.emailTemplates), createResourceUid()),\n {\n name: draft.name,\n subject: subject.trim(),\n body: body.trim(),\n kind: 'template',\n visibility: draft.visibility,\n ...(draft.visibility === 'personal' ? { ownerUid: uid } : {}),\n createdByUid: uid,\n createdAtMs: now,\n updatedAtMs: now,\n hostId: templateScope.createHostId,\n visibleTo: [...templateScope.createTokens],\n createdAt: new Date(now),\n updatedAt: new Date(now),\n },\n )\n enqueueSnackbar(`Template \"${draft.name}\" saved`, { variant: 'success', persist: false })\n setSaveOpen(false)\n },\n [templateScope, user?.uid, firestore, subject, body, enqueueSnackbar],\n )\n\n /*\n * The address, when the caller had none: one read of the contact, on\n * open. The route reads it again for itself; this one is what the To\n * field prints, and a field that printed nothing until the send answered\n * would ask the rep to write to somebody unnamed.\n */\n useEffect(() => {\n if (!open) return\n const given = normalizeContactEmail(props.email)\n if (given) {\n setRecipient(given)\n return\n }\n if (!contactId || !orgId) return\n let cancelled = false\n void getDoc(doc(firestore, 'orgs', orgId, 'contacts', contactId))\n .then((snapshot) => {\n if (cancelled) return\n setRecipient(normalizeContactEmail(snapshot.get('email')) ?? null)\n })\n .catch((cause) => {\n console.error(cause)\n if (!cancelled) setRecipient(null)\n })\n return () => {\n cancelled = true\n }\n }, [open, props.email, contactId, orgId, firestore])\n\n /*\n * What the platform knows about the address before Send (AGL-3328): a\n * domain with no mail server, or a mail gateway that refused this site's\n * sending domain twice this month. Read off the server's cache — the\n * browser does no DNS — and said as a warning; the route decides.\n */\n const deliverability = useCrmEmailCheck(sendHostId, recipient, { enabled: open })\n\n const replyTo = normalizeContactEmail(user?.email) ?? ''\n const canSend =\n identity.status === 'ready' &&\n Boolean(recipient) &&\n subject.trim().length > 0 &&\n body.trim().length > 0 &&\n !sending\n\n const handleSend = useCallback(async () => {\n if (!canSend) return\n setSending(true)\n setError(null)\n try {\n const { response, payload } = await crmApi('email-send', {\n ...(contactId ? { contactId } : {}),\n ...(leadId ? { leadId } : {}),\n ...(dealId ? { dealId } : {}),\n subject: subject.trim(),\n body: body.trim(),\n })\n if (!response.ok) {\n setError(String(payload['error'] ?? 'The email could not be sent.'))\n return\n }\n enqueueSnackbar('Email sent', { variant: 'success', persist: false })\n onSent?.({ activityId: String(payload['activityId'] ?? '') })\n onClose()\n } catch (cause) {\n console.error(cause)\n setError('The email could not be sent.')\n } finally {\n setSending(false)\n }\n }, [canSend, crmApi, contactId, leadId, dealId, subject, body, enqueueSnackbar, onSent, onClose])\n\n const toLabel = props.name && recipient ? `${props.name} <${recipient}>` : (recipient ?? '')\n\n /*\n * The booking door (AGL-2660): a link to one of the sending site's\n * services, dropped where the caret is. The link carries the most\n * specific record this message is about — the deal over its contact, so\n * the booking lands on the deal's timeline as the email does.\n */\n const bookingRef = dealId\n ? { kind: 'deal' as const, id: dealId }\n : leadId\n ? { kind: 'lead' as const, id: leadId }\n : contactId\n ? { kind: 'contact' as const, id: contactId }\n : null\n const handleInsertLink = useCallback((link: string) => {\n const input = messageRef.current\n const current = input?.value ?? ''\n const start = input?.selectionStart ?? current.length\n const end = input?.selectionEnd ?? start\n const next = insertLinkAtCaret(current, link, start, end)\n setBody(next.text)\n // Once the new value has painted, typing continues after the link.\n window.setTimeout(() => {\n input?.focus()\n input?.setSelectionRange(next.caret, next.caret)\n }, 0)\n }, [])\n\n return (\n <Dialog open={open} onClose={sending ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{'Send email'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {!hostId ? (\n <CrmSitePicker\n hostId={hostId}\n label=\"Send from\"\n helperText=\"The site whose sending address the email leaves on.\"\n disabled={sending}\n />\n ) : null}\n {identity.status === 'site' ? (\n <Alert severity=\"info\" sx={{ mt: 1 }}>\n {'Pick the site the email leaves from.'}\n </Alert>\n ) : identity.status === 'refused' ? (\n <Alert severity=\"warning\" sx={{ mt: 1 }}>\n <Typography variant=\"body2\">{identity.message}</Typography>\n {sendingPath ? (\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n <AppLink href={sendingPath}>\n {identity.canManage ? 'Set up sending' : 'See sending'}\n </AppLink>\n </Typography>\n ) : (\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n {'An admin sets this up under Emails › Sending.'}\n </Typography>\n )}\n </Alert>\n ) : identity.status === 'error' ? (\n <Alert severity=\"error\" sx={{ mt: 1 }}>\n {identity.message}\n </Alert>\n ) : null}\n {open && !recipient && (identity.status === 'ready' || identity.status === 'loading') ? (\n <Alert severity=\"warning\" sx={{ mt: 1 }}>\n {'This record has no email address.'}\n </Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"To\"\n value={toLabel}\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n sx={{ mt: identity.status === 'ready' || identity.status === 'loading' ? 1 : 0 }}\n />\n <CrmEmailCheckAlert check={deliverability} />\n <TextField\n size=\"small\"\n label=\"From\"\n value={\n identity.status === 'ready'\n ? identity.from\n : identity.status === 'loading'\n ? 'Resolving the site’s sending address…'\n : identity.status === 'site'\n ? 'Pick a site to send from'\n : ''\n }\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n />\n <TextField\n size=\"small\"\n label=\"Reply-to\"\n value={replyTo}\n helperText=\"Replies come to you, not to the site's mailbox.\"\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n />\n {capture.status === 'ready' ? (\n <TextField\n size=\"small\"\n label=\"Log replies\"\n value={capture.address}\n helperText={CAPTURE_ADDRESS_HELPER}\n slotProps={{\n input: {\n readOnly: true,\n endAdornment: (\n <InputAdornment position=\"end\">\n <CopyCaptureAddressButton address={capture.address} />\n </InputAdornment>\n ),\n },\n inputLabel: { shrink: true },\n }}\n />\n ) : null}\n {pickable.length ? (\n <CrmTemplatePicker\n templates={pickable}\n value={appliedTemplate}\n onChange={(template) => void applyTemplate(template)}\n disabled={sending}\n />\n ) : null}\n <TextField\n size=\"small\"\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value)}\n autoFocus\n slotProps={{ htmlInput: { maxLength: CRM_EMAIL_SUBJECT_MAX } }}\n />\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <CrmInsertMenu\n snippets={snippets}\n groups={fieldGroups}\n disabled={sending}\n onInsert={insertAtCaret}\n />\n <Button\n size=\"small\"\n disabled={sending || !templateScope.scope || (!subject.trim() && !body.trim())}\n onClick={() => setSaveOpen(true)}\n >\n {'Save as template…'}\n </Button>\n </Stack>\n <TextField\n size=\"small\"\n label=\"Message\"\n value={body}\n onChange={(event) => setBody(event.target.value)}\n multiline\n minRows={6}\n inputRef={messageRef}\n helperText={\n unresolved.length\n ? crmMergeUnresolvedMessage(unresolved)\n : draftHasFields\n ? 'Plain text. Merge fields are filled in from this record when the email is sent.'\n : 'Plain text. A blank line starts a new paragraph.'\n }\n slotProps={{\n htmlInput: { maxLength: CRM_EMAIL_BODY_MAX },\n formHelperText: { sx: unresolved.length ? { color: 'warning.main' } : undefined },\n }}\n />\n {EmailDraftSlot && draftRecord ? (\n <EmailDraftSlot\n slot=\"recordEmail\"\n // A lead is read on its own site; a contact or a deal at the level\n // the composer was opened at.\n hostId={draftRecord.kind === 'lead' || !mount ? hostId : null}\n orgId={orgId ?? undefined}\n record={{ ...draftRecord, name: String(props.name ?? '') }}\n subject={subject}\n body={body}\n proposeDraft={(draft: ConsoleRecordEmailDraft) => void proposeDraft(draft)}\n />\n ) : null}\n {preview ? (\n <Stack spacing={0.5} data-testid=\"crm-email-preview\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Preview — as it will be sent'}\n </Typography>\n <Typography variant=\"subtitle2\">{preview.subject.text}</Typography>\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {preview.body.text}\n </Typography>\n </Stack>\n ) : null}\n {bookingRef ? (\n <CrmRecordBookingZone\n variant=\"chip\"\n hostId={sendHostId}\n org={org}\n kind={bookingRef.kind}\n recordId={bookingRef.id}\n onInsert={handleInsertLink}\n />\n ) : null}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </DialogContent>\n <CrmSaveAsTemplateDialog\n open={saveOpen}\n onClose={() => setSaveOpen(false)}\n onSave={saveAsTemplate}\n />\n <DialogActions>\n <Button onClick={onClose} disabled={sending}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" color=\"primary\" disabled={!canSend} onClick={handleSend}>\n {sending ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nCrmSendEmailDialog.displayName = 'CrmSendEmailDialog'\n\nexport default CrmSendEmailDialog\n"],"names":["CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","normalizeContactEmail","consentGroupForHost","CRM_COLLECTIONS","createResourceUid","crmMergeUnresolvedMessage","hasCrmMergeFields","resolveCrmMergeFields","useConsoleWidgetSlot","useSendingApi","AppLink","useConfirmationContext","useSnackbar","useFirestore","useOrgDataScope","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","InputAdornment","Stack","TextField","Typography","collection","doc","getDoc","setDoc","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","CrmRecordBookingZone","insertLinkAtCaret","CAPTURE_ADDRESS_HELPER","CopyCaptureAddressButton","CrmSitePicker","CrmEmailCheckAlert","useCrmEmailCheck","useCrmApi","useCrmInboundAddress","useEmailsHubPath","useCrmEmailTemplates","useCrmEmailTemplateScope","CrmInsertMenu","CrmSaveAsTemplateDialog","CrmTemplatePicker","NO_MERGE_RECORDS","contact","deal","lead","site","loaded","CrmSendEmailDialog","props","hostId","open","onClose","org","contactId","leadId","dealId","onSent","firestore","mount","orgId","undefined","$id","data","user","enqueueSnackbar","sendingApi","sendHostId","createHostId","crmApi","emailsHub","capture","enabled","sendingPath","identity","setIdentity","status","recipient","setRecipient","email","subject","setSubject","body","setBody","sending","setSending","error","setError","confirm","templateScope","templates","scope","visibleTo","uid","pickable","filter","row","kind","snippets","appliedTemplate","setAppliedTemplate","saveOpen","setSaveOpen","records","setRecords","messageRef","draftHasFields","fieldGroups","cancelled","senders","payload","response","path","method","query","ok","message","String","refusal","canManage","Array","isArray","from","find","sender","isDefault","read","snapshot","slice","exists","mountedSite","hosts","host","id","Promise","all","deals","then","dealContactId","trim","withContact","resolve","resolvedContact","name","catch","cause","console","mergeContext","contactGroupId","groupId","displayName","preview","unresolved","Set","applyTemplate","template","written","length","accepted","title","description","confirmationText","EmailDraftSlot","proposeDraft","draft","next","draftRecord","insertAtCaret","text","element","current","start","selectionStart","end","selectionEnd","caret","Math","min","window","setTimeout","focus","setSelectionRange","saveAsTemplate","createTokens","Error","now","Date","emailTemplates","visibility","ownerUid","createdByUid","createdAtMs","updatedAtMs","createdAt","updatedAt","variant","persist","given","get","deliverability","replyTo","canSend","Boolean","handleSend","activityId","toLabel","bookingRef","handleInsertLink","link","input","value","maxWidth","fullWidth","sx","display","flexDirection","gap","label","helperText","disabled","severity","mt","href","size","slotProps","readOnly","inputLabel","shrink","check","address","endAdornment","position","onChange","event","target","autoFocus","htmlInput","maxLength","direction","spacing","justifyContent","groups","onInsert","onClick","multiline","minRows","inputRef","formHelperText","color","slot","record","data-testid","whiteSpace","recordId","onSave"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-send-email-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 {\n type AglynOrgBilling,\n CRM_EMAIL_BODY_MAX,\n CRM_EMAIL_SUBJECT_MAX,\n normalizeContactEmail,\n consentGroupForHost,\n CRM_COLLECTIONS,\n createResourceUid,\n type CrmEmailTemplateRow,\n type CrmMergeContext,\n type CrmMergeFieldGroup,\n crmMergeUnresolvedMessage,\n hasCrmMergeFields,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport type { ConsoleRecordEmailDraft } from '@aglyn/aglyn/plugin-manager/record-zone-props'\nimport { useSendingApi } from '@aglyn/tenant-feature-instance/hooks/use-sending-identity-api'\nimport { AppLink, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useOrgDataScope,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n InputAdornment,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, getDoc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { CrmRecordBookingZone } from './crm-booking-zone'\nimport { insertLinkAtCaret } from './insert-link-at-caret'\nimport {\n CAPTURE_ADDRESS_HELPER,\n CopyCaptureAddressButton,\n} from './copy-capture-address-button'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CrmEmailCheckAlert, useCrmEmailCheck } from './crm-email-check'\nimport { useCrmApi } from './use-crm-api'\nimport { useCrmInboundAddress } from './use-crm-inbound-address'\nimport { useSiteRouteContext } from './use-site-route-context'\nimport {\n useCrmEmailTemplates,\n useCrmEmailTemplateScope,\n} from '../hooks/use-crm-email-templates'\nimport {\n CrmInsertMenu,\n CrmSaveAsTemplateDialog,\n CrmTemplatePicker,\n} from './crm-email-template-tools'\n\n/**\n * The documents a merge-field preview reads (AGL-2658), fetched once per\n * open and only once the draft names a field: a letter with no fields\n * costs no reads. `site` is the sending site's name; `loaded` says the\n * fetch has answered, so the helper text can tell \"no value\" from \"not\n * yet read\".\n */\ninterface MergeRecords {\n contact: Record<string, unknown> | null\n deal: Record<string, unknown> | null\n lead: Record<string, unknown> | null\n site: string | null\n loaded: boolean\n}\n\nconst NO_MERGE_RECORDS: MergeRecords = {\n contact: null,\n deal: null,\n lead: null,\n site: null,\n loaded: false,\n}\n\n/**\n * What the sending-identity route said about this site, as the dialog\n * needs it: an address to print, or the reason there is none — or, at the\n * organization level, that no site has been picked to ask about.\n */\ntype IdentityState =\n | { status: 'loading' }\n | { status: 'site' }\n | { status: 'ready'; from: string }\n | { status: 'refused'; message: string; canManage: boolean }\n | { status: 'error'; message: string }\n\nexport interface CrmSendEmailDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The site the message leaves from — passed in, never assumed from the\n * URL. At the organization level the record's own capturing site, or\n * `null` for a record no site has captured (AGL-2634): the dialog then\n * offers the org's sites to send from, defaulting to the reader's pick.\n */\n hostId: string | null\n /** The org the shell passed; lets a deal's contact be read without a lookup. */\n org?: Partial<AglynOrgBilling> | null\n contactId?: string\n leadId?: string\n dealId?: string\n /**\n * The recipient as the record shows it. A deal's page holds no address,\n * so a caller with only a `contactId` may leave this out and the dialog\n * reads it off the contact when it opens.\n */\n email?: string | null\n /** What the dialog says it is writing to, beside the address. */\n name?: string | null\n /** Told once the route has accepted the message. */\n onSent?: (result: { activityId: string }) => void\n}\n\n/**\n * One email to one person, from their record (AGL-2615).\n *\n * ## What the reader sees, and what they cannot change\n *\n * To is the record's address and is read-only: the route resolves the\n * recipient off the record again, so an editable field here would be a\n * promise the server does not keep. From is the site's sending identity as\n * the sending-identity route reports it — the same answer the campaign\n * composer prints — and when that route refuses, the dialog says why and\n * points at **Emails › Sending** rather than offering a Send button that\n * 409s. Reply-to is the signed-in user's own address, because a reply to a\n * relationship email belongs in the inbox of the person who wrote it, not\n * the site's shared mailbox. Subject and Body are the whole of what a\n * person writes; the body is plain paragraphs, and the email design system\n * is deliberately not here — this is a letter, not a campaign.\n *\n * ## A refusal stays on screen\n *\n * The route's refusals — the daily cap, a suppressed address, a stated\n * refusal, the ceiling — are answers the rep should read, so they render\n * inside the dialog with the draft intact rather than as a toast over a\n * closed one. Only a send the provider accepted closes it.\n *\n * Mounted on open by its button, so the identity request and the contact\n * read happen when somebody asks and not on every record page paint.\n */\nexport function CrmSendEmailDialog(props: CrmSendEmailDialogProps) {\n const { open, onClose, hostId, org, contactId, leadId, dealId, onSent } = props\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const { orgId } = useOrgDataScope({\n hostId: hostId ?? undefined,\n orgId: org?.$id ?? mount?.orgId,\n })\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const sendingApi = useSendingApi()\n /*\n * The site the message LEAVES FROM: the record's own, or at the\n * organization level — for a record no site captured — the site the\n * reader picked (AGL-2634). The identity, the suppression list and the\n * person's stated refusal are each that site's, which is why the org\n * level cannot do without one.\n */\n const sendHostId = hostId ?? mount?.createHostId ?? null\n const crmApi = useCrmApi(sendHostId)\n const sendSite = useSiteRouteContext(sendHostId)\n // The capture address (AGL-2657), for the reply this send will get: the\n // console logs what IT sends, so the address is shown to be forwarded\n // to or copied from a mailbox, never put on this message.\n const capture = useCrmInboundAddress(sendHostId, { enabled: open })\n // Where the site's sending identities are kept — the page that fixes a\n // missing one, at the address the plugin that keeps them publishes as\n // `sendingIdentity` (AGL-3080). `null` on a surface that cannot name the\n // site, or where nothing publishes the kind, so the refusal prints the\n // section's name instead of a link to nowhere.\n const sendingPath = sendSite ? pluginRecordListHref('sendingIdentity', sendSite) : null\n\n const [identity, setIdentity] = useState<IdentityState>({ status: 'loading' })\n const [recipient, setRecipient] = useState<string | null>(\n normalizeContactEmail(props.email) ?? null,\n )\n const [subject, setSubject] = useState('')\n const [body, setBody] = useState('')\n const [sending, setSending] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n /*\n * TEMPLATES, SNIPPETS AND MERGE FIELDS (AGL-2658). The listener is the\n * dialog's own scope — the mounted site's tokens, or none at the org\n * level — and what a saved template is stamped with follows the same\n * rule. The message field's element is held so a snippet lands at the\n * caret rather than at the end.\n */\n const { confirm } = useConfirmationContext()\n const templateScope = useCrmEmailTemplateScope({ hostId, org })\n const { templates } = useCrmEmailTemplates({\n scope: templateScope.scope,\n visibleTo: templateScope.visibleTo,\n uid: user?.uid,\n })\n const pickable = useMemo(() => templates.filter((row) => row.kind === 'template'), [templates])\n const snippets = useMemo(() => templates.filter((row) => row.kind === 'snippet'), [templates])\n const [appliedTemplate, setAppliedTemplate] = useState<CrmEmailTemplateRow | null>(null)\n const [saveOpen, setSaveOpen] = useState(false)\n const [records, setRecords] = useState<MergeRecords>(NO_MERGE_RECORDS)\n const messageRef = useRef<HTMLTextAreaElement | null>(null)\n const draftHasFields = hasCrmMergeFields(`${subject}\\n${body}`)\n // The groups the Insert menu offers: what the record can answer.\n const fieldGroups = useMemo<CrmMergeFieldGroup[]>(\n () => [\n ...(contactId || dealId ? (['Contact'] as const) : []),\n ...(leadId && !contactId ? (['Lead'] as const) : []),\n ...(dealId ? (['Deal'] as const) : []),\n 'You',\n 'Site',\n ],\n [contactId, dealId, leadId],\n )\n\n /*\n * The identity, asked for on open. The route is the send path's own\n * resolver, so what it prints here is what the message will leave as; a\n * dialog that derived \"verified\" from the domain list would be a second\n * opinion that drifts from the send.\n */\n useEffect(() => {\n if (!open) return\n let cancelled = false\n setError(null)\n // The draft survives a change of sending site — the site is not the\n // letter — and is cleared only when the dialog opens.\n if (!sendHostId) {\n setIdentity({ status: 'site' })\n return undefined\n }\n setIdentity({ status: 'loading' })\n void (async () => {\n const { response, payload } = await sendingApi({\n path: 'sending-identity',\n method: 'GET',\n query: { hostId: sendHostId },\n })\n if (cancelled) return\n if (!response.ok) {\n setIdentity({\n status: 'error',\n message: String(payload['error'] ?? 'The sending identity could not be read.'),\n })\n return\n }\n const refusal = payload['refusal'] as { message?: string } | null\n if (refusal) {\n setIdentity({\n status: 'refused',\n message: String(refusal.message ?? 'This site has no verified sending identity.'),\n canManage: payload['canManage'] === true,\n })\n return\n }\n const senders = Array.isArray(payload['senders'])\n ? (payload['senders'] as { isDefault?: boolean; from?: string | null }[])\n : []\n const from =\n senders.find((sender) => sender.isDefault && sender.from)?.from ??\n senders.find((sender) => sender.from)?.from ??\n null\n setIdentity(\n from\n ? { status: 'ready', from }\n : {\n status: 'error',\n message: String(payload['identity'] ?? 'No sending address is in effect.'),\n },\n )\n })()\n return () => {\n cancelled = true\n }\n }, [open, sendHostId, sendingApi])\n useEffect(() => {\n if (!open) return\n setSubject('')\n setBody('')\n setAppliedTemplate(null)\n setSaveOpen(false)\n setRecords(NO_MERGE_RECORDS)\n }, [open])\n\n /*\n * The documents a preview reads, fetched the first time the draft names\n * a merge field and kept for the rest of the open: the contact, the\n * deal, the lead, and the sending site's name — off the org mount when\n * it knows the site, off the host document otherwise. The route reads\n * the same documents again for the send; these are what the rep sees.\n */\n useEffect(() => {\n if (!open || !draftHasFields || records.loaded) return\n let cancelled = false\n const read = async (path: string[] | null): Promise<Record<string, unknown> | null> => {\n if (!path) return null\n const snapshot = await getDoc(doc(firestore, path[0], ...path.slice(1)))\n return snapshot.exists() ? ((snapshot.data() ?? null) as Record<string, unknown> | null) : null\n }\n const mountedSite = sendHostId\n ? (mount?.hosts ?? []).find((host) => host.id === sendHostId)\n : undefined\n void Promise.all([\n read(contactId && orgId ? ['orgs', orgId, 'contacts', contactId] : null),\n read(dealId && orgId ? ['orgs', orgId, CRM_COLLECTIONS.deals, dealId] : null),\n // One org row (AGL-3275); the site still decides WHICH lead is offered,\n // but not where it lives.\n read(leadId && orgId ? ['orgs', orgId, 'leads', leadId] : null),\n mountedSite || !sendHostId ? null : read(['hosts', sendHostId]),\n ])\n .then(([contact, deal, lead, host]) => {\n if (cancelled) return\n // A deal's contact, when the page named only the deal.\n const dealContactId = String(deal?.['contactId'] ?? '').trim()\n const withContact =\n !contact && dealContactId && orgId\n ? read(['orgs', orgId, 'contacts', dealContactId])\n : Promise.resolve(contact)\n return withContact.then((resolvedContact) => {\n if (cancelled) return\n setRecords({\n contact: resolvedContact,\n deal,\n lead,\n site: mountedSite?.name ?? (host ? String(host['displayName'] ?? '') : null),\n loaded: true,\n })\n })\n })\n .catch((cause) => {\n console.error(cause)\n if (!cancelled) setRecords({ ...NO_MERGE_RECORDS, loaded: true })\n })\n return () => {\n cancelled = true\n }\n }, [\n open,\n draftHasFields,\n records.loaded,\n firestore,\n orgId,\n contactId,\n dealId,\n leadId,\n sendHostId,\n mount?.hosts,\n ])\n\n const mergeContext = useMemo<CrmMergeContext>(\n () => ({\n contact: records.contact,\n contactGroupId: sendHostId ? consentGroupForHost(org ?? null, sendHostId).groupId : null,\n deal: records.deal,\n lead: records.lead,\n sender: { name: user?.displayName ?? '', email: user?.email ?? '' },\n site: { name: records.site ?? '' },\n }),\n [records, sendHostId, org, user?.displayName, user?.email],\n )\n const preview = useMemo(\n () =>\n draftHasFields && records.loaded\n ? {\n subject: resolveCrmMergeFields(subject, mergeContext),\n body: resolveCrmMergeFields(body, mergeContext),\n }\n : null,\n [draftHasFields, records.loaded, subject, body, mergeContext],\n )\n const unresolved = useMemo(\n () =>\n preview\n ? [...new Set([...preview.subject.unresolved, ...preview.body.unresolved])]\n : [],\n [preview],\n )\n\n /** A template into both fields — asking first when a message is already written. */\n const applyTemplate = useCallback(\n async (template: CrmEmailTemplateRow | null) => {\n if (!template) {\n setAppliedTemplate(null)\n return\n }\n const written = body.trim().length > 0 && body !== appliedTemplate?.body\n if (written) {\n const accepted = await confirm({\n title: 'Replace the message?',\n description: `\"${template.name}\" replaces what you have written in the subject and the message.`,\n confirmationText: 'Replace',\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n if (template.subject) setSubject(template.subject)\n setBody(template.body)\n setAppliedTemplate(template)\n },\n [body, appliedTemplate, confirm],\n )\n\n /*\n * The `recordEmail` zone (AGL-2917), hosted through the shell's renderer: a\n * widget there proposes a subject and a message, which land in the fields\n * above like a template does, asking first when a message is already\n * written. Send is still the only write, and the zone reads the record the\n * message is written from: the deal over its contact, a lead on its own site,\n * and at the organization level a contact or a deal the org reads.\n */\n const EmailDraftSlot = useConsoleWidgetSlot()\n const proposeDraft = useCallback(\n async (draft: ConsoleRecordEmailDraft) => {\n const next = { subject: draft.subject.slice(0, CRM_EMAIL_SUBJECT_MAX), body: draft.body.slice(0, CRM_EMAIL_BODY_MAX) }\n if (body.trim() && body !== next.body) {\n const accepted = await confirm({\n title: 'Replace the message?',\n description: 'The draft replaces what you have written in the subject and the message.',\n confirmationText: 'Replace',\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setSubject(next.subject)\n setBody(next.body)\n setAppliedTemplate(null)\n },\n [body, confirm],\n )\n const draftRecord = dealId\n ? { kind: 'deal', id: dealId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : contactId\n ? { kind: 'contact', id: contactId }\n : null\n\n /** A snippet or a field where the caret is, the caret moved past it. */\n const insertAtCaret = useCallback(\n (text: string) => {\n const element = messageRef.current\n const start = element?.selectionStart ?? body.length\n const end = element?.selectionEnd ?? body.length\n const next = `${body.slice(0, start)}${text}${body.slice(end)}`.slice(0, CRM_EMAIL_BODY_MAX)\n setBody(next)\n const caret = Math.min(start + text.length, next.length)\n window.setTimeout(() => {\n element?.focus()\n element?.setSelectionRange(caret, caret)\n }, 0)\n },\n [body],\n )\n\n /**\n * The draft kept as a template, stamped as this surface's scope stamps\n * every CRM record — the site's tokens under a site, the org token from\n * the organization's hub — and, when personal, owned by the writer.\n */\n const saveAsTemplate = useCallback(\n async (draft: { name: string; visibility: 'shared' | 'personal' }) => {\n const scope = templateScope.scope\n const uid = user?.uid\n if (!scope || !uid || templateScope.createTokens.length === 0) {\n throw new Error('a template needs an organization and a signed-in writer')\n }\n const now = Date.now()\n await setDoc(\n doc(collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.emailTemplates), createResourceUid()),\n {\n name: draft.name,\n subject: subject.trim(),\n body: body.trim(),\n kind: 'template',\n visibility: draft.visibility,\n ...(draft.visibility === 'personal' ? { ownerUid: uid } : {}),\n createdByUid: uid,\n createdAtMs: now,\n updatedAtMs: now,\n hostId: templateScope.createHostId,\n visibleTo: [...templateScope.createTokens],\n createdAt: new Date(now),\n updatedAt: new Date(now),\n },\n )\n enqueueSnackbar(`Template \"${draft.name}\" saved`, { variant: 'success', persist: false })\n setSaveOpen(false)\n },\n [templateScope, user?.uid, firestore, subject, body, enqueueSnackbar],\n )\n\n /*\n * The address, when the caller had none: one read of the contact, on\n * open. The route reads it again for itself; this one is what the To\n * field prints, and a field that printed nothing until the send answered\n * would ask the rep to write to somebody unnamed.\n */\n useEffect(() => {\n if (!open) return\n const given = normalizeContactEmail(props.email)\n if (given) {\n setRecipient(given)\n return\n }\n if (!contactId || !orgId) return\n let cancelled = false\n void getDoc(doc(firestore, 'orgs', orgId, 'contacts', contactId))\n .then((snapshot) => {\n if (cancelled) return\n setRecipient(normalizeContactEmail(snapshot.get('email')) ?? null)\n })\n .catch((cause) => {\n console.error(cause)\n if (!cancelled) setRecipient(null)\n })\n return () => {\n cancelled = true\n }\n }, [open, props.email, contactId, orgId, firestore])\n\n /*\n * What the platform knows about the address before Send (AGL-3328): a\n * domain with no mail server, or a mail gateway that refused this site's\n * sending domain twice this month. Read off the server's cache — the\n * browser does no DNS — and said as a warning; the route decides.\n */\n const deliverability = useCrmEmailCheck(sendHostId, recipient, { enabled: open })\n\n const replyTo = normalizeContactEmail(user?.email) ?? ''\n const canSend =\n identity.status === 'ready' &&\n Boolean(recipient) &&\n subject.trim().length > 0 &&\n body.trim().length > 0 &&\n !sending\n\n const handleSend = useCallback(async () => {\n if (!canSend) return\n setSending(true)\n setError(null)\n try {\n const { response, payload } = await crmApi('email-send', {\n ...(contactId ? { contactId } : {}),\n ...(leadId ? { leadId } : {}),\n ...(dealId ? { dealId } : {}),\n subject: subject.trim(),\n body: body.trim(),\n })\n if (!response.ok) {\n setError(String(payload['error'] ?? 'The email could not be sent.'))\n return\n }\n enqueueSnackbar('Email sent', { variant: 'success', persist: false })\n onSent?.({ activityId: String(payload['activityId'] ?? '') })\n onClose()\n } catch (cause) {\n console.error(cause)\n setError('The email could not be sent.')\n } finally {\n setSending(false)\n }\n }, [canSend, crmApi, contactId, leadId, dealId, subject, body, enqueueSnackbar, onSent, onClose])\n\n const toLabel = props.name && recipient ? `${props.name} <${recipient}>` : (recipient ?? '')\n\n /*\n * The booking door (AGL-2660): a link to one of the sending site's\n * services, dropped where the caret is. The link carries the most\n * specific record this message is about — the deal over its contact, so\n * the booking lands on the deal's timeline as the email does.\n */\n const bookingRef = dealId\n ? { kind: 'deal' as const, id: dealId }\n : leadId\n ? { kind: 'lead' as const, id: leadId }\n : contactId\n ? { kind: 'contact' as const, id: contactId }\n : null\n const handleInsertLink = useCallback((link: string) => {\n const input = messageRef.current\n const current = input?.value ?? ''\n const start = input?.selectionStart ?? current.length\n const end = input?.selectionEnd ?? start\n const next = insertLinkAtCaret(current, link, start, end)\n setBody(next.text)\n // Once the new value has painted, typing continues after the link.\n window.setTimeout(() => {\n input?.focus()\n input?.setSelectionRange(next.caret, next.caret)\n }, 0)\n }, [])\n\n return (\n <Dialog open={open} onClose={sending ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{'Send email'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {!hostId ? (\n <CrmSitePicker\n hostId={hostId}\n label=\"Send from\"\n helperText=\"The site whose sending address the email leaves on.\"\n disabled={sending}\n />\n ) : null}\n {identity.status === 'site' ? (\n <Alert severity=\"info\" sx={{ mt: 1 }}>\n {'Pick the site the email leaves from.'}\n </Alert>\n ) : identity.status === 'refused' ? (\n <Alert severity=\"warning\" sx={{ mt: 1 }}>\n <Typography variant=\"body2\">{identity.message}</Typography>\n {sendingPath ? (\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n <AppLink href={sendingPath}>\n {identity.canManage ? 'Set up sending' : 'See sending'}\n </AppLink>\n </Typography>\n ) : (\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n {'An admin sets this up under Emails › Sending.'}\n </Typography>\n )}\n </Alert>\n ) : identity.status === 'error' ? (\n <Alert severity=\"error\" sx={{ mt: 1 }}>\n {identity.message}\n </Alert>\n ) : null}\n {open && !recipient && (identity.status === 'ready' || identity.status === 'loading') ? (\n <Alert severity=\"warning\" sx={{ mt: 1 }}>\n {'This record has no email address.'}\n </Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"To\"\n value={toLabel}\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n sx={{ mt: identity.status === 'ready' || identity.status === 'loading' ? 1 : 0 }}\n />\n <CrmEmailCheckAlert check={deliverability} />\n <TextField\n size=\"small\"\n label=\"From\"\n value={\n identity.status === 'ready'\n ? identity.from\n : identity.status === 'loading'\n ? 'Resolving the site’s sending address…'\n : identity.status === 'site'\n ? 'Pick a site to send from'\n : ''\n }\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n />\n <TextField\n size=\"small\"\n label=\"Reply-to\"\n value={replyTo}\n helperText=\"Replies come to you, not to the site's mailbox.\"\n slotProps={{ input: { readOnly: true }, inputLabel: { shrink: true } }}\n />\n {capture.status === 'ready' ? (\n <TextField\n size=\"small\"\n label=\"Log replies\"\n value={capture.address}\n helperText={CAPTURE_ADDRESS_HELPER}\n slotProps={{\n input: {\n readOnly: true,\n endAdornment: (\n <InputAdornment position=\"end\">\n <CopyCaptureAddressButton address={capture.address} />\n </InputAdornment>\n ),\n },\n inputLabel: { shrink: true },\n }}\n />\n ) : null}\n {pickable.length ? (\n <CrmTemplatePicker\n templates={pickable}\n value={appliedTemplate}\n onChange={(template) => void applyTemplate(template)}\n disabled={sending}\n />\n ) : null}\n <TextField\n size=\"small\"\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value)}\n autoFocus\n slotProps={{ htmlInput: { maxLength: CRM_EMAIL_SUBJECT_MAX } }}\n />\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <CrmInsertMenu\n snippets={snippets}\n groups={fieldGroups}\n disabled={sending}\n onInsert={insertAtCaret}\n />\n <Button\n size=\"small\"\n disabled={sending || !templateScope.scope || (!subject.trim() && !body.trim())}\n onClick={() => setSaveOpen(true)}\n >\n {'Save as template…'}\n </Button>\n </Stack>\n <TextField\n size=\"small\"\n label=\"Message\"\n value={body}\n onChange={(event) => setBody(event.target.value)}\n multiline\n minRows={6}\n inputRef={messageRef}\n helperText={\n unresolved.length\n ? crmMergeUnresolvedMessage(unresolved)\n : draftHasFields\n ? 'Plain text. Merge fields are filled in from this record when the email is sent.'\n : 'Plain text. A blank line starts a new paragraph.'\n }\n slotProps={{\n htmlInput: { maxLength: CRM_EMAIL_BODY_MAX },\n formHelperText: { sx: unresolved.length ? { color: 'warning.main' } : undefined },\n }}\n />\n {EmailDraftSlot && draftRecord ? (\n <EmailDraftSlot\n slot=\"recordEmail\"\n // A lead is read on its own site; a contact or a deal at the level\n // the composer was opened at.\n hostId={draftRecord.kind === 'lead' || !mount ? hostId : null}\n orgId={orgId ?? undefined}\n record={{ ...draftRecord, name: String(props.name ?? '') }}\n subject={subject}\n body={body}\n proposeDraft={(draft: ConsoleRecordEmailDraft) => void proposeDraft(draft)}\n />\n ) : null}\n {preview ? (\n <Stack spacing={0.5} data-testid=\"crm-email-preview\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Preview — as it will be sent'}\n </Typography>\n <Typography variant=\"subtitle2\">{preview.subject.text}</Typography>\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {preview.body.text}\n </Typography>\n </Stack>\n ) : null}\n {bookingRef ? (\n <CrmRecordBookingZone\n variant=\"chip\"\n hostId={sendHostId}\n org={org}\n kind={bookingRef.kind}\n recordId={bookingRef.id}\n onInsert={handleInsertLink}\n />\n ) : null}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </DialogContent>\n <CrmSaveAsTemplateDialog\n open={saveOpen}\n onClose={() => setSaveOpen(false)}\n onSave={saveAsTemplate}\n />\n <DialogActions>\n <Button onClick={onClose} disabled={sending}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" color=\"primary\" disabled={!canSend} onClick={handleSend}>\n {sending ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nCrmSendEmailDialog.displayName = 'CrmSendEmailDialog'\n\nexport default CrmSendEmailDialog\n"],"names":["CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","normalizeContactEmail","consentGroupForHost","CRM_COLLECTIONS","createResourceUid","crmMergeUnresolvedMessage","hasCrmMergeFields","resolveCrmMergeFields","useConsoleWidgetSlot","pluginRecordListHref","useSendingApi","AppLink","useConfirmationContext","useSnackbar","useFirestore","useOrgDataScope","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","InputAdornment","Stack","TextField","Typography","collection","doc","getDoc","setDoc","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","CrmRecordBookingZone","insertLinkAtCaret","CAPTURE_ADDRESS_HELPER","CopyCaptureAddressButton","CrmSitePicker","CrmEmailCheckAlert","useCrmEmailCheck","useCrmApi","useCrmInboundAddress","useSiteRouteContext","useCrmEmailTemplates","useCrmEmailTemplateScope","CrmInsertMenu","CrmSaveAsTemplateDialog","CrmTemplatePicker","NO_MERGE_RECORDS","contact","deal","lead","site","loaded","CrmSendEmailDialog","props","hostId","open","onClose","org","contactId","leadId","dealId","onSent","firestore","mount","orgId","undefined","$id","data","user","enqueueSnackbar","sendingApi","sendHostId","createHostId","crmApi","sendSite","capture","enabled","sendingPath","identity","setIdentity","status","recipient","setRecipient","email","subject","setSubject","body","setBody","sending","setSending","error","setError","confirm","templateScope","templates","scope","visibleTo","uid","pickable","filter","row","kind","snippets","appliedTemplate","setAppliedTemplate","saveOpen","setSaveOpen","records","setRecords","messageRef","draftHasFields","fieldGroups","cancelled","senders","payload","response","path","method","query","ok","message","String","refusal","canManage","Array","isArray","from","find","sender","isDefault","read","snapshot","slice","exists","mountedSite","hosts","host","id","Promise","all","deals","then","dealContactId","trim","withContact","resolve","resolvedContact","name","catch","cause","console","mergeContext","contactGroupId","groupId","displayName","preview","unresolved","Set","applyTemplate","template","written","length","accepted","title","description","confirmationText","EmailDraftSlot","proposeDraft","draft","next","draftRecord","insertAtCaret","text","element","current","start","selectionStart","end","selectionEnd","caret","Math","min","window","setTimeout","focus","setSelectionRange","saveAsTemplate","createTokens","Error","now","Date","emailTemplates","visibility","ownerUid","createdByUid","createdAtMs","updatedAtMs","createdAt","updatedAt","variant","persist","given","get","deliverability","replyTo","canSend","Boolean","handleSend","activityId","toLabel","bookingRef","handleInsertLink","link","input","value","maxWidth","fullWidth","sx","display","flexDirection","gap","label","helperText","disabled","severity","mt","href","size","slotProps","readOnly","inputLabel","shrink","check","address","endAdornment","position","onChange","event","target","autoFocus","htmlInput","maxLength","direction","spacing","justifyContent","groups","onInsert","onClick","multiline","minRows","inputRef","formHelperText","color","slot","record","data-testid","whiteSpace","recordId","onSave"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,kBAAkB,EAClBC,qBAAqB,EACrBC,qBAAqB,EACrBC,mBAAmB,EACnBC,eAAe,EACfC,iBAAiB,EAIjBC,yBAAyB,EACzBC,iBAAiB,EACjBC,qBAAqB,QAChB,eAAc;AACrB,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SAASC,oBAAoB,QAAQ,mDAAkD;AAEvF,SAASC,aAAa,QAAQ,gEAA+D;AAC7F,SAASC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AACtE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,YAAY,EACZC,eAAe,EACfC,OAAO,QACF,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,cAAc,EACdC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,UAAU,EAAEC,GAAG,EAAEC,MAAM,EAAEC,MAAM,QAAQ,qBAAoB;AACpE,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACzE,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,oBAAoB,QAAQ,wBAAoB;AACzD,SAASC,iBAAiB,QAAQ,4BAAwB;AAC1D,SACEC,sBAAsB,EACtBC,wBAAwB,QACnB,mCAA+B;AACtC,SAASC,aAAa,QAAQ,uBAAmB;AACjD,SAASC,kBAAkB,EAAEC,gBAAgB,QAAQ,uBAAmB;AACxE,SAASC,SAAS,QAAQ,mBAAe;AACzC,SAASC,oBAAoB,QAAQ,+BAA2B;AAChE,SAASC,mBAAmB,QAAQ,8BAA0B;AAC9D,SACEC,oBAAoB,EACpBC,wBAAwB,QACnB,sCAAkC;AACzC,SACEC,aAAa,EACbC,uBAAuB,EACvBC,iBAAiB,QACZ,gCAA4B;AAiBnC,MAAMC,mBAAiC;IACrCC,SAAS;IACTC,MAAM;IACNC,MAAM;IACNC,MAAM;IACNC,QAAQ;AACV;AAyCA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;cAkB5CC,OAgBjB3D,wBAmWcA,yBAmNiC0D;IAvlBjD,MAAM,EAAEE,IAAI,EAAEC,OAAO,EAAEF,MAAM,EAAEG,GAAG,EAAEC,SAAS,EAAEC,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGR;IAC1E,MAAMS,YAAYtD;IAClB,MAAMuD,QAAQjC;IACd,MAAM,EAAEkC,KAAK,EAAE,GAAGvD,gBAAgB;QAChC6C,MAAM,EAAEA,iBAAAA,SAAUW;QAClBD,KAAK,UAAEP,uBAAAA,IAAKS,GAAG,mBAAIH,yBAAAA,MAAOC,KAAK;IACjC;IACA,MAAM,EAAEG,MAAMC,IAAI,EAAE,GAAG1D;IACvB,MAAM,EAAE2D,eAAe,EAAE,GAAG9D;IAC5B,MAAM+D,aAAalE;IACnB;;;;;;GAMC,GACD,MAAMmE,cAAajB,QAAAA,iBAAAA,SAAUS,yBAAAA,MAAOS,YAAY,YAA7BlB,QAAiC;IACpD,MAAMmB,SAASnC,UAAUiC;IACzB,MAAMG,WAAWlC,oBAAoB+B;IACrC,wEAAwE;IACxE,sEAAsE;IACtE,0DAA0D;IAC1D,MAAMI,UAAUpC,qBAAqBgC,YAAY;QAAEK,SAASrB;IAAK;IACjE,uEAAuE;IACvE,sEAAsE;IACtE,yEAAyE;IACzE,uEAAuE;IACvE,+CAA+C;IAC/C,MAAMsB,cAAcH,WAAWvE,qBAAqB,mBAAmBuE,YAAY;IAEnF,MAAM,CAACI,UAAUC,YAAY,GAAGlD,SAAwB;QAAEmD,QAAQ;IAAU;IAC5E,MAAM,CAACC,WAAWC,aAAa,GAAGrD,UAChClC,yBAAAA,sBAAsB0D,MAAM8B,KAAK,aAAjCxF,yBAAsC;IAExC,MAAM,CAACyF,SAASC,WAAW,GAAGxD,SAAS;IACvC,MAAM,CAACyD,MAAMC,QAAQ,GAAG1D,SAAS;IACjC,MAAM,CAAC2D,SAASC,WAAW,GAAG5D,SAAS;IACvC,MAAM,CAAC6D,OAAOC,SAAS,GAAG9D,SAAwB;IAElD;;;;;;GAMC,GACD,MAAM,EAAE+D,OAAO,EAAE,GAAGtF;IACpB,MAAMuF,gBAAgBnD,yBAAyB;QAAEY;QAAQG;IAAI;IAC7D,MAAM,EAAEqC,SAAS,EAAE,GAAGrD,qBAAqB;QACzCsD,OAAOF,cAAcE,KAAK;QAC1BC,WAAWH,cAAcG,SAAS;QAClCC,GAAG,EAAE7B,wBAAAA,KAAM6B,GAAG;IAChB;IACA,MAAMC,WAAWvE,QAAQ,IAAMmE,UAAUK,MAAM,CAAC,CAACC,MAAQA,IAAIC,IAAI,KAAK,aAAa;QAACP;KAAU;IAC9F,MAAMQ,WAAW3E,QAAQ,IAAMmE,UAAUK,MAAM,CAAC,CAACC,MAAQA,IAAIC,IAAI,KAAK,YAAY;QAACP;KAAU;IAC7F,MAAM,CAACS,iBAAiBC,mBAAmB,GAAG3E,SAAqC;IACnF,MAAM,CAAC4E,UAAUC,YAAY,GAAG7E,SAAS;IACzC,MAAM,CAAC8E,SAASC,WAAW,GAAG/E,SAAuBiB;IACrD,MAAM+D,aAAajF,OAAmC;IACtD,MAAMkF,iBAAiB9G,kBAAkB,GAAGoF,QAAQ,EAAE,EAAEE,MAAM;IAC9D,iEAAiE;IACjE,MAAMyB,cAAcpF,QAClB,IAAM;eACA+B,aAAaE,SAAU;gBAAC;aAAU,GAAa,EAAE;eACjDD,UAAU,CAACD,YAAa;gBAAC;aAAO,GAAa,EAAE;eAC/CE,SAAU;gBAAC;aAAO,GAAa,EAAE;YACrC;YACA;SACD,EACD;QAACF;QAAWE;QAAQD;KAAO;IAG7B;;;;;GAKC,GACDjC,UAAU;QACR,IAAI,CAAC6B,MAAM;QACX,IAAIyD,YAAY;QAChBrB,SAAS;QACT,oEAAoE;QACpE,sDAAsD;QACtD,IAAI,CAACpB,YAAY;YACfQ,YAAY;gBAAEC,QAAQ;YAAO;YAC7B,OAAOf;QACT;QACAc,YAAY;YAAEC,QAAQ;QAAU;QAChC,KAAK,AAAC,CAAA;gBA2BFiC,aAQsBC;gBARtBD,eACAA;YA3BF,MAAM,EAAEE,QAAQ,EAAED,OAAO,EAAE,GAAG,MAAM5C,WAAW;gBAC7C8C,MAAM;gBACNC,QAAQ;gBACRC,OAAO;oBAAEhE,QAAQiB;gBAAW;YAC9B;YACA,IAAIyC,WAAW;YACf,IAAI,CAACG,SAASI,EAAE,EAAE;oBAGEL;gBAFlBnC,YAAY;oBACVC,QAAQ;oBACRwC,SAASC,QAAOP,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB;gBACtC;gBACA;YACF;YACA,MAAMQ,UAAUR,OAAO,CAAC,UAAU;YAClC,IAAIQ,SAAS;oBAGOA;gBAFlB3C,YAAY;oBACVC,QAAQ;oBACRwC,SAASC,QAAOC,mBAAAA,QAAQF,OAAO,YAAfE,mBAAmB;oBACnCC,WAAWT,OAAO,CAAC,YAAY,KAAK;gBACtC;gBACA;YACF;YACA,MAAMD,UAAUW,MAAMC,OAAO,CAACX,OAAO,CAAC,UAAU,IAC3CA,OAAO,CAAC,UAAU,GACnB,EAAE;YACN,MAAMY,QACJb,iBAAAA,gBAAAA,QAAQc,IAAI,CAAC,CAACC,SAAWA,OAAOC,SAAS,IAAID,OAAOF,IAAI,sBAAxDb,cAA2Da,IAAI,qBAC/Db,iBAAAA,QAAQc,IAAI,CAAC,CAACC,SAAWA,OAAOF,IAAI,sBAApCb,eAAuCa,IAAI,YAD3Cb,OAEA;YACFlC,YACE+C,OACI;gBAAE9C,QAAQ;gBAAS8C;YAAK,IACxB;gBACE9C,QAAQ;gBACRwC,SAASC,QAAOP,oBAAAA,OAAO,CAAC,WAAW,YAAnBA,oBAAuB;YACzC;QAER,CAAA;QACA,OAAO;YACLF,YAAY;QACd;IACF,GAAG;QAACzD;QAAMgB;QAAYD;KAAW;IACjC5C,UAAU;QACR,IAAI,CAAC6B,MAAM;QACX8B,WAAW;QACXE,QAAQ;QACRiB,mBAAmB;QACnBE,YAAY;QACZE,WAAW9D;IACb,GAAG;QAACS;KAAK;IAET;;;;;;GAMC,GACD7B,UAAU;;QACR,IAAI,CAAC6B,QAAQ,CAACuD,kBAAkBH,QAAQxD,MAAM,EAAE;QAChD,IAAI6D,YAAY;QAChB,MAAMkB,OAAO,OAAOd;gBAGWe;YAF7B,IAAI,CAACf,MAAM,OAAO;YAClB,MAAMe,WAAW,MAAM5G,OAAOD,IAAIwC,WAAWsD,IAAI,CAAC,EAAE,KAAKA,KAAKgB,KAAK,CAAC;YACpE,OAAOD,SAASE,MAAM,MAAOF,iBAAAA,SAAShE,IAAI,cAAbgE,iBAAmB,OAA2C;QAC7F;QACA,MAAMG,cAAc/D,aAChB,SAACR,yBAAAA,MAAOwE,KAAK,mBAAI,EAAE,EAAER,IAAI,CAAC,CAACS,OAASA,KAAKC,EAAE,KAAKlE,cAChDN;QACJ,KAAKyE,QAAQC,GAAG,CAAC;YACfT,KAAKxE,aAAaM,QAAQ;gBAAC;gBAAQA;gBAAO;gBAAYN;aAAU,GAAG;YACnEwE,KAAKtE,UAAUI,QAAQ;gBAAC;gBAAQA;gBAAOnE,gBAAgB+I,KAAK;gBAAEhF;aAAO,GAAG;YACxE,wEAAwE;YACxE,0BAA0B;YAC1BsE,KAAKvE,UAAUK,QAAQ;gBAAC;gBAAQA;gBAAO;gBAASL;aAAO,GAAG;YAC1D2E,eAAe,CAAC/D,aAAa,OAAO2D,KAAK;gBAAC;gBAAS3D;aAAW;SAC/D,EACEsE,IAAI,CAAC,CAAC,CAAC9F,SAASC,MAAMC,MAAMuF,KAAK;;YAChC,IAAIxB,WAAW;YACf,uDAAuD;YACvD,MAAM8B,gBAAgBrB,eAAOzE,wBAAAA,IAAM,CAAC,YAAY,mBAAI,IAAI+F,IAAI;YAC5D,MAAMC,cACJ,CAACjG,WAAW+F,iBAAiB9E,QACzBkE,KAAK;gBAAC;gBAAQlE;gBAAO;gBAAY8E;aAAc,IAC/CJ,QAAQO,OAAO,CAAClG;YACtB,OAAOiG,YAAYH,IAAI,CAAC,CAACK;0BAMqBV;gBAL5C,IAAIxB,WAAW;gBACfJ,WAAW;oBACT7D,SAASmG;oBACTlG;oBACAC;oBACAC,IAAI,UAAEoF,+BAAAA,YAAaa,IAAI,mBAAKX,OAAOf,QAAOe,oBAAAA,IAAI,CAAC,cAAc,YAAnBA,oBAAuB,MAAM;oBACvErF,QAAQ;gBACV;YACF;QACF,GACCiG,KAAK,CAAC,CAACC;YACNC,QAAQ5D,KAAK,CAAC2D;YACd,IAAI,CAACrC,WAAWJ,WAAW,aAAK9D;gBAAkBK,QAAQ;;QAC5D;QACF,OAAO;YACL6D,YAAY;QACd;IACF,GAAG;QACDzD;QACAuD;QACAH,QAAQxD,MAAM;QACdW;QACAE;QACAN;QACAE;QACAD;QACAY;QACAR,yBAAAA,MAAOwE,KAAK;KACb;IAED,MAAMgB,eAAe5H,QACnB;yBAMgBgF;eANT;YACL5D,SAAS4D,QAAQ5D,OAAO;YACxByG,gBAAgBjF,aAAa3E,oBAAoB6D,cAAAA,MAAO,MAAMc,YAAYkF,OAAO,GAAG;YACpFzG,MAAM2D,QAAQ3D,IAAI;YAClBC,MAAM0D,QAAQ1D,IAAI;YAClB+E,QAAQ;gBAAEmB,IAAI,UAAE/E,wBAAAA,KAAMsF,WAAW,mBAAI;gBAAIvE,KAAK,WAAEf,wBAAAA,KAAMe,KAAK,oBAAI;YAAG;YAClEjC,MAAM;gBAAEiG,IAAI,GAAExC,gBAAAA,QAAQzD,IAAI,YAAZyD,gBAAgB;YAAG;QACnC;OACA;QAACA;QAASpC;QAAYd;QAAKW,wBAAAA,KAAMsF,WAAW;QAAEtF,wBAAAA,KAAMe,KAAK;KAAC;IAE5D,MAAMwE,UAAUhI,QACd,IACEmF,kBAAkBH,QAAQxD,MAAM,GAC5B;YACEiC,SAASnF,sBAAsBmF,SAASmE;YACxCjE,MAAMrF,sBAAsBqF,MAAMiE;QACpC,IACA,MACN;QAACzC;QAAgBH,QAAQxD,MAAM;QAAEiC;QAASE;QAAMiE;KAAa;IAE/D,MAAMK,aAAajI,QACjB,IACEgI,UACI;eAAI,IAAIE,IAAI;mBAAIF,QAAQvE,OAAO,CAACwE,UAAU;mBAAKD,QAAQrE,IAAI,CAACsE,UAAU;aAAC;SAAE,GACzE,EAAE,EACR;QAACD;KAAQ;IAGX,kFAAkF,GAClF,MAAMG,gBAAgBrI,YACpB,OAAOsI;QACL,IAAI,CAACA,UAAU;YACbvD,mBAAmB;YACnB;QACF;QACA,MAAMwD,UAAU1E,KAAKyD,IAAI,GAAGkB,MAAM,GAAG,KAAK3E,UAASiB,mCAAAA,gBAAiBjB,IAAI;QACxE,IAAI0E,SAAS;YACX,MAAME,WAAW,MAAMtE,QAAQ;gBAC7BuE,OAAO;gBACPC,aAAa,CAAC,CAAC,EAAEL,SAASZ,IAAI,CAAC,gEAAgE,CAAC;gBAChGkB,kBAAkB;YACpB,GACGxB,IAAI,CAAC,IAAM,MACXO,KAAK,CAAC,IAAM;YACf,IAAI,CAACc,UAAU;QACjB;QACA,IAAIH,SAAS3E,OAAO,EAAEC,WAAW0E,SAAS3E,OAAO;QACjDG,QAAQwE,SAASzE,IAAI;QACrBkB,mBAAmBuD;IACrB,GACA;QAACzE;QAAMiB;QAAiBX;KAAQ;IAGlC;;;;;;;GAOC,GACD,MAAM0E,iBAAiBpK;IACvB,MAAMqK,eAAe9I,YACnB,OAAO+I;QACL,MAAMC,OAAO;YAAErF,SAASoF,MAAMpF,OAAO,CAACgD,KAAK,CAAC,GAAG1I;YAAwB4F,MAAMkF,MAAMlF,IAAI,CAAC8C,KAAK,CAAC,GAAG3I;QAAoB;QACrH,IAAI6F,KAAKyD,IAAI,MAAMzD,SAASmF,KAAKnF,IAAI,EAAE;YACrC,MAAM4E,WAAW,MAAMtE,QAAQ;gBAC7BuE,OAAO;gBACPC,aAAa;gBACbC,kBAAkB;YACpB,GACGxB,IAAI,CAAC,IAAM,MACXO,KAAK,CAAC,IAAM;YACf,IAAI,CAACc,UAAU;QACjB;QACA7E,WAAWoF,KAAKrF,OAAO;QACvBG,QAAQkF,KAAKnF,IAAI;QACjBkB,mBAAmB;IACrB,GACA;QAAClB;QAAMM;KAAQ;IAEjB,MAAM8E,cAAc9G,SAChB;QAAEyC,MAAM;QAAQoC,IAAI7E;IAAO,IAC3BD,SACE;QAAE0C,MAAM;QAAQoC,IAAI9E;IAAO,IAC3BD,YACE;QAAE2C,MAAM;QAAWoC,IAAI/E;IAAU,IACjC;IAER,sEAAsE,GACtE,MAAMiH,gBAAgBlJ,YACpB,CAACmJ;;QACC,MAAMC,UAAUhE,WAAWiE,OAAO;QAClC,MAAMC,gBAAQF,2BAAAA,QAASG,cAAc,mBAAI1F,KAAK2E,MAAM;QACpD,MAAMgB,eAAMJ,2BAAAA,QAASK,YAAY,oBAAI5F,KAAK2E,MAAM;QAChD,MAAMQ,OAAO,GAAGnF,KAAK8C,KAAK,CAAC,GAAG2C,SAASH,OAAOtF,KAAK8C,KAAK,CAAC6C,MAAM,CAAC7C,KAAK,CAAC,GAAG3I;QACzE8F,QAAQkF;QACR,MAAMU,QAAQC,KAAKC,GAAG,CAACN,QAAQH,KAAKX,MAAM,EAAEQ,KAAKR,MAAM;QACvDqB,OAAOC,UAAU,CAAC;YAChBV,2BAAAA,QAASW,KAAK;YACdX,2BAAAA,QAASY,iBAAiB,CAACN,OAAOA;QACpC,GAAG;IACL,GACA;QAAC7F;KAAK;IAGR;;;;GAIC,GACD,MAAMoG,iBAAiBjK,YACrB,OAAO+I;QACL,MAAMzE,QAAQF,cAAcE,KAAK;QACjC,MAAME,MAAM7B,wBAAAA,KAAM6B,GAAG;QACrB,IAAI,CAACF,SAAS,CAACE,OAAOJ,cAAc8F,YAAY,CAAC1B,MAAM,KAAK,GAAG;YAC7D,MAAM,IAAI2B,MAAM;QAClB;QACA,MAAMC,MAAMC,KAAKD,GAAG;QACpB,MAAMrK,OACJF,IAAID,WAAWyC,WAAWiC,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAElG,gBAAgBkM,cAAc,GAAGjM,sBAC/E;YACEqJ,MAAMqB,MAAMrB,IAAI;YAChB/D,SAASA,QAAQ2D,IAAI;YACrBzD,MAAMA,KAAKyD,IAAI;YACf1C,MAAM;YACN2F,YAAYxB,MAAMwB,UAAU;WACxBxB,MAAMwB,UAAU,KAAK,aAAa;YAAEC,UAAUhG;QAAI,IAAI,CAAC;YAC3DiG,cAAcjG;YACdkG,aAAaN;YACbO,aAAaP;YACbvI,QAAQuC,cAAcrB,YAAY;YAClCwB,WAAW;mBAAIH,cAAc8F,YAAY;aAAC;YAC1CU,WAAW,IAAIP,KAAKD;YACpBS,WAAW,IAAIR,KAAKD;;QAGxBxH,gBAAgB,CAAC,UAAU,EAAEmG,MAAMrB,IAAI,CAAC,OAAO,CAAC,EAAE;YAAEoD,SAAS;YAAWC,SAAS;QAAM;QACvF9F,YAAY;IACd,GACA;QAACb;QAAezB,wBAAAA,KAAM6B,GAAG;QAAEnC;QAAWsB;QAASE;QAAMjB;KAAgB;IAGvE;;;;;GAKC,GACD3C,UAAU;QACR,IAAI,CAAC6B,MAAM;QACX,MAAMkJ,QAAQ9M,sBAAsB0D,MAAM8B,KAAK;QAC/C,IAAIsH,OAAO;YACTvH,aAAauH;YACb;QACF;QACA,IAAI,CAAC/I,aAAa,CAACM,OAAO;QAC1B,IAAIgD,YAAY;QAChB,KAAKzF,OAAOD,IAAIwC,WAAW,QAAQE,OAAO,YAAYN,YACnDmF,IAAI,CAAC,CAACV;gBAEQxI;YADb,IAAIqH,WAAW;YACf9B,cAAavF,yBAAAA,sBAAsBwI,SAASuE,GAAG,CAAC,qBAAnC/M,yBAAgD;QAC/D,GACCyJ,KAAK,CAAC,CAACC;YACNC,QAAQ5D,KAAK,CAAC2D;YACd,IAAI,CAACrC,WAAW9B,aAAa;QAC/B;QACF,OAAO;YACL8B,YAAY;QACd;IACF,GAAG;QAACzD;QAAMF,MAAM8B,KAAK;QAAEzB;QAAWM;QAAOF;KAAU;IAEnD;;;;;GAKC,GACD,MAAM6I,iBAAiBtK,iBAAiBkC,YAAYU,WAAW;QAAEL,SAASrB;IAAK;IAE/E,MAAMqJ,WAAUjN,0BAAAA,sBAAsByE,wBAAAA,KAAMe,KAAK,aAAjCxF,0BAAsC;IACtD,MAAMkN,UACJ/H,SAASE,MAAM,KAAK,WACpB8H,QAAQ7H,cACRG,QAAQ2D,IAAI,GAAGkB,MAAM,GAAG,KACxB3E,KAAKyD,IAAI,GAAGkB,MAAM,GAAG,KACrB,CAACzE;IAEH,MAAMuH,aAAatL,YAAY;QAC7B,IAAI,CAACoL,SAAS;QACdpH,WAAW;QACXE,SAAS;QACT,IAAI;gBAa4BuB;YAZ9B,MAAM,EAAEC,QAAQ,EAAED,OAAO,EAAE,GAAG,MAAMzC,OAAO,cAAc,aACnDf,YAAY;gBAAEA;YAAU,IAAI,CAAC,GAC7BC,SAAS;gBAAEA;YAAO,IAAI,CAAC,GACvBC,SAAS;gBAAEA;YAAO,IAAI,CAAC;gBAC3BwB,SAASA,QAAQ2D,IAAI;gBACrBzD,MAAMA,KAAKyD,IAAI;;YAEjB,IAAI,CAAC5B,SAASI,EAAE,EAAE;oBACAL;gBAAhBvB,SAAS8B,QAAOP,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB;gBACpC;YACF;YACA7C,gBAAgB,cAAc;gBAAEkI,SAAS;gBAAWC,SAAS;YAAM;YACnE3I,0BAAAA,OAAS;gBAAEmJ,YAAYvF,QAAOP,sBAAAA,OAAO,CAAC,aAAa,YAArBA,sBAAyB;YAAI;YAC3D1D;QACF,EAAE,OAAO6F,OAAO;YACdC,QAAQ5D,KAAK,CAAC2D;YACd1D,SAAS;QACX,SAAU;YACRF,WAAW;QACb;IACF,GAAG;QAACoH;QAASpI;QAAQf;QAAWC;QAAQC;QAAQwB;QAASE;QAAMjB;QAAiBR;QAAQL;KAAQ;IAEhG,MAAMyJ,UAAU5J,MAAM8F,IAAI,IAAIlE,YAAY,GAAG5B,MAAM8F,IAAI,CAAC,EAAE,EAAElE,UAAU,CAAC,CAAC,GAAIA,oBAAAA,YAAa;IAEzF;;;;;GAKC,GACD,MAAMiI,aAAatJ,SACf;QAAEyC,MAAM;QAAiBoC,IAAI7E;IAAO,IACpCD,SACE;QAAE0C,MAAM;QAAiBoC,IAAI9E;IAAO,IACpCD,YACE;QAAE2C,MAAM;QAAoBoC,IAAI/E;IAAU,IAC1C;IACR,MAAMyJ,mBAAmB1L,YAAY,CAAC2L;;QACpC,MAAMC,QAAQxG,WAAWiE,OAAO;QAChC,MAAMA,kBAAUuC,yBAAAA,MAAOC,KAAK,mBAAI;QAChC,MAAMvC,iBAAQsC,yBAAAA,MAAOrC,cAAc,oBAAIF,QAAQb,MAAM;QACrD,MAAMgB,eAAMoC,yBAAAA,MAAOnC,YAAY,oBAAIH;QACnC,MAAMN,OAAOzI,kBAAkB8I,SAASsC,MAAMrC,OAAOE;QACrD1F,QAAQkF,KAAKG,IAAI;QACjB,mEAAmE;QACnEU,OAAOC,UAAU,CAAC;YAChB8B,yBAAAA,MAAO7B,KAAK;YACZ6B,yBAAAA,MAAO5B,iBAAiB,CAAChB,KAAKU,KAAK,EAAEV,KAAKU,KAAK;QACjD,GAAG;IACL,GAAG,EAAE;IAEL,qBACE,MAACtK;QAAO0C,MAAMA;QAAMC,SAASgC,UAAUvB,YAAYT;QAAS+J,UAAS;QAAKC,SAAS;;0BACjF,KAACxM;0BAAa;;0BACd,MAACD;gBAAc0M,IAAI;oBAAEC,SAAS;oBAAQC,eAAe;oBAAUC,KAAK;gBAAE;;oBACnE,CAACtK,uBACA,KAACnB;wBACCmB,QAAQA;wBACRuK,OAAM;wBACNC,YAAW;wBACXC,UAAUvI;yBAEV;oBACHV,SAASE,MAAM,KAAK,uBACnB,KAACrE;wBAAMqN,UAAS;wBAAOP,IAAI;4BAAEQ,IAAI;wBAAE;kCAChC;yBAEDnJ,SAASE,MAAM,KAAK,0BACtB,MAACrE;wBAAMqN,UAAS;wBAAUP,IAAI;4BAAEQ,IAAI;wBAAE;;0CACpC,KAAC7M;gCAAWmL,SAAQ;0CAASzH,SAAS0C,OAAO;;4BAC5C3C,4BACC,KAACzD;gCAAWmL,SAAQ;gCAAQkB,IAAI;oCAAEQ,IAAI;gCAAI;0CACxC,cAAA,KAAC5N;oCAAQ6N,MAAMrJ;8CACZC,SAAS6C,SAAS,GAAG,mBAAmB;;+CAI7C,KAACvG;gCAAWmL,SAAQ;gCAAQkB,IAAI;oCAAEQ,IAAI;gCAAI;0CACvC;;;yBAILnJ,SAASE,MAAM,KAAK,wBACtB,KAACrE;wBAAMqN,UAAS;wBAAQP,IAAI;4BAAEQ,IAAI;wBAAE;kCACjCnJ,SAAS0C,OAAO;yBAEjB;oBACHjE,QAAQ,CAAC0B,aAAcH,CAAAA,SAASE,MAAM,KAAK,WAAWF,SAASE,MAAM,KAAK,SAAQ,kBACjF,KAACrE;wBAAMqN,UAAS;wBAAUP,IAAI;4BAAEQ,IAAI;wBAAE;kCACnC;yBAED;kCACJ,KAAC9M;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OAAOL;wBACPmB,WAAW;4BAAEf,OAAO;gCAAEgB,UAAU;4BAAK;4BAAGC,YAAY;gCAAEC,QAAQ;4BAAK;wBAAE;wBACrEd,IAAI;4BAAEQ,IAAInJ,SAASE,MAAM,KAAK,WAAWF,SAASE,MAAM,KAAK,YAAY,IAAI;wBAAE;;kCAEjF,KAAC5C;wBAAmBoM,OAAO7B;;kCAC3B,KAACxL;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OACExI,SAASE,MAAM,KAAK,UAChBF,SAASgD,IAAI,GACbhD,SAASE,MAAM,KAAK,YAClB,0CACAF,SAASE,MAAM,KAAK,SAClB,6BACA;wBAEVoJ,WAAW;4BAAEf,OAAO;gCAAEgB,UAAU;4BAAK;4BAAGC,YAAY;gCAAEC,QAAQ;4BAAK;wBAAE;;kCAEvE,KAACpN;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OAAOV;wBACPkB,YAAW;wBACXM,WAAW;4BAAEf,OAAO;gCAAEgB,UAAU;4BAAK;4BAAGC,YAAY;gCAAEC,QAAQ;4BAAK;wBAAE;;oBAEtE5J,QAAQK,MAAM,KAAK,wBAClB,KAAC7D;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OAAO3I,QAAQ8J,OAAO;wBACtBX,YAAY7L;wBACZmM,WAAW;4BACTf,OAAO;gCACLgB,UAAU;gCACVK,4BACE,KAACzN;oCAAe0N,UAAS;8CACvB,cAAA,KAACzM;wCAAyBuM,SAAS9J,QAAQ8J,OAAO;;;4BAGxD;4BACAH,YAAY;gCAAEC,QAAQ;4BAAK;wBAC7B;yBAEA;oBACHrI,SAAS+D,MAAM,iBACd,KAACpH;wBACCiD,WAAWI;wBACXoH,OAAO/G;wBACPqI,UAAU,CAAC7E,WAAa,KAAKD,cAAcC;wBAC3CgE,UAAUvI;yBAEV;kCACJ,KAACrE;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OAAOlI;wBACPwJ,UAAU,CAACC,QAAUxJ,WAAWwJ,MAAMC,MAAM,CAACxB,KAAK;wBAClDyB,SAAS;wBACTX,WAAW;4BAAEY,WAAW;gCAAEC,WAAWvP;4BAAsB;wBAAE;;kCAE/D,MAACwB;wBAAMgO,WAAU;wBAAMC,SAAS;wBAAG1B,IAAI;4BAAE2B,gBAAgB;wBAAW;;0CAClE,KAACzM;gCACC2D,UAAUA;gCACV+I,QAAQtI;gCACRgH,UAAUvI;gCACV8J,UAAU3E;;0CAEZ,KAAC/J;gCACCuN,MAAK;gCACLJ,UAAUvI,WAAW,CAACK,cAAcE,KAAK,IAAK,CAACX,QAAQ2D,IAAI,MAAM,CAACzD,KAAKyD,IAAI;gCAC3EwG,SAAS,IAAM7I,YAAY;0CAE1B;;;;kCAGL,KAACvF;wBACCgN,MAAK;wBACLN,OAAM;wBACNP,OAAOhI;wBACPsJ,UAAU,CAACC,QAAUtJ,QAAQsJ,MAAMC,MAAM,CAACxB,KAAK;wBAC/CkC,SAAS;wBACTC,SAAS;wBACTC,UAAU7I;wBACViH,YACElE,WAAWK,MAAM,GACblK,0BAA0B6J,cAC1B9C,iBACE,oFACA;wBAERsH,WAAW;4BACTY,WAAW;gCAAEC,WAAWxP;4BAAmB;4BAC3CkQ,gBAAgB;gCAAElC,IAAI7D,WAAWK,MAAM,GAAG;oCAAE2F,OAAO;gCAAe,IAAI3L;4BAAU;wBAClF;;oBAEDqG,kBAAkBI,4BACjB,KAACJ;wBACCuF,MAAK;wBACL,mEAAmE;wBACnE,8BAA8B;wBAC9BvM,QAAQoH,YAAYrE,IAAI,KAAK,UAAU,CAACtC,QAAQT,SAAS;wBACzDU,KAAK,EAAEA,gBAAAA,QAASC;wBAChB6L,QAAQ,aAAKpF;4BAAavB,MAAM1B,QAAOpE,cAAAA,MAAM8F,IAAI,YAAV9F,cAAc;;wBACrD+B,SAASA;wBACTE,MAAMA;wBACNiF,cAAc,CAACC,QAAmC,KAAKD,aAAaC;yBAEpE;oBACHb,wBACC,MAACzI;wBAAMiO,SAAS;wBAAKY,eAAY;;0CAC/B,KAAC3O;gCAAWmL,SAAQ;gCAAUqD,OAAM;0CACjC;;0CAEH,KAACxO;gCAAWmL,SAAQ;0CAAa5C,QAAQvE,OAAO,CAACwF,IAAI;;0CACrD,KAACxJ;gCAAWmL,SAAQ;gCAAQkB,IAAI;oCAAEuC,YAAY;gCAAW;0CACtDrG,QAAQrE,IAAI,CAACsF,IAAI;;;yBAGpB;oBACHsC,2BACC,KAACnL;wBACCwK,SAAQ;wBACRjJ,QAAQiB;wBACRd,KAAKA;wBACL4C,MAAM6G,WAAW7G,IAAI;wBACrB4J,UAAU/C,WAAWzE,EAAE;wBACvB6G,UAAUnC;yBAEV;oBACHzH,sBAAQ,KAAC/E;wBAAMqN,UAAS;kCAAStI;yBAAiB;;;0BAErD,KAAC9C;gBACCW,MAAMkD;gBACNjD,SAAS,IAAMkD,YAAY;gBAC3BwJ,QAAQxE;;0BAEV,MAAC5K;;kCACC,KAACF;wBAAO2O,SAAS/L;wBAASuK,UAAUvI;kCACjC;;kCAEH,KAAC5E;wBAAO2L,SAAQ;wBAAYqD,OAAM;wBAAU7B,UAAU,CAAClB;wBAAS0C,SAASxC;kCACtEvH,UAAU,aAAa;;;;;;AAKlC;AACApC,mBAAmBsG,WAAW,GAAG;AAEjC,eAAetG,mBAAkB"}
|
|
@@ -49,7 +49,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
49
49
|
* is idempotent by construction (the door dedupes on the address, or the
|
|
50
50
|
* domain and the name), so running the same file again finishes what a
|
|
51
51
|
* closed drawer left, reporting the earlier rows as merged.
|
|
52
|
-
*/ import { customImportTarget, customImportTargetKey, importNoMailServerSentence,
|
|
52
|
+
*/ import { customImportTarget, customImportTargetKey, importNoMailServerSentence, CSV_UPLOAD_MAX_CHARACTERS, parseCsv } from "@aglyn/aglyn";
|
|
53
53
|
import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
|
|
54
54
|
import { Container, HelpTip, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
|
|
55
55
|
import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
|
|
@@ -125,7 +125,7 @@ export function CsvImportDrawer(props) {
|
|
|
125
125
|
const handleFile = useCallback(async (file)=>{
|
|
126
126
|
if (!file) return;
|
|
127
127
|
const contents = await file.text().catch(()=>'');
|
|
128
|
-
if (contents.length >
|
|
128
|
+
if (contents.length > CSV_UPLOAD_MAX_CHARACTERS) {
|
|
129
129
|
return void enqueueSnackbar('That file is too large to read in one go. Split it and import the ' + 'pieces.', {
|
|
130
130
|
variant: 'warning',
|
|
131
131
|
allowDuplicate: true
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/csv-import-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\n/**\n * IMPORTING A CSV INTO A CRM COLLECTION — the screen, for whichever\n * collection (AGL-2602, shared since AGL-2621).\n *\n * The contact import settled the walk from a file on disk to a result the\n * operator can act on: choose the file, read what is in it, match its\n * columns to fields with the first row beside each, preview the ten rows\n * the mapping produces, run it in bounded requests with a progress bar,\n * and report what was added, updated and skipped with the skipped rows as\n * a file to fix. A companies file walks the same path with a different\n * vocabulary, so the walk lives here once and each collection hands in a\n * {@link CsvImportVocabulary}: its fields and their labels, the one field\n * every row needs, its route, its tally, its template.\n *\n * ## The file is read in the browser, and judged on the server\n *\n * `parseCsv` — the same parser the list import and the dataset import run\n * — turns the file into cells here, so the operator can map and preview\n * without a round trip and without uploading a file they may still decide\n * not to import. Nothing is VALIDATED here beyond counting the rows whose\n * required cell is unusable, and that count is a courtesy: every row is\n * judged by the route, through the normalizers every other door uses, and\n * the numbers on the result panel are the server's.\n *\n * ## Bounded requests, and closing stops them\n *\n * A chunk per request, as many requests as the file needs, and a progress\n * bar between them. Closing the drawer mid-run stops the loop after the\n * request in flight; the rows already written stay written and the result\n * panel says how far it got. There is no durable job to resume — an import\n * is idempotent by construction (the door dedupes on the address, or the\n * domain and the name), so running the same file again finishes what a\n * closed drawer left, reporting the earlier rows as merged.\n */\n\nimport {\n customImportTarget,\n customImportTargetKey,\n type ImportChunkResult,\n type ImportSkippedRow,\n importNoMailServerSentence,\n LIST_IMPORT_MAX_CHARACTERS,\n parseCsv,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useOrgDataScope, useUser } from '@aglyn/tenant-feature-instance'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n Alert,\n AlertTitle,\n Box,\n Button,\n Chip,\n IconButton,\n LinearProgress,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n type ComponentProps,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { importColumns } from '../model/import-column-shapes'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A column's target: a standard field, or a custom one as `custom:<key>`. */\nexport type CsvImportTarget<F extends string> = F | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type CsvImportMapping<F extends string> = Record<number, CsvImportTarget<F>>\n\n/** A custom field a column may be mapped to, when the collection has any. */\nexport interface CsvImportCustomField {\n key: string\n label: string\n}\n\n/**\n * Everything one collection's import says for itself. Pure values and pure\n * functions, so a vocabulary is built once outside React and handed in.\n */\nexport interface CsvImportVocabulary<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n /** The drawer's title — \"Import contacts from CSV\". */\n title: string\n /**\n * What the import is called where the `importMapping` zone names it —\n * `contacts`, `companies`, `deals`, `leads` (AGL-2917). Absent, the drawer\n * hosts no zone.\n */\n collection?: string\n help: ComponentProps<typeof HelpTip>\n /**\n * What the site picker's answer decides, said under it at the\n * organization level (AGL-2630). Absent, the picker's own sentence.\n */\n sitePickerHelperText?: string\n /** The paragraph under the title. */\n intro: string\n /** The standard targets, in the order the mapping menu lists them. */\n fields: readonly F[]\n fieldLabels: Record<F, string>\n /** Custom targets, offered after the standard ones; none for most collections. */\n customFields?: readonly CsvImportCustomField[]\n /** The one field every row needs, and what the drawer says when it is unmapped. */\n requiredField: F\n requiredWarning: string\n /**\n * Whether a required cell is one the server would refuse — the courtesy\n * count the preview shows. Absent, only a blank cell counts.\n */\n unusable?: (cell: string) => boolean\n /** The preview's sentence about them. */\n unusableNotice: (count: number, total: number) => string\n guessMapping: (columns: readonly string[]) => CsvImportMapping<F>\n mapRow: (cells: readonly string[], mapping: CsvImportMapping<F>) => R\n /** The browser-side URL of the route one chunk is posted to. */\n route: string\n maxRows: number\n chunkSize: number\n previewRows: number\n emptyResult: () => ImportChunkResult<S>\n mergeResults: (\n total: ImportChunkResult<S>,\n chunk: ImportChunkResult<S>,\n offset: number,\n ) => ImportChunkResult<S>\n skipLabels: Record<S['reason'], string>\n skippedCsv: (\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: S['reason'] }[],\n ) => string\n skippedFileName: string\n /** The export's header over no rows. */\n templateCsv: () => string\n templateFileName: string\n /** The result panel's collection-specific lines, when there are any. */\n resultExtras?: (result: ImportChunkResult<S>) => ReactNode\n /**\n * A collection-specific warning about the mapped file, shown above the\n * preview before anything is sent — a value the server will refuse that\n * the operator can fix first. Given every row as `mapRow` reads it;\n * answers nothing when the file is fine.\n */\n rowsNotice?: (rows: readonly R[]) => ReactNode\n}\n\nexport interface CsvImportDrawerProps<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n open: boolean\n onClose: () => void\n /**\n * The site the file is imported INTO — what the route files every row\n * under — or `null` at the organization level (AGL-2630), where the\n * drawer asks which site with a picker and holds the import until one is\n * named.\n */\n hostId: string | null\n vocabulary: CsvImportVocabulary<F, R, S>\n}\n\n/** The value the mapping select shows for a column that is not imported. */\nconst SKIP_TARGET = ''\n\n/** What a target is called — a standard field's label, or the custom field's. */\nfunction targetLabel<F extends string>(\n target: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return vocabulary.customFields?.find((field) => field.key === customKey)?.label ?? customKey\n }\n return vocabulary.fieldLabels[target as F] ?? target\n}\n\n/** What a dropped-value field is called in the result panel — the label without its hint. */\nfunction droppedFieldLabel<F extends string>(\n field: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n return targetLabel(field, vocabulary).replace(/ \\(.*\\)$/, '')\n}\n\nexport function CsvImportDrawer<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n>(props: CsvImportDrawerProps<F, R, S>) {\n const { open, onClose, hostId, vocabulary } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // The site every row is captured under: the mounted one, or at the\n // organization level the one the picker named — `null` until it has.\n const mount = useCrmOrgMount()\n const createHostId = hostId ?? mount?.createHostId ?? null\n\n const [fileName, setFileName] = useState('')\n const [columns, setColumns] = useState<string[]>([])\n const [rows, setRows] = useState<string[][]>([])\n /** How many rows the file had past the ceiling, so the drawer can say so. */\n const [overCeiling, setOverCeiling] = useState(0)\n const [mapping, setMapping] = useState<CsvImportMapping<F>>({})\n const [result, setResult] = useState<ImportChunkResult<S> | null>(null)\n const [progress, setProgress] = useState<{ sent: number; total: number } | null>(\n null,\n )\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state, for the reason the list import gives: the loop is\n * an `async` function that captured its closure when it started, and a\n * state value it read would be the one from that render.\n */\n const abandoned = useRef(false)\n\n useEffect(() => {\n if (open) abandoned.current = false\n }, [open])\n\n const reset = useCallback(() => {\n setFileName('')\n setColumns([])\n setRows([])\n setOverCeiling(0)\n setMapping({})\n setResult(null)\n setProgress(null)\n }, [])\n\n const handleClose = useCallback(() => {\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const table = parseCsv(contents).filter((cells) =>\n cells.some((cell) => String(cell ?? '').trim()),\n )\n if (table.length < 2) {\n return void enqueueSnackbar(\n 'That file needs a header row and at least one row under it.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const header = table[0].map((cell) => String(cell ?? '').trim())\n const body = table.slice(1)\n setFileName(file.name)\n setColumns(header)\n setRows(body.slice(0, vocabulary.maxRows))\n setOverCeiling(Math.max(0, body.length - vocabulary.maxRows))\n setMapping(vocabulary.guessMapping(header))\n setResult(null)\n setProgress(null)\n },\n [enqueueSnackbar, vocabulary],\n )\n\n const requiredMapped = Object.values(mapping).includes(vocabulary.requiredField)\n /** Rows whose required cell the server would refuse. */\n const unusableRows = useMemo(() => {\n const at = Number(\n Object.entries(mapping).find(\n ([, target]) => target === vocabulary.requiredField,\n )?.[0] ?? -1,\n )\n if (at < 0) return rows.length\n const unusable = vocabulary.unusable ?? ((cell: string) => !cell.trim())\n return rows.filter((cells) => unusable(String(cells[at] ?? ''))).length\n }, [mapping, rows, vocabulary])\n\n const preview = useMemo(\n () =>\n rows\n .slice(0, vocabulary.previewRows)\n .map((cells) => vocabulary.mapRow(cells, mapping)),\n [rows, mapping, vocabulary],\n )\n // The collection's own warning over the whole mapped file, when it has one.\n const rowsNotice = useMemo(\n () =>\n vocabulary.rowsNotice && rows.length\n ? vocabulary.rowsNotice(rows.map((cells) => vocabulary.mapRow(cells, mapping)))\n : null,\n [rows, mapping, vocabulary],\n )\n const mappedTargets = useMemo(\n () =>\n Object.entries(mapping)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([, target]) => target),\n [mapping],\n )\n\n /*\n * The `importMapping` zone (AGL-2917), hosted through the shell's renderer:\n * a widget there reads each column's header and shape, never a cell, and\n * proposes a matching, which replaces the table's above for the preview to\n * show. A target this import does not offer, or one a column before it\n * already took, is dropped, as the table itself would refuse it.\n */\n const MappingSlot = useConsoleWidgetSlot()\n const { orgId } = useOrgDataScope({ hostId: hostId ?? undefined, orgId: mount?.orgId })\n const shapedColumns = useMemo(() => importColumns(columns, rows), [columns, rows])\n const offeredTargets = useMemo(\n () =>\n new Set<string>([\n ...vocabulary.fields,\n ...(vocabulary.customFields ?? []).map((field) => customImportTarget(field.key)),\n ]),\n [vocabulary],\n )\n const proposeMapping = useCallback(\n (proposed: Readonly<Record<number, string>>) => {\n const taken = new Set<string>()\n const next: CsvImportMapping<F> = {}\n for (const [column, target] of Object.entries(proposed).sort(([a], [b]) => Number(a) - Number(b))) {\n const index = Number(column)\n if (!Number.isInteger(index) || index < 0 || index >= columns.length) continue\n if (!offeredTargets.has(target) || taken.has(target)) continue\n taken.add(target)\n next[index] = target as CsvImportTarget<F>\n }\n setMapping(next)\n },\n [offeredTargets, columns.length],\n )\n\n const handleMap = useCallback((index: number, target: string) => {\n setMapping((previous) => {\n const next: CsvImportMapping<F> = {}\n for (const [column, existing] of Object.entries(previous)) {\n // One column per target: choosing a target already taken moves it\n // here rather than importing one field from two columns.\n if (Number(column) !== index && existing !== target) {\n next[Number(column)] = existing\n }\n }\n if (target !== SKIP_TARGET) next[index] = target as CsvImportTarget<F>\n return next\n })\n }, [])\n\n /**\n * Runs the file, one bounded request at a time.\n *\n * Every chunk's result is folded into the running total as it arrives,\n * so a file that stops halfway — a lost connection, a closed drawer —\n * still reports what it did rather than nothing.\n */\n const handleImport = useCallback(async () => {\n if (busy || !rows.length || !requiredMapped || !createHostId) return\n setBusy(true)\n let total = vocabulary.emptyResult()\n setResult(total)\n setProgress({ sent: 0, total: rows.length })\n try {\n for (let offset = 0; offset < rows.length; offset += vocabulary.chunkSize) {\n if (abandoned.current) return\n const chunk = rows\n .slice(offset, offset + vocabulary.chunkSize)\n .map((cells) => vocabulary.mapRow(cells, mapping))\n const response = await authorizedFetch(user, vocabulary.route, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId: createHostId, rows: chunk }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n total = vocabulary.mergeResults(total, payload as ImportChunkResult<S>, offset)\n setResult(total)\n setProgress({ sent: Math.min(offset + chunk.length, rows.length), total: rows.length })\n }\n enqueueSnackbar('Import finished', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The import could not continue.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, rows, requiredMapped, mapping, user, createHostId, enqueueSnackbar, vocabulary])\n\n const handleDownloadSkipped = useCallback(() => {\n if (!result?.skipped.length) return\n downloadTextFile(\n vocabulary.skippedFileName,\n 'text/csv',\n vocabulary.skippedCsv(\n columns,\n result.skipped.map((entry) => ({\n cells: rows[entry.index] ?? [],\n reason: entry.reason as S['reason'],\n })),\n ),\n )\n }, [result, columns, rows, vocabulary])\n\n const finished = !!result && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\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 {vocabulary.title}\n </Typography>\n <HelpTip {...vocabulary.help} />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {result ? (\n <ImportResultPanel\n result={result}\n progress={progress}\n busy={busy}\n vocabulary={vocabulary}\n onDownloadSkipped={handleDownloadSkipped}\n onAnother={reset}\n />\n ) : (\n <>\n {/*\n First, because every row is filed under the answer: the site\n the file is imported into. Nothing under a site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText={vocabulary.sitePickerHelperText}\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {vocabulary.intro}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={(event) => {\n void handleFile(event.target.files?.[0])\n event.target.value = ''\n }}\n />\n </Button>\n {/* The export's own header over no rows, so a sheet filled in\n against it maps itself. */}\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() =>\n downloadTextFile(\n vocabulary.templateFileName,\n 'text/csv',\n vocabulary.templateCsv(),\n )\n }\n >\n {'Download template'}\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n {rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${rows.length.toLocaleString()} rows`}\n </Typography>\n ) : null}\n </Stack>\n {overCeiling > 0 ? (\n <Alert severity=\"warning\">\n {`This file has ${(rows.length + overCeiling).toLocaleString()} ` +\n `rows. Only the first ${vocabulary.maxRows.toLocaleString()} ` +\n 'are imported — split the file and import the rest ' +\n 'separately.'}\n </Alert>\n ) : null}\n {columns.length ? (\n <ImportMappingTable\n columns={columns}\n sample={rows[0] ?? []}\n mapping={mapping}\n vocabulary={vocabulary}\n onMap={handleMap}\n />\n ) : null}\n {columns.length && MappingSlot && vocabulary.collection ? (\n <MappingSlot\n slot=\"importMapping\"\n hostId={hostId}\n orgId={orgId ?? undefined}\n collection={vocabulary.collection}\n columns={shapedColumns}\n mapping={mapping}\n proposeMapping={(proposed: Readonly<Record<number, string>>) => proposeMapping(proposed)}\n />\n ) : null}\n {columns.length > 0 && !requiredMapped ? (\n <Alert severity=\"warning\">{vocabulary.requiredWarning}</Alert>\n ) : null}\n {columns.length > 0 && requiredMapped ? rowsNotice : null}\n {columns.length > 0 && requiredMapped ? (\n <ImportPreviewTable\n targets={mappedTargets}\n preview={preview}\n total={rows.length}\n unusable={unusableRows}\n vocabulary={vocabulary}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n // Held until the capturing site is known.\n disabled={busy || !rows.length || !requiredMapped || !createHostId}\n onClick={() => void handleImport()}\n >\n {rows.length\n ? `Import ${rows.length.toLocaleString()} rows`\n : 'Import'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCsvImportDrawer.displayName = 'CsvImportDrawer'\n\n/**\n * WHICH COLUMN IS WHICH.\n *\n * One row per column in the file: its header, the first row's value under\n * it so the operator can tell \"Title\" the job from \"Title\" the honorific,\n * and a select naming where it goes. The proposal came from the alias\n * table; every row of this table is the operator's to overrule.\n */\nfunction ImportMappingTable<F extends string>(props: {\n columns: readonly string[]\n sample: readonly string[]\n mapping: CsvImportMapping<F>\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fields' | 'fieldLabels' | 'customFields'>\n onMap: (index: number, target: string) => void\n}) {\n const { columns, sample, mapping, vocabulary, onMap } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Columns'}</Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Column'}</TableCell>\n <TableCell>{'First row'}</TableCell>\n <TableCell>{'Imports as'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {columns.map((column, index) => (\n <TableRow key={`${column}-${index}`}>\n <TableCell>{column || `Column ${index + 1}`}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>\n {String(sample[index] ?? '')}\n </Typography>\n </TableCell>\n <TableCell>\n <TextField\n select\n size=\"small\"\n value={mapping[index] ?? SKIP_TARGET}\n onChange={(event) => onMap(index, event.target.value)}\n sx={{ minWidth: 220 }}\n >\n <MenuItem value={SKIP_TARGET}>{'Do not import'}</MenuItem>\n {vocabulary.fields.map((field) => (\n <MenuItem key={field} value={field}>\n {vocabulary.fieldLabels[field]}\n </MenuItem>\n ))}\n {(vocabulary.customFields ?? []).map((field) => (\n <MenuItem key={field.key} value={customImportTarget(field.key)}>\n {`${field.label} (custom)`}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Stack>\n )\n}\n\n/**\n * The first rows, as the mapping reads them.\n *\n * Verbatim cell values under the fields they were mapped to — not the\n * normalized ones, because normalization is the server's and a preview that\n * showed a cleaned phone number would be promising a result the route may\n * refuse. What it does say is how many rows have no usable required cell,\n * since that is the one refusal the operator can fix before sending.\n */\nfunction ImportPreviewTable<F extends string>(props: {\n targets: readonly string[]\n preview: readonly Record<string, unknown>[]\n total: number\n unusable: number\n vocabulary: Pick<\n CsvImportVocabulary<F, never, never>,\n 'fieldLabels' | 'customFields' | 'previewRows' | 'unusableNotice'\n >\n}) {\n const { targets, preview, total, unusable, vocabulary } = props\n const cell = (row: Record<string, unknown>, target: string) => {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return String((row['custom'] as Record<string, string> | undefined)?.[customKey] ?? '')\n }\n return String(row[target] ?? '')\n }\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">\n {`Preview — first ${Math.min(preview.length, vocabulary.previewRows)} of ${total.toLocaleString()}`}\n </Typography>\n {unusable > 0 ? (\n <Alert severity=\"info\">{vocabulary.unusableNotice(unusable, total)}</Alert>\n ) : null}\n <Box>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {targets.map((target) => (\n <TableCell key={target}>{targetLabel(target, vocabulary)}</TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.map((row, index) => (\n <TableRow key={index}>\n {targets.map((target) => (\n <TableCell key={target}>\n <Typography variant=\"body2\" noWrap sx={{ maxWidth: 240 }}>\n {cell(row, target)}\n </Typography>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportResultPanel<F extends string, S extends ImportSkippedRow<string>>(props: {\n result: ImportChunkResult<S>\n progress: { sent: number; total: number } | null\n busy: boolean\n vocabulary: Pick<\n CsvImportVocabulary<F, never, S>,\n 'fieldLabels' | 'customFields' | 'skipLabels' | 'resultExtras'\n >\n onDownloadSkipped: () => void\n onAnother: () => void\n}) {\n const { result, progress, busy, vocabulary, onDownloadSkipped, onAnother } = props\n const percent = progress?.total\n ? Math.min((progress.sent / progress.total) * 100, 100)\n : 0\n const skippedByReason = useMemo(() => {\n const counts: Partial<Record<string, number>> = {}\n for (const entry of result.skipped) {\n counts[entry.reason] = (counts[entry.reason] ?? 0) + 1\n }\n return Object.entries(counts) as [S['reason'], number][]\n }, [result.skipped])\n const droppedFields = Object.entries(result.dropped).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {busy\n ? `${(progress?.sent ?? 0).toLocaleString()} of ${(progress?.total ?? 0).toLocaleString()} rows sent.`\n : `Finished. ${result.created.toLocaleString()} added, ` +\n `${result.merged.toLocaleString()} updated, ` +\n `${result.skipped.length.toLocaleString()} skipped.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {skippedByReason.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Skipped'}</TableCell>\n <TableCell align=\"right\">{'Rows'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {skippedByReason.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{vocabulary.skipLabels[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {droppedFields.length ? (\n <Alert severity=\"info\">\n <AlertTitle>{'Some values could not be read'}</AlertTitle>\n {'The rows were imported without them: ' +\n droppedFields\n .map(([field, count]) => `${droppedFieldLabel(field, vocabulary)} (${count})`)\n .join(', ') +\n '.'}\n </Alert>\n ) : null}\n {result.ownersUnresolved.length ? (\n <Alert severity=\"info\">\n {'No team member has these owner addresses, so those rows ' +\n `have no owner: ${result.ownersUnresolved.slice(0, 5).join(', ')}` +\n (result.ownersUnresolved.length > 5\n ? ` and ${result.ownersUnresolved.length - 5} more.`\n : '.')}\n </Alert>\n ) : null}\n {importNoMailServerSentence(result.noMailServer) ? (\n <Alert severity=\"warning\">{importNoMailServerSentence(result.noMailServer)}</Alert>\n ) : null}\n {vocabulary.resultExtras?.(result)}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {result.skipped.length ? (\n <Button variant=\"outlined\" onClick={onDownloadSkipped}>\n {'Download skipped rows'}\n </Button>\n ) : null}\n {!busy ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * An \"Import CSV\" button that mounts its drawer only once it has been\n * opened, so the list pays nothing — no hooks, no listeners — for a\n * feature the operator has not reached for.\n */\nexport function CsvImportButton(props: {\n label?: string\n children: (open: boolean, onClose: () => void) => ReactNode\n}) {\n const { label = 'Import CSV', children } = props\n const [open, setOpen] = useState(false)\n const [mounted, setMounted] = useState(false)\n const handleOpen = useCallback(() => {\n setMounted(true)\n setOpen(true)\n }, [])\n const handleClose = useCallback(() => setOpen(false), [])\n return (\n <>\n <Button size=\"small\" onClick={handleOpen}>\n {label}\n </Button>\n {mounted ? children(open, handleClose) : null}\n </>\n )\n}\nCsvImportButton.displayName = 'CsvImportButton'\n\nexport default CsvImportDrawer\n"],"names":["customImportTarget","customImportTargetKey","importNoMailServerSentence","LIST_IMPORT_MAX_CHARACTERS","parseCsv","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","authorizedFetch","useOrgDataScope","useUser","useConsoleWidgetSlot","Alert","AlertTitle","Box","Button","Chip","IconButton","LinearProgress","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","downloadTextFile","importColumns","CrmSitePicker","SKIP_TARGET","targetLabel","target","vocabulary","customKey","customFields","find","field","key","label","fieldLabels","droppedFieldLabel","replace","CsvImportDrawer","props","hostId","rows","open","onClose","data","user","enqueueSnackbar","mount","createHostId","fileName","setFileName","columns","setColumns","setRows","overCeiling","setOverCeiling","mapping","setMapping","result","setResult","progress","setProgress","busy","setBusy","abandoned","current","reset","handleClose","handleFile","file","contents","text","catch","length","variant","allowDuplicate","table","filter","cells","some","cell","String","trim","header","map","body","slice","name","maxRows","Math","max","guessMapping","requiredMapped","Object","values","includes","requiredField","unusableRows","at","Number","entries","unusable","preview","previewRows","mapRow","rowsNotice","mappedTargets","sort","a","b","MappingSlot","orgId","undefined","shapedColumns","offeredTargets","Set","fields","proposeMapping","proposed","taken","next","column","index","isInteger","has","add","handleMap","previous","existing","handleImport","total","emptyResult","sent","offset","chunkSize","chunk","response","route","method","headers","JSON","stringify","payload","json","ok","error","mergeResults","min","persist","handleDownloadSkipped","skipped","skippedFileName","skippedCsv","entry","reason","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","title","help","appBarRight","gutterY","spacing","ImportResultPanel","onDownloadSkipped","onAnother","disabled","helperText","sitePickerHelperText","intro","direction","alignItems","flexWrap","rowGap","input","type","accept","hidden","onChange","event","files","value","size","templateFileName","templateCsv","toLocaleString","severity","ImportMappingTable","sample","onMap","collection","slot","requiredWarning","ImportPreviewTable","targets","justifyContent","displayName","noWrap","select","minWidth","row","unusableNotice","maxWidth","percent","skippedByReason","counts","droppedFields","dropped","count","created","merged","borderRadius","align","skipLabels","join","ownersUnresolved","noMailServer","resultExtras","CsvImportButton","children","setOpen","mounted","setMounted","handleOpen"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/csv-import-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\n/**\n * IMPORTING A CSV INTO A CRM COLLECTION — the screen, for whichever\n * collection (AGL-2602, shared since AGL-2621).\n *\n * The contact import settled the walk from a file on disk to a result the\n * operator can act on: choose the file, read what is in it, match its\n * columns to fields with the first row beside each, preview the ten rows\n * the mapping produces, run it in bounded requests with a progress bar,\n * and report what was added, updated and skipped with the skipped rows as\n * a file to fix. A companies file walks the same path with a different\n * vocabulary, so the walk lives here once and each collection hands in a\n * {@link CsvImportVocabulary}: its fields and their labels, the one field\n * every row needs, its route, its tally, its template.\n *\n * ## The file is read in the browser, and judged on the server\n *\n * `parseCsv` — the same parser the list import and the dataset import run\n * — turns the file into cells here, so the operator can map and preview\n * without a round trip and without uploading a file they may still decide\n * not to import. Nothing is VALIDATED here beyond counting the rows whose\n * required cell is unusable, and that count is a courtesy: every row is\n * judged by the route, through the normalizers every other door uses, and\n * the numbers on the result panel are the server's.\n *\n * ## Bounded requests, and closing stops them\n *\n * A chunk per request, as many requests as the file needs, and a progress\n * bar between them. Closing the drawer mid-run stops the loop after the\n * request in flight; the rows already written stay written and the result\n * panel says how far it got. There is no durable job to resume — an import\n * is idempotent by construction (the door dedupes on the address, or the\n * domain and the name), so running the same file again finishes what a\n * closed drawer left, reporting the earlier rows as merged.\n */\n\nimport {\n customImportTarget,\n customImportTargetKey,\n type ImportChunkResult,\n type ImportSkippedRow,\n importNoMailServerSentence,\n CSV_UPLOAD_MAX_CHARACTERS,\n parseCsv,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useOrgDataScope, useUser } from '@aglyn/tenant-feature-instance'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n Alert,\n AlertTitle,\n Box,\n Button,\n Chip,\n IconButton,\n LinearProgress,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n type ComponentProps,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { importColumns } from '../model/import-column-shapes'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A column's target: a standard field, or a custom one as `custom:<key>`. */\nexport type CsvImportTarget<F extends string> = F | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type CsvImportMapping<F extends string> = Record<number, CsvImportTarget<F>>\n\n/** A custom field a column may be mapped to, when the collection has any. */\nexport interface CsvImportCustomField {\n key: string\n label: string\n}\n\n/**\n * Everything one collection's import says for itself. Pure values and pure\n * functions, so a vocabulary is built once outside React and handed in.\n */\nexport interface CsvImportVocabulary<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n /** The drawer's title — \"Import contacts from CSV\". */\n title: string\n /**\n * What the import is called where the `importMapping` zone names it —\n * `contacts`, `companies`, `deals`, `leads` (AGL-2917). Absent, the drawer\n * hosts no zone.\n */\n collection?: string\n help: ComponentProps<typeof HelpTip>\n /**\n * What the site picker's answer decides, said under it at the\n * organization level (AGL-2630). Absent, the picker's own sentence.\n */\n sitePickerHelperText?: string\n /** The paragraph under the title. */\n intro: string\n /** The standard targets, in the order the mapping menu lists them. */\n fields: readonly F[]\n fieldLabels: Record<F, string>\n /** Custom targets, offered after the standard ones; none for most collections. */\n customFields?: readonly CsvImportCustomField[]\n /** The one field every row needs, and what the drawer says when it is unmapped. */\n requiredField: F\n requiredWarning: string\n /**\n * Whether a required cell is one the server would refuse — the courtesy\n * count the preview shows. Absent, only a blank cell counts.\n */\n unusable?: (cell: string) => boolean\n /** The preview's sentence about them. */\n unusableNotice: (count: number, total: number) => string\n guessMapping: (columns: readonly string[]) => CsvImportMapping<F>\n mapRow: (cells: readonly string[], mapping: CsvImportMapping<F>) => R\n /** The browser-side URL of the route one chunk is posted to. */\n route: string\n maxRows: number\n chunkSize: number\n previewRows: number\n emptyResult: () => ImportChunkResult<S>\n mergeResults: (\n total: ImportChunkResult<S>,\n chunk: ImportChunkResult<S>,\n offset: number,\n ) => ImportChunkResult<S>\n skipLabels: Record<S['reason'], string>\n skippedCsv: (\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: S['reason'] }[],\n ) => string\n skippedFileName: string\n /** The export's header over no rows. */\n templateCsv: () => string\n templateFileName: string\n /** The result panel's collection-specific lines, when there are any. */\n resultExtras?: (result: ImportChunkResult<S>) => ReactNode\n /**\n * A collection-specific warning about the mapped file, shown above the\n * preview before anything is sent — a value the server will refuse that\n * the operator can fix first. Given every row as `mapRow` reads it;\n * answers nothing when the file is fine.\n */\n rowsNotice?: (rows: readonly R[]) => ReactNode\n}\n\nexport interface CsvImportDrawerProps<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n open: boolean\n onClose: () => void\n /**\n * The site the file is imported INTO — what the route files every row\n * under — or `null` at the organization level (AGL-2630), where the\n * drawer asks which site with a picker and holds the import until one is\n * named.\n */\n hostId: string | null\n vocabulary: CsvImportVocabulary<F, R, S>\n}\n\n/** The value the mapping select shows for a column that is not imported. */\nconst SKIP_TARGET = ''\n\n/** What a target is called — a standard field's label, or the custom field's. */\nfunction targetLabel<F extends string>(\n target: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return vocabulary.customFields?.find((field) => field.key === customKey)?.label ?? customKey\n }\n return vocabulary.fieldLabels[target as F] ?? target\n}\n\n/** What a dropped-value field is called in the result panel — the label without its hint. */\nfunction droppedFieldLabel<F extends string>(\n field: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n return targetLabel(field, vocabulary).replace(/ \\(.*\\)$/, '')\n}\n\nexport function CsvImportDrawer<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n>(props: CsvImportDrawerProps<F, R, S>) {\n const { open, onClose, hostId, vocabulary } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // The site every row is captured under: the mounted one, or at the\n // organization level the one the picker named — `null` until it has.\n const mount = useCrmOrgMount()\n const createHostId = hostId ?? mount?.createHostId ?? null\n\n const [fileName, setFileName] = useState('')\n const [columns, setColumns] = useState<string[]>([])\n const [rows, setRows] = useState<string[][]>([])\n /** How many rows the file had past the ceiling, so the drawer can say so. */\n const [overCeiling, setOverCeiling] = useState(0)\n const [mapping, setMapping] = useState<CsvImportMapping<F>>({})\n const [result, setResult] = useState<ImportChunkResult<S> | null>(null)\n const [progress, setProgress] = useState<{ sent: number; total: number } | null>(\n null,\n )\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state, for the reason the list import gives: the loop is\n * an `async` function that captured its closure when it started, and a\n * state value it read would be the one from that render.\n */\n const abandoned = useRef(false)\n\n useEffect(() => {\n if (open) abandoned.current = false\n }, [open])\n\n const reset = useCallback(() => {\n setFileName('')\n setColumns([])\n setRows([])\n setOverCeiling(0)\n setMapping({})\n setResult(null)\n setProgress(null)\n }, [])\n\n const handleClose = useCallback(() => {\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > CSV_UPLOAD_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const table = parseCsv(contents).filter((cells) =>\n cells.some((cell) => String(cell ?? '').trim()),\n )\n if (table.length < 2) {\n return void enqueueSnackbar(\n 'That file needs a header row and at least one row under it.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const header = table[0].map((cell) => String(cell ?? '').trim())\n const body = table.slice(1)\n setFileName(file.name)\n setColumns(header)\n setRows(body.slice(0, vocabulary.maxRows))\n setOverCeiling(Math.max(0, body.length - vocabulary.maxRows))\n setMapping(vocabulary.guessMapping(header))\n setResult(null)\n setProgress(null)\n },\n [enqueueSnackbar, vocabulary],\n )\n\n const requiredMapped = Object.values(mapping).includes(vocabulary.requiredField)\n /** Rows whose required cell the server would refuse. */\n const unusableRows = useMemo(() => {\n const at = Number(\n Object.entries(mapping).find(\n ([, target]) => target === vocabulary.requiredField,\n )?.[0] ?? -1,\n )\n if (at < 0) return rows.length\n const unusable = vocabulary.unusable ?? ((cell: string) => !cell.trim())\n return rows.filter((cells) => unusable(String(cells[at] ?? ''))).length\n }, [mapping, rows, vocabulary])\n\n const preview = useMemo(\n () =>\n rows\n .slice(0, vocabulary.previewRows)\n .map((cells) => vocabulary.mapRow(cells, mapping)),\n [rows, mapping, vocabulary],\n )\n // The collection's own warning over the whole mapped file, when it has one.\n const rowsNotice = useMemo(\n () =>\n vocabulary.rowsNotice && rows.length\n ? vocabulary.rowsNotice(rows.map((cells) => vocabulary.mapRow(cells, mapping)))\n : null,\n [rows, mapping, vocabulary],\n )\n const mappedTargets = useMemo(\n () =>\n Object.entries(mapping)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([, target]) => target),\n [mapping],\n )\n\n /*\n * The `importMapping` zone (AGL-2917), hosted through the shell's renderer:\n * a widget there reads each column's header and shape, never a cell, and\n * proposes a matching, which replaces the table's above for the preview to\n * show. A target this import does not offer, or one a column before it\n * already took, is dropped, as the table itself would refuse it.\n */\n const MappingSlot = useConsoleWidgetSlot()\n const { orgId } = useOrgDataScope({ hostId: hostId ?? undefined, orgId: mount?.orgId })\n const shapedColumns = useMemo(() => importColumns(columns, rows), [columns, rows])\n const offeredTargets = useMemo(\n () =>\n new Set<string>([\n ...vocabulary.fields,\n ...(vocabulary.customFields ?? []).map((field) => customImportTarget(field.key)),\n ]),\n [vocabulary],\n )\n const proposeMapping = useCallback(\n (proposed: Readonly<Record<number, string>>) => {\n const taken = new Set<string>()\n const next: CsvImportMapping<F> = {}\n for (const [column, target] of Object.entries(proposed).sort(([a], [b]) => Number(a) - Number(b))) {\n const index = Number(column)\n if (!Number.isInteger(index) || index < 0 || index >= columns.length) continue\n if (!offeredTargets.has(target) || taken.has(target)) continue\n taken.add(target)\n next[index] = target as CsvImportTarget<F>\n }\n setMapping(next)\n },\n [offeredTargets, columns.length],\n )\n\n const handleMap = useCallback((index: number, target: string) => {\n setMapping((previous) => {\n const next: CsvImportMapping<F> = {}\n for (const [column, existing] of Object.entries(previous)) {\n // One column per target: choosing a target already taken moves it\n // here rather than importing one field from two columns.\n if (Number(column) !== index && existing !== target) {\n next[Number(column)] = existing\n }\n }\n if (target !== SKIP_TARGET) next[index] = target as CsvImportTarget<F>\n return next\n })\n }, [])\n\n /**\n * Runs the file, one bounded request at a time.\n *\n * Every chunk's result is folded into the running total as it arrives,\n * so a file that stops halfway — a lost connection, a closed drawer —\n * still reports what it did rather than nothing.\n */\n const handleImport = useCallback(async () => {\n if (busy || !rows.length || !requiredMapped || !createHostId) return\n setBusy(true)\n let total = vocabulary.emptyResult()\n setResult(total)\n setProgress({ sent: 0, total: rows.length })\n try {\n for (let offset = 0; offset < rows.length; offset += vocabulary.chunkSize) {\n if (abandoned.current) return\n const chunk = rows\n .slice(offset, offset + vocabulary.chunkSize)\n .map((cells) => vocabulary.mapRow(cells, mapping))\n const response = await authorizedFetch(user, vocabulary.route, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId: createHostId, rows: chunk }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n total = vocabulary.mergeResults(total, payload as ImportChunkResult<S>, offset)\n setResult(total)\n setProgress({ sent: Math.min(offset + chunk.length, rows.length), total: rows.length })\n }\n enqueueSnackbar('Import finished', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The import could not continue.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, rows, requiredMapped, mapping, user, createHostId, enqueueSnackbar, vocabulary])\n\n const handleDownloadSkipped = useCallback(() => {\n if (!result?.skipped.length) return\n downloadTextFile(\n vocabulary.skippedFileName,\n 'text/csv',\n vocabulary.skippedCsv(\n columns,\n result.skipped.map((entry) => ({\n cells: rows[entry.index] ?? [],\n reason: entry.reason as S['reason'],\n })),\n ),\n )\n }, [result, columns, rows, vocabulary])\n\n const finished = !!result && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\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 {vocabulary.title}\n </Typography>\n <HelpTip {...vocabulary.help} />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {result ? (\n <ImportResultPanel\n result={result}\n progress={progress}\n busy={busy}\n vocabulary={vocabulary}\n onDownloadSkipped={handleDownloadSkipped}\n onAnother={reset}\n />\n ) : (\n <>\n {/*\n First, because every row is filed under the answer: the site\n the file is imported into. Nothing under a site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText={vocabulary.sitePickerHelperText}\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {vocabulary.intro}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={(event) => {\n void handleFile(event.target.files?.[0])\n event.target.value = ''\n }}\n />\n </Button>\n {/* The export's own header over no rows, so a sheet filled in\n against it maps itself. */}\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() =>\n downloadTextFile(\n vocabulary.templateFileName,\n 'text/csv',\n vocabulary.templateCsv(),\n )\n }\n >\n {'Download template'}\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n {rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${rows.length.toLocaleString()} rows`}\n </Typography>\n ) : null}\n </Stack>\n {overCeiling > 0 ? (\n <Alert severity=\"warning\">\n {`This file has ${(rows.length + overCeiling).toLocaleString()} ` +\n `rows. Only the first ${vocabulary.maxRows.toLocaleString()} ` +\n 'are imported — split the file and import the rest ' +\n 'separately.'}\n </Alert>\n ) : null}\n {columns.length ? (\n <ImportMappingTable\n columns={columns}\n sample={rows[0] ?? []}\n mapping={mapping}\n vocabulary={vocabulary}\n onMap={handleMap}\n />\n ) : null}\n {columns.length && MappingSlot && vocabulary.collection ? (\n <MappingSlot\n slot=\"importMapping\"\n hostId={hostId}\n orgId={orgId ?? undefined}\n collection={vocabulary.collection}\n columns={shapedColumns}\n mapping={mapping}\n proposeMapping={(proposed: Readonly<Record<number, string>>) => proposeMapping(proposed)}\n />\n ) : null}\n {columns.length > 0 && !requiredMapped ? (\n <Alert severity=\"warning\">{vocabulary.requiredWarning}</Alert>\n ) : null}\n {columns.length > 0 && requiredMapped ? rowsNotice : null}\n {columns.length > 0 && requiredMapped ? (\n <ImportPreviewTable\n targets={mappedTargets}\n preview={preview}\n total={rows.length}\n unusable={unusableRows}\n vocabulary={vocabulary}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n // Held until the capturing site is known.\n disabled={busy || !rows.length || !requiredMapped || !createHostId}\n onClick={() => void handleImport()}\n >\n {rows.length\n ? `Import ${rows.length.toLocaleString()} rows`\n : 'Import'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCsvImportDrawer.displayName = 'CsvImportDrawer'\n\n/**\n * WHICH COLUMN IS WHICH.\n *\n * One row per column in the file: its header, the first row's value under\n * it so the operator can tell \"Title\" the job from \"Title\" the honorific,\n * and a select naming where it goes. The proposal came from the alias\n * table; every row of this table is the operator's to overrule.\n */\nfunction ImportMappingTable<F extends string>(props: {\n columns: readonly string[]\n sample: readonly string[]\n mapping: CsvImportMapping<F>\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fields' | 'fieldLabels' | 'customFields'>\n onMap: (index: number, target: string) => void\n}) {\n const { columns, sample, mapping, vocabulary, onMap } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Columns'}</Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Column'}</TableCell>\n <TableCell>{'First row'}</TableCell>\n <TableCell>{'Imports as'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {columns.map((column, index) => (\n <TableRow key={`${column}-${index}`}>\n <TableCell>{column || `Column ${index + 1}`}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>\n {String(sample[index] ?? '')}\n </Typography>\n </TableCell>\n <TableCell>\n <TextField\n select\n size=\"small\"\n value={mapping[index] ?? SKIP_TARGET}\n onChange={(event) => onMap(index, event.target.value)}\n sx={{ minWidth: 220 }}\n >\n <MenuItem value={SKIP_TARGET}>{'Do not import'}</MenuItem>\n {vocabulary.fields.map((field) => (\n <MenuItem key={field} value={field}>\n {vocabulary.fieldLabels[field]}\n </MenuItem>\n ))}\n {(vocabulary.customFields ?? []).map((field) => (\n <MenuItem key={field.key} value={customImportTarget(field.key)}>\n {`${field.label} (custom)`}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Stack>\n )\n}\n\n/**\n * The first rows, as the mapping reads them.\n *\n * Verbatim cell values under the fields they were mapped to — not the\n * normalized ones, because normalization is the server's and a preview that\n * showed a cleaned phone number would be promising a result the route may\n * refuse. What it does say is how many rows have no usable required cell,\n * since that is the one refusal the operator can fix before sending.\n */\nfunction ImportPreviewTable<F extends string>(props: {\n targets: readonly string[]\n preview: readonly Record<string, unknown>[]\n total: number\n unusable: number\n vocabulary: Pick<\n CsvImportVocabulary<F, never, never>,\n 'fieldLabels' | 'customFields' | 'previewRows' | 'unusableNotice'\n >\n}) {\n const { targets, preview, total, unusable, vocabulary } = props\n const cell = (row: Record<string, unknown>, target: string) => {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return String((row['custom'] as Record<string, string> | undefined)?.[customKey] ?? '')\n }\n return String(row[target] ?? '')\n }\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">\n {`Preview — first ${Math.min(preview.length, vocabulary.previewRows)} of ${total.toLocaleString()}`}\n </Typography>\n {unusable > 0 ? (\n <Alert severity=\"info\">{vocabulary.unusableNotice(unusable, total)}</Alert>\n ) : null}\n <Box>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {targets.map((target) => (\n <TableCell key={target}>{targetLabel(target, vocabulary)}</TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.map((row, index) => (\n <TableRow key={index}>\n {targets.map((target) => (\n <TableCell key={target}>\n <Typography variant=\"body2\" noWrap sx={{ maxWidth: 240 }}>\n {cell(row, target)}\n </Typography>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportResultPanel<F extends string, S extends ImportSkippedRow<string>>(props: {\n result: ImportChunkResult<S>\n progress: { sent: number; total: number } | null\n busy: boolean\n vocabulary: Pick<\n CsvImportVocabulary<F, never, S>,\n 'fieldLabels' | 'customFields' | 'skipLabels' | 'resultExtras'\n >\n onDownloadSkipped: () => void\n onAnother: () => void\n}) {\n const { result, progress, busy, vocabulary, onDownloadSkipped, onAnother } = props\n const percent = progress?.total\n ? Math.min((progress.sent / progress.total) * 100, 100)\n : 0\n const skippedByReason = useMemo(() => {\n const counts: Partial<Record<string, number>> = {}\n for (const entry of result.skipped) {\n counts[entry.reason] = (counts[entry.reason] ?? 0) + 1\n }\n return Object.entries(counts) as [S['reason'], number][]\n }, [result.skipped])\n const droppedFields = Object.entries(result.dropped).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {busy\n ? `${(progress?.sent ?? 0).toLocaleString()} of ${(progress?.total ?? 0).toLocaleString()} rows sent.`\n : `Finished. ${result.created.toLocaleString()} added, ` +\n `${result.merged.toLocaleString()} updated, ` +\n `${result.skipped.length.toLocaleString()} skipped.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {skippedByReason.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Skipped'}</TableCell>\n <TableCell align=\"right\">{'Rows'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {skippedByReason.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{vocabulary.skipLabels[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {droppedFields.length ? (\n <Alert severity=\"info\">\n <AlertTitle>{'Some values could not be read'}</AlertTitle>\n {'The rows were imported without them: ' +\n droppedFields\n .map(([field, count]) => `${droppedFieldLabel(field, vocabulary)} (${count})`)\n .join(', ') +\n '.'}\n </Alert>\n ) : null}\n {result.ownersUnresolved.length ? (\n <Alert severity=\"info\">\n {'No team member has these owner addresses, so those rows ' +\n `have no owner: ${result.ownersUnresolved.slice(0, 5).join(', ')}` +\n (result.ownersUnresolved.length > 5\n ? ` and ${result.ownersUnresolved.length - 5} more.`\n : '.')}\n </Alert>\n ) : null}\n {importNoMailServerSentence(result.noMailServer) ? (\n <Alert severity=\"warning\">{importNoMailServerSentence(result.noMailServer)}</Alert>\n ) : null}\n {vocabulary.resultExtras?.(result)}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {result.skipped.length ? (\n <Button variant=\"outlined\" onClick={onDownloadSkipped}>\n {'Download skipped rows'}\n </Button>\n ) : null}\n {!busy ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * An \"Import CSV\" button that mounts its drawer only once it has been\n * opened, so the list pays nothing — no hooks, no listeners — for a\n * feature the operator has not reached for.\n */\nexport function CsvImportButton(props: {\n label?: string\n children: (open: boolean, onClose: () => void) => ReactNode\n}) {\n const { label = 'Import CSV', children } = props\n const [open, setOpen] = useState(false)\n const [mounted, setMounted] = useState(false)\n const handleOpen = useCallback(() => {\n setMounted(true)\n setOpen(true)\n }, [])\n const handleClose = useCallback(() => setOpen(false), [])\n return (\n <>\n <Button size=\"small\" onClick={handleOpen}>\n {label}\n </Button>\n {mounted ? children(open, handleClose) : null}\n </>\n )\n}\nCsvImportButton.displayName = 'CsvImportButton'\n\nexport default CsvImportDrawer\n"],"names":["customImportTarget","customImportTargetKey","importNoMailServerSentence","CSV_UPLOAD_MAX_CHARACTERS","parseCsv","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","authorizedFetch","useOrgDataScope","useUser","useConsoleWidgetSlot","Alert","AlertTitle","Box","Button","Chip","IconButton","LinearProgress","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","downloadTextFile","importColumns","CrmSitePicker","SKIP_TARGET","targetLabel","target","vocabulary","customKey","customFields","find","field","key","label","fieldLabels","droppedFieldLabel","replace","CsvImportDrawer","props","hostId","rows","open","onClose","data","user","enqueueSnackbar","mount","createHostId","fileName","setFileName","columns","setColumns","setRows","overCeiling","setOverCeiling","mapping","setMapping","result","setResult","progress","setProgress","busy","setBusy","abandoned","current","reset","handleClose","handleFile","file","contents","text","catch","length","variant","allowDuplicate","table","filter","cells","some","cell","String","trim","header","map","body","slice","name","maxRows","Math","max","guessMapping","requiredMapped","Object","values","includes","requiredField","unusableRows","at","Number","entries","unusable","preview","previewRows","mapRow","rowsNotice","mappedTargets","sort","a","b","MappingSlot","orgId","undefined","shapedColumns","offeredTargets","Set","fields","proposeMapping","proposed","taken","next","column","index","isInteger","has","add","handleMap","previous","existing","handleImport","total","emptyResult","sent","offset","chunkSize","chunk","response","route","method","headers","JSON","stringify","payload","json","ok","error","mergeResults","min","persist","handleDownloadSkipped","skipped","skippedFileName","skippedCsv","entry","reason","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","title","help","appBarRight","gutterY","spacing","ImportResultPanel","onDownloadSkipped","onAnother","disabled","helperText","sitePickerHelperText","intro","direction","alignItems","flexWrap","rowGap","input","type","accept","hidden","onChange","event","files","value","size","templateFileName","templateCsv","toLocaleString","severity","ImportMappingTable","sample","onMap","collection","slot","requiredWarning","ImportPreviewTable","targets","justifyContent","displayName","noWrap","select","minWidth","row","unusableNotice","maxWidth","percent","skippedByReason","counts","droppedFields","dropped","count","created","merged","borderRadius","align","skipLabels","join","ownersUnresolved","noMailServer","resultExtras","CsvImportButton","children","setOpen","mounted","setMounted","handleOpen"],"mappings":"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"}
|
|
@@ -37,6 +37,8 @@ import { DEAL_STATUS_LABELS, formatMoney } from "../model/deal-board-model.js";
|
|
|
37
37
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
38
38
|
import { CrmRecordInsightsZone } from "./crm-record-insights-zone.js";
|
|
39
39
|
import { RecordActivityCard } from "./record-activity-card.js";
|
|
40
|
+
import { CrmShareChipView, RecordSharingCard } from "./record-sharing-card.js";
|
|
41
|
+
import { crmShareChipFor } from "../model/crm-sharing.js";
|
|
40
42
|
import { CrmSendEmailButton } from "./crm-send-email-button.js";
|
|
41
43
|
import { DealEditDrawer } from "./deal-edit-drawer.js";
|
|
42
44
|
import { DealProductsCard } from "./deal-products-card.js";
|
|
@@ -56,7 +58,8 @@ import { RecordTasksCard } from "./record-tasks-card.js";
|
|
|
56
58
|
* client-direct delete the rules allow the same people who could have
|
|
57
59
|
* created it.
|
|
58
60
|
*/ export function DealDetailPage(props) {
|
|
59
|
-
var _ref, _ref1, _ref2, _ref3, _DEAL_STATUS_LABELS_deal_status, _ref4;
|
|
61
|
+
var _ref, _ref1, _ref2, _ref3, _DEAL_STATUS_LABELS_deal_status, _ref4, _ref5;
|
|
62
|
+
var _scope_consentGroup, _scope_consentGroup1;
|
|
60
63
|
const { id, basePath, hostId, org } = props;
|
|
61
64
|
const routes = crmRoutes(basePath);
|
|
62
65
|
const router = useRouter();
|
|
@@ -208,6 +211,10 @@ import { RecordTasksCard } from "./record-tasks-card.js";
|
|
|
208
211
|
/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
209
212
|
label: "Owner",
|
|
210
213
|
value: roster.nameOf(deal.ownerUid) || undefined
|
|
214
|
+
}),
|
|
215
|
+
/*#__PURE__*/ _jsx(CrmShareChipView, {
|
|
216
|
+
chip: crmShareChipFor(deal, (_ref4 = (_scope_consentGroup = scope.consentGroup) == null ? void 0 : _scope_consentGroup.hostIds) != null ? _ref4 : []),
|
|
217
|
+
org: org
|
|
211
218
|
})
|
|
212
219
|
]
|
|
213
220
|
}) : null,
|
|
@@ -282,7 +289,7 @@ import { RecordTasksCard } from "./record-tasks-card.js";
|
|
|
282
289
|
scope.orgId ? /*#__PURE__*/ _jsx(DealProductsCard, {
|
|
283
290
|
deal: deal,
|
|
284
291
|
orgId: scope.orgId,
|
|
285
|
-
hostId: (
|
|
292
|
+
hostId: (_ref5 = hostId != null ? hostId : deal.hostId) != null ? _ref5 : null,
|
|
286
293
|
org: org,
|
|
287
294
|
fromCache: fromCache,
|
|
288
295
|
unreadable: status === 'error'
|
|
@@ -294,6 +301,15 @@ import { RecordTasksCard } from "./record-tasks-card.js";
|
|
|
294
301
|
mediaIds: deal.mediaIds,
|
|
295
302
|
topic: "deals"
|
|
296
303
|
}),
|
|
304
|
+
/*#__PURE__*/ _jsx(RecordSharingCard, {
|
|
305
|
+
object: "deals",
|
|
306
|
+
id: deal.$id,
|
|
307
|
+
record: deal,
|
|
308
|
+
hostId: hostId != null ? hostId : null,
|
|
309
|
+
orgId: scope.orgId,
|
|
310
|
+
org: org,
|
|
311
|
+
viewingHostIds: (_scope_consentGroup1 = scope.consentGroup) == null ? void 0 : _scope_consentGroup1.hostIds
|
|
312
|
+
}),
|
|
297
313
|
/*#__PURE__*/ _jsx(RecordActivityCard, {
|
|
298
314
|
hostId: hostId,
|
|
299
315
|
org: org,
|