@aglyn/plugins-crm 1.0.0-beta.157 → 1.0.0-beta.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/assignment-rule-drawer.js +2 -2
  3. package/src/lib/components/assignment-rule-drawer.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +2 -2
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/companies-section.js +2 -2
  7. package/src/lib/components/companies-section.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +2 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/contact-properties-card.js +1 -1
  11. package/src/lib/components/contact-properties-card.js.map +1 -1
  12. package/src/lib/components/contacts-bulk-bar.js +2 -2
  13. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  14. package/src/lib/components/contacts-section.js +2 -1
  15. package/src/lib/components/contacts-section.js.map +1 -1
  16. package/src/lib/components/deal-edit-drawer.js +2 -2
  17. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  18. package/src/lib/components/deals-bulk-bar.js +2 -2
  19. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  20. package/src/lib/components/lead-owner-select.js +2 -2
  21. package/src/lib/components/lead-owner-select.js.map +1 -1
  22. package/src/lib/components/new-contact-drawer.js +2 -2
  23. package/src/lib/components/new-contact-drawer.js.map +1 -1
  24. package/src/lib/components/settings-section.js +10 -4
  25. package/src/lib/components/settings-section.js.map +1 -1
  26. package/src/lib/components/task-edit-drawer.js +2 -2
  27. package/src/lib/components/task-edit-drawer.js.map +1 -1
  28. package/src/lib/components/tasks-bulk-bar.js +2 -2
  29. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {member.label}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","dealHasLineItems","findOrgMember","nameSearchKey","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
34
34
  * one list of stages to offer. The dialog says so rather than offering the
35
35
  * first pipeline's stages to a deal in the second, which the route would
36
36
  * refuse one row at a time.
37
- */ import { CRM_COLLECTIONS, dealStageById } from "@aglyn/aglyn";
37
+ */ import { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from "@aglyn/aglyn";
38
38
  import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
39
39
  import { useFirestore, useHostActivityLogger } from "@aglyn/tenant-feature-instance";
40
40
  import { Button, MenuItem, TextField, Typography } from "@mui/material";
@@ -278,7 +278,7 @@ function DealsBulkBarBody(props) {
278
278
  }),
279
279
  roster.members.map((member)=>/*#__PURE__*/ _jsx(MenuItem, {
280
280
  value: member.uid,
281
- children: member.label
281
+ children: crmMemberPickerLabel(member)
282
282
  }, member.uid))
283
283
  ]
284
284
  }) : null
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-bulk-bar.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 * The bar over the deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LostReasonDialog } from './lost-reason-dialog'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {member.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","kind","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-bulk-bar.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 * The bar over the deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LostReasonDialog } from './lost-reason-dialog'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","crmMemberPickerLabel","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","kind","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
- import { findOrgMember } from "@aglyn/aglyn";
18
+ import { findOrgMember, crmMemberPickerLabel } from "@aglyn/aglyn";
19
19
  import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
20
20
  import { useId } from "react";
21
21
  /** The `value` an owner select carries for "nobody". */ export const UNASSIGNED_OWNER = '';
@@ -78,7 +78,7 @@ import { useId } from "react";
78
78
  }) : null,
79
79
  roster.options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
80
80
  value: option.uid,
81
- children: option.label
81
+ children: crmMemberPickerLabel(option)
82
82
  }, option.uid))
83
83
  ]
84
84
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-owner-select.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 { type AglynOrgMember, findOrgMember } from '@aglyn/aglyn'\nimport { FormControl, InputLabel, MenuItem, Select } from '@mui/material'\nimport { useId } from 'react'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\n\n/** The `value` an owner select carries for \"nobody\". */\nexport const UNASSIGNED_OWNER = ''\n\n/**\n * How a member reads in a picker: the name they set, else their address,\n * else the id — never blank, because a blank option cannot be chosen on\n * purpose.\n */\nexport function orgMemberLabel(\n member: Pick<AglynOrgMember, '$id' | 'displayName' | 'email'>,\n): string {\n return (\n String(member.displayName ?? '').trim() ||\n String(member.email ?? '').trim() ||\n String(member.$id)\n )\n}\n\nexport interface LeadOwnerSelectProps {\n value: string | null | undefined\n onChange: (uid: string) => void\n roster: OrgMemberOptions\n label?: string\n size?: 'small' | 'medium'\n disabled?: boolean\n fullWidth?: boolean\n}\n\n/**\n * The owner picker: Unassigned, then the roster by name.\n *\n * The stored value is resolved to a member first — by uid, or by an address\n * the roster has — so the picker highlights the person and a save writes\n * their uid. A value the roster does not hold is kept as its own option\n * rather than dropped, because a controlled `Select` whose value is absent\n * from its options renders empty — which would show a lead owned by\n * somebody who left as owned by nobody, and a save from that state would\n * make it true.\n */\nexport function LeadOwnerSelect(props: LeadOwnerSelectProps) {\n const {\n value,\n onChange,\n roster,\n label = 'Owner',\n size = 'small',\n disabled,\n fullWidth = true,\n } = props\n const resolved = findOrgMember(roster.options, value)\n const current = resolved?.uid ?? value ?? UNASSIGNED_OWNER\n const known = Boolean(resolved)\n /*\n * A bare `Select` names its combobox after the VALUE it shows, not after\n * the label beside it — MUI builds `aria-labelledby` from `labelId` and the\n * display element, and with no `labelId` only the display is left. A\n * reader hears \"Unassigned\" and has to guess it is the owner; a test can\n * only find the control by what it happens to say. The label's id is what\n * makes \"Owner\" the control's name.\n */\n const labelId = useId()\n return (\n <FormControl size={size} fullWidth={fullWidth} disabled={disabled}>\n <InputLabel id={labelId}>{label}</InputLabel>\n <Select\n labelId={labelId}\n label={label}\n value={current}\n onChange={(event) => onChange(String(event.target.value))}\n >\n <MenuItem value={UNASSIGNED_OWNER}>\n <em>{'Unassigned'}</em>\n </MenuItem>\n {current && !known ? (\n <MenuItem value={current}>{roster.labelFor(current)}</MenuItem>\n ) : null}\n {roster.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {option.label}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n )\n}\nLeadOwnerSelect.displayName = 'LeadOwnerSelect'\n\nexport default LeadOwnerSelect\n"],"names":["findOrgMember","FormControl","InputLabel","MenuItem","Select","useId","UNASSIGNED_OWNER","orgMemberLabel","member","String","displayName","trim","email","$id","LeadOwnerSelect","props","resolved","value","onChange","roster","label","size","disabled","fullWidth","options","current","uid","known","Boolean","labelId","id","event","target","em","labelFor","map","option"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAA8BA,aAAa,QAAQ,eAAc;AACjE,SAASC,WAAW,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,QAAQ,gBAAe;AACzE,SAASC,KAAK,QAAQ,QAAO;AAG7B,sDAAsD,GACtD,OAAO,MAAMC,mBAAmB,GAAE;AAElC;;;;CAIC,GACD,OAAO,SAASC,eACdC,MAA6D;QAGpDA,qBACAA;IAFT,OACEC,QAAOD,sBAAAA,OAAOE,WAAW,YAAlBF,sBAAsB,IAAIG,IAAI,MACrCF,QAAOD,gBAAAA,OAAOI,KAAK,YAAZJ,gBAAgB,IAAIG,IAAI,MAC/BF,OAAOD,OAAOK,GAAG;AAErB;AAYA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;QAWzCC;IAVhB,MAAM,EACJC,KAAK,EACLC,QAAQ,EACRC,MAAM,EACNC,QAAQ,OAAO,EACfC,OAAO,OAAO,EACdC,QAAQ,EACRC,YAAY,IAAI,EACjB,GAAGR;IACJ,MAAMC,WAAWhB,cAAcmB,OAAOK,OAAO,EAAEP;IAC/C,MAAMQ,WAAUT,gBAAAA,4BAAAA,SAAUU,GAAG,oBAAIT,iBAAjBD,OAA0BV;IAC1C,MAAMqB,QAAQC,QAAQZ;IACtB;;;;;;;GAOC,GACD,MAAMa,UAAUxB;IAChB,qBACE,MAACJ;QAAYoB,MAAMA;QAAME,WAAWA;QAAWD,UAAUA;;0BACvD,KAACpB;gBAAW4B,IAAID;0BAAUT;;0BAC1B,MAAChB;gBACCyB,SAASA;gBACTT,OAAOA;gBACPH,OAAOQ;gBACPP,UAAU,CAACa,QAAUb,SAAST,OAAOsB,MAAMC,MAAM,CAACf,KAAK;;kCAEvD,KAACd;wBAASc,OAAOX;kCACf,cAAA,KAAC2B;sCAAI;;;oBAENR,WAAW,CAACE,sBACX,KAACxB;wBAASc,OAAOQ;kCAAUN,OAAOe,QAAQ,CAACT;yBACzC;oBACHN,OAAOK,OAAO,CAACW,GAAG,CAAC,CAACC,uBACnB,KAACjC;4BAA0Bc,OAAOmB,OAAOV,GAAG;sCACzCU,OAAOhB,KAAK;2BADAgB,OAAOV,GAAG;;;;;AAOnC;AACAZ,gBAAgBJ,WAAW,GAAG;AAE9B,eAAeI,gBAAe"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-owner-select.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 { type AglynOrgMember, findOrgMember, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { FormControl, InputLabel, MenuItem, Select } from '@mui/material'\nimport { useId } from 'react'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\n\n/** The `value` an owner select carries for \"nobody\". */\nexport const UNASSIGNED_OWNER = ''\n\n/**\n * How a member reads in a picker: the name they set, else their address,\n * else the id — never blank, because a blank option cannot be chosen on\n * purpose.\n */\nexport function orgMemberLabel(\n member: Pick<AglynOrgMember, '$id' | 'displayName' | 'email'>,\n): string {\n return (\n String(member.displayName ?? '').trim() ||\n String(member.email ?? '').trim() ||\n String(member.$id)\n )\n}\n\nexport interface LeadOwnerSelectProps {\n value: string | null | undefined\n onChange: (uid: string) => void\n roster: OrgMemberOptions\n label?: string\n size?: 'small' | 'medium'\n disabled?: boolean\n fullWidth?: boolean\n}\n\n/**\n * The owner picker: Unassigned, then the roster by name.\n *\n * The stored value is resolved to a member first — by uid, or by an address\n * the roster has — so the picker highlights the person and a save writes\n * their uid. A value the roster does not hold is kept as its own option\n * rather than dropped, because a controlled `Select` whose value is absent\n * from its options renders empty — which would show a lead owned by\n * somebody who left as owned by nobody, and a save from that state would\n * make it true.\n */\nexport function LeadOwnerSelect(props: LeadOwnerSelectProps) {\n const {\n value,\n onChange,\n roster,\n label = 'Owner',\n size = 'small',\n disabled,\n fullWidth = true,\n } = props\n const resolved = findOrgMember(roster.options, value)\n const current = resolved?.uid ?? value ?? UNASSIGNED_OWNER\n const known = Boolean(resolved)\n /*\n * A bare `Select` names its combobox after the VALUE it shows, not after\n * the label beside it — MUI builds `aria-labelledby` from `labelId` and the\n * display element, and with no `labelId` only the display is left. A\n * reader hears \"Unassigned\" and has to guess it is the owner; a test can\n * only find the control by what it happens to say. The label's id is what\n * makes \"Owner\" the control's name.\n */\n const labelId = useId()\n return (\n <FormControl size={size} fullWidth={fullWidth} disabled={disabled}>\n <InputLabel id={labelId}>{label}</InputLabel>\n <Select\n labelId={labelId}\n label={label}\n value={current}\n onChange={(event) => onChange(String(event.target.value))}\n >\n <MenuItem value={UNASSIGNED_OWNER}>\n <em>{'Unassigned'}</em>\n </MenuItem>\n {current && !known ? (\n <MenuItem value={current}>{roster.labelFor(current)}</MenuItem>\n ) : null}\n {roster.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {crmMemberPickerLabel(option)}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n )\n}\nLeadOwnerSelect.displayName = 'LeadOwnerSelect'\n\nexport default LeadOwnerSelect\n"],"names":["findOrgMember","crmMemberPickerLabel","FormControl","InputLabel","MenuItem","Select","useId","UNASSIGNED_OWNER","orgMemberLabel","member","String","displayName","trim","email","$id","LeadOwnerSelect","props","resolved","value","onChange","roster","label","size","disabled","fullWidth","options","current","uid","known","Boolean","labelId","id","event","target","em","labelFor","map","option"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAA8BA,aAAa,EAAEC,oBAAoB,QAAQ,eAAc;AACvF,SAASC,WAAW,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,QAAQ,gBAAe;AACzE,SAASC,KAAK,QAAQ,QAAO;AAG7B,sDAAsD,GACtD,OAAO,MAAMC,mBAAmB,GAAE;AAElC;;;;CAIC,GACD,OAAO,SAASC,eACdC,MAA6D;QAGpDA,qBACAA;IAFT,OACEC,QAAOD,sBAAAA,OAAOE,WAAW,YAAlBF,sBAAsB,IAAIG,IAAI,MACrCF,QAAOD,gBAAAA,OAAOI,KAAK,YAAZJ,gBAAgB,IAAIG,IAAI,MAC/BF,OAAOD,OAAOK,GAAG;AAErB;AAYA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;QAWzCC;IAVhB,MAAM,EACJC,KAAK,EACLC,QAAQ,EACRC,MAAM,EACNC,QAAQ,OAAO,EACfC,OAAO,OAAO,EACdC,QAAQ,EACRC,YAAY,IAAI,EACjB,GAAGR;IACJ,MAAMC,WAAWjB,cAAcoB,OAAOK,OAAO,EAAEP;IAC/C,MAAMQ,WAAUT,gBAAAA,4BAAAA,SAAUU,GAAG,oBAAIT,iBAAjBD,OAA0BV;IAC1C,MAAMqB,QAAQC,QAAQZ;IACtB;;;;;;;GAOC,GACD,MAAMa,UAAUxB;IAChB,qBACE,MAACJ;QAAYoB,MAAMA;QAAME,WAAWA;QAAWD,UAAUA;;0BACvD,KAACpB;gBAAW4B,IAAID;0BAAUT;;0BAC1B,MAAChB;gBACCyB,SAASA;gBACTT,OAAOA;gBACPH,OAAOQ;gBACPP,UAAU,CAACa,QAAUb,SAAST,OAAOsB,MAAMC,MAAM,CAACf,KAAK;;kCAEvD,KAACd;wBAASc,OAAOX;kCACf,cAAA,KAAC2B;sCAAI;;;oBAENR,WAAW,CAACE,sBACX,KAACxB;wBAASc,OAAOQ;kCAAUN,OAAOe,QAAQ,CAACT;yBACzC;oBACHN,OAAOK,OAAO,CAACW,GAAG,CAAC,CAACC,uBACnB,KAACjC;4BAA0Bc,OAAOmB,OAAOV,GAAG;sCACzC1B,qBAAqBoC;2BADTA,OAAOV,GAAG;;;;;AAOnC;AACAZ,gBAAgBJ,WAAW,GAAG;AAE9B,eAAeI,gBAAe"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
- import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, consentGroupDisclosure, normalizeAddress, normalizeContactEmail, normalizePhone } from "@aglyn/aglyn";
18
+ import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, consentGroupDisclosure, normalizeAddress, normalizeContactEmail, normalizePhone, crmMemberPickerLabel } from "@aglyn/aglyn";
19
19
  import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
20
20
  import { Container, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
21
21
  import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
@@ -289,7 +289,7 @@ import { CrmSitePicker } from "./crm-site-picker.js";
289
289
  }),
290
290
  owners.map((owner)=>/*#__PURE__*/ _jsx(MenuItem, {
291
291
  value: owner.uid,
292
- children: owner.label
292
+ children: crmMemberPickerLabel(owner)
293
293
  }, owner.uid))
294
294
  ]
295
295
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n email: string\n name: string\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site\n * with a picker and holds its submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n if (!normalizedEmail || (trimmedPhone && !normalizedPhone)) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/*\n First, because everything below it is filed under the answer: the\n site decides which of the org's sites may see the person and whose\n consent the checkbox at the foot records. Renders nothing under a\n site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — it is what makes two captures one person.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {owner.label}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {(disclosure ? `${disclosure} ` : '') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the capturing site is known: the route resolves the\n // org from it and stamps the record with it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","normalizeAddress","normalizeContactEmail","normalizePhone","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","consentGroup","companies","enabled","createCompany","email","setEmail","name","setName","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","handleSubmit","normalizedEmail","trimmedPhone","trim","normalizedPhone","slice","companyName","companyId","id","disclosure","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder","control","checked"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n email: string\n name: string\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site\n * with a picker and holds its submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n if (!normalizedEmail || (trimmedPhone && !normalizedPhone)) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/*\n First, because everything below it is filed under the answer: the\n site decides which of the org's sites may see the person and whose\n consent the checkbox at the foot records. Renders nothing under a\n site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — it is what makes two captures one person.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {(disclosure ? `${disclosure} ` : '') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the capturing site is known: the route resolves the\n // org from it and stamps the record with it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","normalizeAddress","normalizeContactEmail","normalizePhone","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","consentGroup","companies","enabled","createCompany","email","setEmail","name","setName","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","handleSubmit","normalizedEmail","trimmedPhone","trim","normalizedPhone","slice","companyName","companyId","id","disclosure","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder","control","checked"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,8BAA8B,EAC9BC,wBAAwB,EACxBC,sBAAsB,EAKtBC,gBAAgB,EAChBC,qBAAqB,EACrBC,cAAc,EACdC,oBAAoB,QACf,eAAc;AACrB,SAASC,kBAAkB,QAAQ,2BAA0B;AAC7D,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACjE,SAASC,yBAAyB,QAAQ,8DAA6D;AACvG,SACEC,KAAK,EACLC,MAAM,EACNC,QAAQ,EACRC,gBAAgB,EAChBC,cAAc,EACdC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,gBAAgB,QAAQ,6BAAyB;AAC1D,SACEC,aAAa,EAEbC,iBAAiB,EACjBC,gBAAgB,QACX,sBAAkB;AACzB,SACEC,oBAAoB,EACpBC,aAAa,QAER,8BAA0B;AACjC,SAASC,aAAa,QAAQ,uBAAmB;AA2CjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,SAASC,iBAAiBC,KAA4B;;IAC3D,MAAM,EACJC,IAAI,EACJC,OAAO,EACPC,MAAM,EACNC,GAAG,EACHC,IAAI,EACJC,KAAK,EACLC,MAAM,EACNC,WAAW,EACXC,QAAQ,EACT,GAAGT;IACJ;;;;;GAKC,GACD,MAAM,EAAEU,YAAY,EAAEC,WAAW,EAAE,GAAGpB,YAAY;QAAEY;QAAQC;IAAI;IAChE,MAAMQ,eAAoCD;IAC1C,MAAME,YAAYnB,kBAAkB;QAAES;QAAQC;QAAKU,SAASb;IAAK;IACjE,MAAMc,gBAAgBpB,iBAAiB;QAAEQ;QAAQC;IAAI;IAErD,MAAM,CAACY,OAAOC,SAAS,GAAG3B,SAAS;IACnC,MAAM,CAAC4B,MAAMC,QAAQ,GAAG7B,SAAS;IACjC,MAAM,CAAC8B,OAAOC,SAAS,GAAG/B,SAAS;IACnC,MAAM,CAACgC,UAAUC,YAAY,GAAGjC,SAAS;IACzC,MAAM,CAACkC,SAASC,WAAW,GAAGnC,SAA+B;IAC7D,MAAM,CAACoC,UAAUC,YAAY,GAAGrC,SAAS;IACzC,MAAM,CAACsC,gBAAgBC,kBAAkB,GAAGvC,SAE1C;IACF,MAAM,CAACwC,MAAMC,QAAQ,GAAGzC,SAAS;IACjC,MAAM,CAAC0C,SAASC,WAAW,GAAG3C,SAAuBO;IACrD,MAAM,CAACqC,kBAAkBC,oBAAoB,GAAG7C,SAAS;IACzD,MAAM,CAAC8C,YAAYC,cAAc,GAAG/C,SAAS;IAC7C,MAAM,CAACgD,YAAYC,cAAc,GAAGjD,SAAS;IAE7C;;;;GAIC,GACDD,UAAU;QACR,IAAI,CAACY,MAAM;QACXgB,SAAS;QACTE,QAAQ;QACRE,SAAS;QACTE,YAAY;QACZE,WAAW;QACXE,YAAY;QACZE,kBAAkB;QAClBE,QAAQ;QACRE,WAAWpC;QACXsC,oBAAoB;QACpBE,cAAc;QACdE,cAAc;IAChB,GAAG;QAACtC;KAAK;IAET,MAAMuC,eAAe;;QACnB,MAAMC,kBAAkBtE,sBAAsB6C;QAC9C,MAAM0B,eAAetB,MAAMuB,IAAI;QAC/B,MAAMC,kBAAkBF,eAAetE,eAAesE,gBAAgB;QACtEL,cAAcI,kBAAkB,KAAK;QACrCF,cACEG,gBAAgB,CAACE,kBACb,0DACA;QAEN,IAAI,CAACH,mBAAoBC,gBAAgB,CAACE,iBAAkB;QAC5DnC,SAAS;YACPO,OAAOyB;YACPvB,MAAMA,KAAKyB,IAAI,GAAGE,KAAK,CAAC,GAAG;YAC3BzB,KAAK,EAAEwB,0BAAAA,kBAAmB;YAC1BtB,UAAUA,SAASqB,IAAI,GAAGE,KAAK,CAAC,GAAG;YACnCC,aAAatB,UAAUA,QAAQN,IAAI,CAACyB,IAAI,GAAGE,KAAK,CAAC,GAAG,OAAO;YAC3DE,SAAS,UAAEvB,2BAAAA,QAASwB,EAAE,mBAAI;YAC1BtB;YACAE;YACAE,MAAMtC,iBAAiBsC;YACvBE,SAAS9D,iBAAiB8D;YAC1BE;QACF;IACF;IAEA,MAAMe,aAAarC,eAAe3C,uBAAuB2C,gBAAgB;IAEzE,qBACE,KAAClC;QACCuB,MAAMA;QACNiD,QAAO;QACPC,SAAQ;QACRjD,SAASA;QACTkD,aAAa;YAAEC,OAAO;QAAU;QAChCC,IAAI;YAAE,sBAAsB;gBAAEC,OAAO;oBAAEC,IAAI;oBAAQC,IAAI;gBAAI;YAAE;QAAE;QAC/DC,0BACE;;8BACE,MAAC1E;oBACCqE,OAAM;oBACNM,MAAK;oBACLC,SAAS1D;oBACToD,IAAI;wBAAEO,IAAI;oBAAE;;sCAEZ,KAACrF;4BAAQsF,MAAMxF,mBAAmBwF,IAAI;;sCACtC,KAACrF;sCAAO;;;;8BAEV,KAACW;oBAAW+D,SAAQ;oBAAKY,WAAU;8BAChC;;;;QAIPC,2BACE,KAACpF;YAAOuE,SAAQ;YAAWE,OAAM;YAAUO,SAAS1D;sBACjD;;kBAIL,cAAA,KAAC3B;YAAU0F,OAAO;sBAChB,cAAA,MAAC/E;gBAAMgF,SAAS;;oBACb5D,sBAAQ,KAAC3B;wBAAMwF,UAAS;kCAAW7D;yBAAiB;kCAOrD,KAACR;wBACCK,QAAQA;wBACRiE,UAAUC,QAAQhE;wBAClBiE,YAAW;;kCAEb,KAACnF;wBACCoF,MAAK;wBACLC,OAAM;wBACNC,MAAK;wBACLC,OAAO1D;wBACP2D,UAAU,CAACC,QAAU3D,SAAS2D,MAAMC,MAAM,CAACH,KAAK;wBAChDpE,OAAO+D,QAAQjC;wBACfkC,YACElC,cACA;wBAEF0C,QAAQ;wBACRC,SAAS;wBACTC,SAAS;;kCAEX,KAAC7F;wBACCoF,MAAK;wBACLC,OAAM;wBACNE,OAAOxD;wBACPyD,UAAU,CAACC,QAAUzD,QAAQyD,MAAMC,MAAM,CAACH,KAAK;wBAC/CO,WAAW;4BAAEC,WAAW;gCAAEC,WAAW;4BAAI;wBAAE;wBAC3CJ,SAAS;;kCAEX,KAAC5F;wBACCoF,MAAK;wBACLC,OAAM;wBACNE,OAAOtD;wBACPuD,UAAU,CAACC,QAAUvD,SAASuD,MAAMC,MAAM,CAACH,KAAK;wBAChDpE,OAAO+D,QAAQ/B;wBACfgC,YAAYhC,cAAc;wBAC1ByC,SAAS;;kCAEX,KAAC5F;wBACCoF,MAAK;wBACLC,OAAM;wBACNE,OAAOpD;wBACPqD,UAAU,CAACC,QAAUrD,YAAYqD,MAAMC,MAAM,CAACH,KAAK;wBACnDO,WAAW;4BAAEC,WAAW;gCAAEC,WAAW;4BAAI;wBAAE;wBAC3CJ,SAAS;;kCAEX,KAACtF;wBACC2F,SAASvE,UAAUuE,OAAO;wBAC1BC,OAAOxE,UAAUwE,KAAK;wBACtBC,WAAWzE,UAAUyE,SAAS;wBAC9BZ,KAAK,UAAElD,2BAAAA,QAASwB,EAAE,mBAAI;wBACtB2B,UAAU,CAACY,KAAKC,SAAW/D,WAAW+D;wBACtCC,UAAU1E;wBACVC,OAAOA;wBACPoD,UAAUC,QAAQhE;wBAClBiE,YAAW;;kCAEb,MAACnF;wBACCuG,MAAM;wBACNnB,MAAK;wBACLC,OAAM;wBACNE,OAAO9C;wBACP+C,UAAU,CAACC,QACT/C,kBAAkB+C,MAAMC,MAAM,CAACH,KAAK;wBAEtCK,SAAS;;0CAET,KAAC9F;gCAASyF,OAAM;0CAAI;;4BACnB1G,yBAAyB2H,GAAG,CAAC,CAACC,sBAC7B,KAAC3G;oCAAqByF,OAAOkB;8CAC1B7H,8BAA8B,CAAC6H,MAAM;mCADzBA;;;kCAKnB,MAACzG;wBACCuG,MAAM;wBACNnB,MAAK;wBACLC,OAAM;wBACNE,OAAOhD;wBACPiD,UAAU,CAACC,QAAUjD,YAAYiD,MAAMC,MAAM,CAACH,KAAK;wBACnDJ,YACE9D,eAAe,CAACD,OAAOsF,MAAM,GACzB,oEACA;wBAENd,SAAS;;0CAET,KAAC9F;gCAASyF,OAAM;0CAAI;;4BACnBnE,OAAOoF,GAAG,CAAC,CAACG,sBACX,KAAC7G;oCAAyByF,OAAOoB,MAAMC,GAAG;8CACvC1H,qBAAqByH;mCADTA,MAAMC,GAAG;;;kCAK5B,KAAC5G;wBACCoF,MAAK;wBACLC,OAAM;wBACNwB,aAAY;wBACZ1B,YAAW;wBACXI,OAAO5C;wBACP6C,UAAU,CAACC,QAAU7C,QAAQ6C,MAAMC,MAAM,CAACH,KAAK;wBAC/CK,SAAS;;kCAEX,KAAC3F;wBAAW+D,SAAQ;kCAAa;;kCACjC,KAACvD;wBAAqB8E,OAAO1C;wBAAS2C,UAAU1C;;kCAChD,MAAC/C;;0CACC,KAACJ;gCACCmH,uBACE,KAACpH;oCACCqH,SAAShE;oCACTyC,UAAU,CAACC,QAAUzC,oBAAoByC,MAAMC,MAAM,CAACqB,OAAO;;gCAGjE1B,OAAM;;0CAER,KAACzF;0CACE,AAACkE,CAAAA,aAAa,GAAGA,WAAW,CAAC,CAAC,GAAG,EAAC,IACjC;;;;kCAGN,KAACrE;wBACCuE,SAAQ;wBACRE,OAAM;wBACN,iEAAiE;wBACjE,6CAA6C;wBAC7Ce,UAAUC,QAAQhE,SAAS,CAACK;wBAC5BkD,SAASpB;kCAERnC,OAAO,YAAY;;;;;;AAMhC;AAEA,eAAeN,iBAAgB"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
- import { canManageOrg, CRM_ASSIGNMENT_RULES_MAX, CRM_ASSIGNMENT_RULES_PATH, CRM_AUTO_CREATE_COMPANIES_PATH, CRM_ROUND_ROBIN_POOL_MAX, CRM_ROUND_ROBIN_POOL_PATH, crmHostDefaultOwnerSegments, describeAssignmentRule, orgAutoCreatesCompanies, pluginDocsHelp, readCrmAssignmentSettings, roundRobinOrder } from "@aglyn/aglyn";
18
+ import { canManageOrg, CRM_ASSIGNMENT_RULES_MAX, CRM_ASSIGNMENT_RULES_PATH, CRM_AUTO_CREATE_COMPANIES_PATH, CRM_ROUND_ROBIN_POOL_MAX, CRM_ROUND_ROBIN_POOL_PATH, crmHostDefaultOwnerSegments, describeAssignmentRule, orgAutoCreatesCompanies, pluginDocsHelp, readCrmAssignmentSettings, roundRobinOrder, crmMemberPickerLabel } from "@aglyn/aglyn";
19
19
  import { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from "@aglyn/shared-data-mdi";
20
20
  import { CardDisplay, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
21
21
  import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
@@ -322,7 +322,7 @@ DefaultOwnerCard.displayName = 'DefaultOwnerCard';
322
322
  }),
323
323
  options.map((member)=>/*#__PURE__*/ _jsx(MenuItem, {
324
324
  value: member.uid,
325
- children: member.label
325
+ children: crmMemberPickerLabel(member)
326
326
  }, member.uid))
327
327
  ]
328
328
  });
@@ -651,6 +651,12 @@ AssignmentRulesCard.displayName = 'AssignmentRulesCard';
651
651
  const canEdit = ready && canManage && !busy && !roster.loading;
652
652
  const order = roundRobinOrder(pool.memberUids, pool.lastAssignedUid);
653
653
  const nextUp = order[0];
654
+ // The rotation names people the way the checkboxes above do — with the
655
+ // address, since two members can share a display name.
656
+ const rotationName = (uid)=>{
657
+ const member = roster.members.find((row)=>row.uid === uid);
658
+ return member ? crmMemberPickerLabel(member) : roster.nameOf(uid);
659
+ };
654
660
  return /*#__PURE__*/ _jsx(CardDisplay, {
655
661
  header: 'Round robin',
656
662
  help: pluginDocsHelp('crmSettings', {
@@ -682,13 +688,13 @@ AssignmentRulesCard.displayName = 'AssignmentRulesCard';
682
688
  disabled: !canEdit,
683
689
  onChange: (event)=>void toggle(member.uid, event.target.checked)
684
690
  }),
685
- label: member.label
691
+ label: crmMemberPickerLabel(member)
686
692
  }, member.uid))
687
693
  }),
688
694
  /*#__PURE__*/ _jsx(Typography, {
689
695
  variant: "caption",
690
696
  color: "text.secondary",
691
- children: pool.memberUids.length === 0 ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.' : `Rotation: ${pool.memberUids.map(roster.nameOf).join(' → ')}. Next up: ${roster.nameOf(nextUp)}.`
697
+ children: pool.memberUids.length === 0 ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.' : `Rotation: ${pool.memberUids.map(rotationName).join(' → ')}. Next up: ${rotationName(nextUp)}.`
692
698
  }),
693
699
  /*#__PURE__*/ _jsx(ManagersOnlyNote, {
694
700
  ready: ready,