@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +14 -2
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +115 -163
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +59 -84
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +11 -3
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +128 -121
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +49 -43
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-list-columns.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 activityTimeLabel,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n EMAIL_STATE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn'\nimport {\n hiddenFilterColumns,\n listFilterColumn,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n} from '../constants/contact-filters'\nimport type { ContactRecord } from '../model/contact-record'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { nextActivityColumn } from './crm-next-activity-column'\n\n/**\n * The filterable fields that get a column. The rest of\n * `CONTACT_LIST_FILTER_FIELDS` still reaches the filter panel, hidden.\n */\n/**\n * The declared filter fields that ARE table columns, so the hidden\n * filter-only columns are built for the others alone. Owner and Stage\n * joined it when the grammar learned them (AGL-2617): each is a shown\n * column already, and a second, hidden one under the same field would be\n * a duplicate the grid refuses.\n */\nexport const CONTACT_FILTER_COLUMNS = [\n 'name',\n 'ownerUid',\n 'lifecycleStage',\n 'sources',\n 'tags',\n 'updatedAt',\n 'nextTaskAtMs',\n 'emailState',\n]\n\n/**\n * The columns that ship HIDDEN, for the grid's visibility model (AGL-2616).\n *\n * \"Last engaged\" is a fact most lists do not need on screen and every list\n * can turn on from the column menu; shipping it visible would widen a\n * table that already carries seven columns for a figure only a marketing\n * reader scans.\n */\nexport const CONTACT_OPTIONAL_COLUMNS = ['lastEmailEngagementAtMs']\n\nexport interface ContactListColumnOptions {\n /** The owner's name for a uid — the roster's, or the uid itself. */\n memberName: (uid: string) => string\n /**\n * How a site reads, by document id — handed in at the ORGANIZATION level\n * only (AGL-2630), where it turns on the \"Known by\" column: the sites\n * that have captured each person. Absent under a site, where every row\n * is known by the site and the column would say so on every line.\n */\n siteName?: (hostId: string) => string\n /**\n * One clock for every row of one paint, so two contacts engaged a second\n * apart cannot read \"just now\" and \"1 min ago\". Defaults to the paint's\n * own `Date.now()`.\n */\n nowMs?: number\n}\n\n/**\n * The contacts list's columns (AGL-2596), in their own module so the list\n * file stays about the QUERY and the toolbar while other surfaces — a bulk\n * bar, an import — grow beside it.\n *\n * Owner and Stage read off the flattened row, which is already this\n * holder's facet (`contactRecordFromDoc`), so neither column can show\n * another holder's assignment. Both are `filterable: false` on purpose: the\n * grid's filter panel translates to a Firestore query over the top-level\n * fields in `CONTACT_LIST_FILTER_FIELDS`, and a facet path is not one of\n * them. The toolbar's stage select and \"Assigned to me\" switch narrow the\n * loaded window instead, and say so.\n */\nexport function contactListColumns(\n options: ContactListColumnOptions,\n): GridColDef[] {\n const { memberName, siteName } = options\n const nowMs = options.nowMs ?? Date.now()\n return [\n {\n field: 'name',\n headerName: 'Contact',\n flex: 1.6,\n minWidth: 220,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: ContactRecord) =>\n String(row.name || row.email || ''),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name || row.email}\n </Typography>\n {row.name ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.ownerUid ? memberName(row.ownerUid) : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.ownerUid ? (\n <Typography variant=\"body2\" noWrap>\n {memberName(row.ownerUid)}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'lifecycleStage',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 150,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lifecycleStage ? CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lifecycleStage ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage]}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n /*\n * KNOWN BY (AGL-2630): the sites that have captured this person, which\n * is the cross-site fact the organization-level list exists to show —\n * one deduplicated row per person, and which of the org's sites know\n * them. `capturedByHostIds` is a top-level array precisely so the\n * question can be answered without opening a facet. Not filterable from\n * the panel: an `array-contains` on it would displace the scope clause\n * under a site, and at the org level a saved view narrows the window\n * instead. A row that names no site predates attribution and says so,\n * rather than reading as \"every site\".\n */\n ...(siteName\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Known by',\n flex: 1.1,\n minWidth: 200,\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: ContactRecord) =>\n row.capturedByHostIds.map(siteName).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.capturedByHostIds.length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%', flexWrap: 'nowrap' }}\n >\n {row.capturedByHostIds.slice(0, 2).map((hostId) => (\n <Chip key={hostId} size=\"small\" label={siteName(hostId)} />\n ))}\n {row.capturedByHostIds.length > 2 ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={`+${row.capturedByHostIds.length - 2}`}\n />\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n ),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'sources',\n headerName: 'Sources',\n flex: 1,\n minWidth: 160,\n // A map of provenance flags, not a scalar — `sources.form == true` is\n // queryable one key at a time, which is a menu of its own rather than\n // a filter on this column.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: ContactRecord) =>\n Object.keys(row.sources ?? {}).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n {Object.keys(row.sources ?? {}).map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source as ContactSource] ?? source}\n size=\"small\"\n />\n ))}\n </Stack>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 150,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'tags'),\n sortable: false,\n valueGetter: (_value, row: ContactRecord) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n (row.tags ?? []).slice(0, 3).join(', '),\n },\n {\n field: 'updatedAt',\n headerName: 'Last activity',\n flex: 0.8,\n minWidth: 150,\n // `type: 'date'` is what gives the panel a date PICKER rather than a\n // free-text box for a value the query reads as a day.\n type: 'date',\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'updatedAt'),\n valueGetter: (_value, row: ContactRecord) => {\n const seconds = (row.updatedAt as { seconds?: number } | undefined)?.seconds\n return seconds ? new Date(seconds * 1000) : null\n },\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.interactions?.[0]\n ? new Date(row.interactions[0].atMs).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the person is due (AGL-2661).\n nextActivityColumn(nowMs),\n {\n /*\n * When the person last opened or clicked one of this holder's\n * campaigns (AGL-2616). Off the facet like Owner and Stage, so it can\n * never show another holder's readers; `filterable: false` for the\n * same reason theirs are — a facet path is not a query the panel can\n * make. A date column, so a sort on it orders by the instant and an\n * export gets a date; the cell prints the relative form the timeline\n * uses and carries the full stamp in its tooltip.\n */\n field: 'lastEmailEngagementAtMs',\n headerName: 'Last engaged',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lastEmailEngagementAtMs ? new Date(row.lastEmailEngagementAtMs) : null,\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(row.lastEmailEngagementAtMs).toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {activityTimeLabel(row.lastEmailEngagementAtMs, nowMs)}\n </Typography>\n </Tooltip>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the same chip the record page\n * carries, so a bounced or blocked person is told apart in the list a\n * member picks recipients from. Sorted and exported by its label;\n * filtered through the bar's `emailState` clause, not the panel.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.emailState ? EMAIL_STATE_LABELS[row.emailState.status] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.emailState ? (\n <CrmEmailStateChip state={row.emailState} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n ...hiddenFilterColumns(\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_FILTER_COLUMNS,\n CONTACT_LIST_FILTER_HEADERS,\n ),\n ]\n}\n"],"names":["activityTimeLabel","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","EMAIL_STATE_LABELS","hiddenFilterColumns","listFilterColumn","Chip","Stack","Tooltip","Typography","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CrmEmailStateChip","nextActivityColumn","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","options","memberName","siteName","nowMs","Date","now","field","headerName","flex","minWidth","valueGetter","_value","row","String","name","email","renderCell","sx","justifyContent","height","lineHeight","variant","color","noWrap","filterable","ownerUid","lifecycleStage","size","label","sortable","capturedByHostIds","map","join","length","direction","spacing","alignItems","flexWrap","slice","hostId","Object","keys","sources","source","tags","type","seconds","updatedAt","interactions","atMs","toLocaleDateString","lastEmailEngagementAtMs","title","toLocaleString","emailState","status","state"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,iBAAiB,EACjBC,8BAA8B,EAC9BC,qBAAqB,EACrBC,kBAAkB,QAEb,eAAc;AACrB,SACEC,mBAAmB,EACnBC,gBAAgB,QACX,yCAAwC;AAC/C,SAASC,IAAI,EAAEC,KAAK,EAAEC,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEhE,SACEC,0BAA0B,EAC1BC,2BAA2B,QACtB,kCAA8B;AAErC,SAASC,iBAAiB,QAAQ,4BAAwB;AAC1D,SAASC,kBAAkB,QAAQ,gCAA4B;AAE/D;;;CAGC,GACD;;;;;;CAMC,GACD,OAAO,MAAMC,yBAAyB;IACpC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA2B;IAAC;CAA0B,CAAA;AAoBnE;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,mBACdC,OAAiC;QAGnBA;IADd,MAAM,EAAEC,UAAU,EAAEC,QAAQ,EAAE,GAAGF;IACjC,MAAMG,SAAQH,iBAAAA,QAAQG,KAAK,YAAbH,iBAAiBI,KAAKC,GAAG;IACvC,OAAO;QACL;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;WACPrB,iBAAiBK,4BAA4B;YAChDiB,aAAa,CAACC,QAAQC,MACpBC,OAAOD,IAAIE,IAAI,IAAIF,IAAIG,KAAK,IAAI;YAClCC,YAAY,CAAC,EAAEJ,GAAG,EAA0B,iBAC1C,MAACtB;oBAAM2B,IAAI;wBAAEC,gBAAgB;wBAAUC,QAAQ;wBAAQC,YAAY;oBAAK;;sCACtE,KAAC5B;4BAAW6B,SAAQ;4BAAQJ,IAAI;gCAAEG,YAAY;4BAAK;sCAChDR,IAAIE,IAAI,IAAIF,IAAIG,KAAK;;wBAEvBH,IAAIE,IAAI,iBACP,KAACtB;4BACC6B,SAAQ;4BACRC,OAAM;4BACNL,IAAI;gCAAEG,YAAY;4BAAK;4BACvBG,MAAM;sCAELX,IAAIG,KAAK;6BAEV;;;;QAIV;YACET,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVe,YAAY;YACZd,aAAa,CAACC,QAAQC,MACpBA,IAAIa,QAAQ,GAAGxB,WAAWW,IAAIa,QAAQ,IAAI;YAC5CT,YAAY,CAAC,EAAEJ,GAAG,EAA0B,GAC1CA,IAAIa,QAAQ,iBACV,KAACjC;oBAAW6B,SAAQ;oBAAQE,MAAM;8BAC/BtB,WAAWW,IAAIa,QAAQ;mCAG1B,KAACjC;oBAAW6B,SAAQ;oBAAUC,OAAM;8BACjC;;QAGT;QACA;YACEhB,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVe,YAAY;YACZd,aAAa,CAACC,QAAQC,MACpBA,IAAIc,cAAc,GAAG1C,8BAA8B,CAAC4B,IAAIc,cAAc,CAAC,GAAG;YAC5EV,YAAY,CAAC,EAAEJ,GAAG,EAA0B,GAC1CA,IAAIc,cAAc,iBAChB,KAACrC;oBACCsC,MAAK;oBACLN,SAAQ;oBACRO,OAAO5C,8BAA8B,CAAC4B,IAAIc,cAAc,CAAC;mCAG3D,KAAClC;oBAAW6B,SAAQ;oBAAUC,OAAM;8BACjC;;QAGT;QACA;;;;;;;;;;KAUC,MACGpB,WACA;YACE;gBACEI,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVoB,UAAU;gBACVL,YAAY;gBACZd,aAAa,CAACC,QAAiBC,MAC7BA,IAAIkB,iBAAiB,CAACC,GAAG,CAAC7B,UAAU8B,IAAI,CAAC;gBAC3ChB,YAAY,CAAC,EAAEJ,GAAG,EAA0B,GAC1CA,IAAIkB,iBAAiB,CAACG,MAAM,iBAC1B,MAAC3C;wBACC4C,WAAU;wBACVC,SAAS;wBACTlB,IAAI;4BAAEmB,YAAY;4BAAUjB,QAAQ;4BAAQkB,UAAU;wBAAS;;4BAE9DzB,IAAIkB,iBAAiB,CAACQ,KAAK,CAAC,GAAG,GAAGP,GAAG,CAAC,CAACQ,uBACtC,KAAClD;oCAAkBsC,MAAK;oCAAQC,OAAO1B,SAASqC;mCAArCA;4BAEZ3B,IAAIkB,iBAAiB,CAACG,MAAM,GAAG,kBAC9B,KAAC5C;gCACCsC,MAAK;gCACLN,SAAQ;gCACRO,OAAO,CAAC,CAAC,EAAEhB,IAAIkB,iBAAiB,CAACG,MAAM,GAAG,GAAG;iCAE7C;;uCAGN,KAACzC;wBAAW6B,SAAQ;wBAAUC,OAAM;kCACjC;;YAGT;SACD,GACD,EAAE;QACN;YACEhB,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACV,sEAAsE;YACtE,sEAAsE;YACtE,2BAA2B;YAC3Be,YAAY;YACZK,UAAU;YACVnB,aAAa,CAACC,QAAQC;oBACRA;uBAAZ4B,OAAOC,IAAI,EAAC7B,eAAAA,IAAI8B,OAAO,YAAX9B,eAAe,CAAC,GAAGoB,IAAI,CAAC;;YACtChB,YAAY,CAAC,EAAEJ,GAAG,EAA0B;oBAM3BA;qCALf,KAACtB;oBACC4C,WAAU;oBACVC,SAAS;oBACTlB,IAAI;wBAAEmB,YAAY;wBAAUjB,QAAQ;oBAAO;8BAE1CqB,OAAOC,IAAI,EAAC7B,eAAAA,IAAI8B,OAAO,YAAX9B,eAAe,CAAC,GAAGmB,GAAG,CAAC,CAACY;4BAG1B1D;6CAFT,KAACI;4BAECuC,KAAK,GAAE3C,gCAAAA,qBAAqB,CAAC0D,OAAwB,YAA9C1D,gCAAkD0D;4BACzDhB,MAAK;2BAFAgB;;;;QAOf;QACA;YACErC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;WACPrB,iBAAiBK,4BAA4B;YAChDoC,UAAU;YACVnB,aAAa,CAACC,QAAQC;oBAAwBA;uBAAD,EAACA,YAAAA,IAAIgC,IAAI,YAARhC,YAAY,EAAE,EAAEoB,IAAI,CAAC;;YACnEhB,YAAY,CAAC,EAAEJ,GAAG,EAA0B;oBACzCA;uBAAD,EAACA,YAAAA,IAAIgC,IAAI,YAARhC,YAAY,EAAE,EAAE0B,KAAK,CAAC,GAAG,GAAGN,IAAI,CAAC;;;QAEtC;YACE1B,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACV,qEAAqE;YACrE,sDAAsD;YACtDoC,MAAM;WACHzD,iBAAiBK,4BAA4B;YAChDiB,aAAa,CAACC,QAAQC;oBACHA;gBAAjB,MAAMkC,WAAWlC,iBAAAA,IAAImC,SAAS,qBAAd,AAACnC,eAAoDkC,OAAO;gBAC5E,OAAOA,UAAU,IAAI1C,KAAK0C,UAAU,QAAQ;YAC9C;YACA9B,YAAY,CAAC,EAAEJ,GAAG,EAA0B;oBAEvCA;qCADH,KAACpB;oBAAW6B,SAAQ;oBAAUC,OAAM;8BACjCV,EAAAA,oBAAAA,IAAIoC,YAAY,qBAAhBpC,iBAAkB,CAAC,EAAE,IAClB,IAAIR,KAAKQ,IAAIoC,YAAY,CAAC,EAAE,CAACC,IAAI,EAAEC,kBAAkB,KACrD;;;;QAIV,oEAAoE;QACpEtD,mBAAmBO;QACnB;YACE;;;;;;;;OAQC,GACDG,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVoC,MAAM;YACNrB,YAAY;YACZd,aAAa,CAACC,QAAQC,MACpBA,IAAIuC,uBAAuB,GAAG,IAAI/C,KAAKQ,IAAIuC,uBAAuB,IAAI;YACxEnC,YAAY,CAAC,EAAEJ,GAAG,EAA0B,GAC1CA,IAAIuC,uBAAuB,iBACzB,KAAC5D;oBAAQ6D,OAAO,IAAIhD,KAAKQ,IAAIuC,uBAAuB,EAAEE,cAAc;8BAClE,cAAA,KAAC7D;wBAAW6B,SAAQ;wBAAUC,OAAM;kCACjCvC,kBAAkB6B,IAAIuC,uBAAuB,EAAEhD;;mCAIpD,KAACX;oBAAW6B,SAAQ;oBAAUC,OAAM;8BACjC;;QAGT;QACA;YACE;;;;;OAKC,GACDhB,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVe,YAAY;YACZd,aAAa,CAACC,QAAQC,MACpBA,IAAI0C,UAAU,GAAGpE,kBAAkB,CAAC0B,IAAI0C,UAAU,CAACC,MAAM,CAAC,GAAG;YAC/DvC,YAAY,CAAC,EAAEJ,GAAG,EAA0B,GAC1CA,IAAI0C,UAAU,iBACZ,KAAC3D;oBAAkB6D,OAAO5C,IAAI0C,UAAU;mCAExC,KAAC9D;oBAAW6B,SAAQ;oBAAUC,OAAM;8BACjC;;QAGT;WACGnC,oBACDM,4BACAI,wBACAH;KAEH;AACH"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-list-columns.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 activityTimeLabel,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n EMAIL_STATE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn'\nimport {\n hiddenFilterColumns,\n listFilterColumn,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { Chip, Stack, Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n} from '../constants/contact-filters'\nimport type { ContactRecord } from '../model/contact-record'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { nextActivityColumn } from './crm-next-activity-column'\n\n/**\n * The filterable fields that get a column. The rest of\n * `CONTACT_LIST_FILTER_FIELDS` still reaches the filter panel, hidden.\n */\n/**\n * The declared filter fields that ARE table columns, so the hidden\n * filter-only columns are built for the others alone. Owner and Stage\n * joined it when the grammar learned them (AGL-2617): each is a shown\n * column already, and a second, hidden one under the same field would be\n * a duplicate the grid refuses.\n */\nexport const CONTACT_FILTER_COLUMNS = [\n 'name',\n 'ownerUid',\n 'lifecycleStage',\n 'sources',\n 'tags',\n 'updatedAt',\n 'nextTaskAtMs',\n 'emailState',\n]\n\n/**\n * The columns that ship HIDDEN, for the grid's visibility model (AGL-2616).\n *\n * \"Last engaged\" is a fact most lists do not need on screen and every list\n * can turn on from the column menu; shipping it visible would widen a\n * table that already carries seven columns for a figure only a marketing\n * reader scans.\n */\nexport const CONTACT_OPTIONAL_COLUMNS = ['lastEmailEngagementAtMs']\n\nexport interface ContactListColumnOptions {\n /** The owner's name for a uid — the roster's, or the uid itself. */\n memberName: (uid: string) => string\n /**\n * How a site reads, by document id — handed in at the ORGANIZATION level\n * only (AGL-2630), where it turns on the \"Known by\" column: the sites\n * that have captured each person. Absent under a site, where every row\n * is known by the site and the column would say so on every line.\n */\n siteName?: (hostId: string) => string\n /**\n * One clock for every row of one paint, so two contacts engaged a second\n * apart cannot read \"just now\" and \"1 min ago\". Defaults to the paint's\n * own `Date.now()`.\n */\n nowMs?: number\n}\n\n/**\n * The contacts list's columns (AGL-2596), in their own module so the list\n * file stays about the QUERY and the toolbar while other surfaces — a bulk\n * bar, an import — grow beside it.\n *\n * Owner and Stage read off the flattened row, which is already this\n * holder's facet (`contactRecordFromDoc`), so neither column can show\n * another holder's assignment. Filtering by either is the list's: the panel\n * names the field and the list asks the holder's facet key of its query\n * (`contactQueryClause`, AGL-3321).\n */\nexport function contactListColumns(\n options: ContactListColumnOptions,\n): GridColDef[] {\n const { memberName, siteName } = options\n const nowMs = options.nowMs ?? Date.now()\n return [\n {\n field: 'name',\n headerName: 'Contact',\n flex: 1.6,\n minWidth: 220,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: ContactRecord) =>\n String(row.name || row.email || ''),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name || row.email}\n </Typography>\n {row.name ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.ownerUid ? memberName(row.ownerUid) : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.ownerUid ? (\n <Typography variant=\"body2\" noWrap>\n {memberName(row.ownerUid)}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'lifecycleStage',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 150,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lifecycleStage ? CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lifecycleStage ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={CONTACT_LIFECYCLE_STAGE_LABELS[row.lifecycleStage]}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n /*\n * KNOWN BY (AGL-2630): the sites that have captured this person, which\n * is the cross-site fact the organization-level list exists to show —\n * one deduplicated row per person, and which of the org's sites know\n * them. `capturedByHostIds` is a top-level array precisely so the\n * question can be answered without opening a facet. Not filterable from\n * the panel: an `array-contains` on it would displace the scope clause\n * under a site, and a search or a facet filter at the org level (one\n * array clause per query, AGL-3321). A row that names no site predates attribution and says so,\n * rather than reading as \"every site\".\n */\n ...(siteName\n ? [\n {\n field: 'capturedByHostIds',\n headerName: 'Known by',\n flex: 1.1,\n minWidth: 200,\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: ContactRecord) =>\n row.capturedByHostIds.map(siteName).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.capturedByHostIds.length ? (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%', flexWrap: 'nowrap' }}\n >\n {row.capturedByHostIds.slice(0, 2).map((hostId) => (\n <Chip key={hostId} size=\"small\" label={siteName(hostId)} />\n ))}\n {row.capturedByHostIds.length > 2 ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={`+${row.capturedByHostIds.length - 2}`}\n />\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No site recorded'}\n </Typography>\n ),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'sources',\n headerName: 'Sources',\n flex: 1,\n minWidth: 160,\n // A map of provenance flags, not a scalar — `sources.form == true` is\n // queryable one key at a time, which is a menu of its own rather than\n // a filter on this column.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: ContactRecord) =>\n Object.keys(row.sources ?? {}).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n {Object.keys(row.sources ?? {}).map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source as ContactSource] ?? source}\n size=\"small\"\n />\n ))}\n </Stack>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 150,\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'tags'),\n sortable: false,\n valueGetter: (_value, row: ContactRecord) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: ContactRecord }) =>\n (row.tags ?? []).slice(0, 3).join(', '),\n },\n {\n field: 'updatedAt',\n headerName: 'Last activity',\n flex: 0.8,\n minWidth: 150,\n // `type: 'date'` is what gives the panel a date PICKER rather than a\n // free-text box for a value the query reads as a day.\n type: 'date',\n ...listFilterColumn(CONTACT_LIST_FILTER_FIELDS, 'updatedAt'),\n valueGetter: (_value, row: ContactRecord) => {\n const seconds = (row.updatedAt as { seconds?: number } | undefined)?.seconds\n return seconds ? new Date(seconds * 1000) : null\n },\n renderCell: ({ row }: { row: ContactRecord }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.interactions?.[0]\n ? new Date(row.interactions[0].atMs).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the person is due (AGL-2661).\n nextActivityColumn(nowMs),\n {\n /*\n * When the person last opened or clicked one of this holder's\n * campaigns (AGL-2616). Off the facet like Owner and Stage, so it can\n * never show another holder's readers; `filterable: false` for the\n * same reason theirs are — a facet path is not a query the panel can\n * make. A date column, so a sort on it orders by the instant and an\n * export gets a date; the cell prints the relative form the timeline\n * uses and carries the full stamp in its tooltip.\n */\n field: 'lastEmailEngagementAtMs',\n headerName: 'Last engaged',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.lastEmailEngagementAtMs ? new Date(row.lastEmailEngagementAtMs) : null,\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(row.lastEmailEngagementAtMs).toLocaleString()}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {activityTimeLabel(row.lastEmailEngagementAtMs, nowMs)}\n </Typography>\n </Tooltip>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the same chip the record page\n * carries, so a bounced or blocked person is told apart in the list a\n * member picks recipients from. Sorted and exported by its label;\n * filtered through the bar's `emailState` clause, not the panel.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n valueGetter: (_value, row: ContactRecord) =>\n row.emailState ? EMAIL_STATE_LABELS[row.emailState.status] : '',\n renderCell: ({ row }: { row: ContactRecord }) =>\n row.emailState ? (\n <CrmEmailStateChip state={row.emailState} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n ...hiddenFilterColumns(\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_FILTER_COLUMNS,\n CONTACT_LIST_FILTER_HEADERS,\n ),\n ]\n}\n"],"names":["activityTimeLabel","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","EMAIL_STATE_LABELS","hiddenFilterColumns","listFilterColumn","Chip","Stack","Tooltip","Typography","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CrmEmailStateChip","nextActivityColumn","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","options","memberName","siteName","nowMs","Date","now","field","headerName","flex","minWidth","valueGetter","_value","row","String","name","email","renderCell","sx","justifyContent","height","lineHeight","variant","color","noWrap","filterable","ownerUid","lifecycleStage","size","label","sortable","capturedByHostIds","map","join","length","direction","spacing","alignItems","flexWrap","slice","hostId","Object","keys","sources","source","tags","type","seconds","updatedAt","interactions","atMs","toLocaleDateString","lastEmailEngagementAtMs","title","toLocaleString","emailState","status","state"],"mappings":"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"}
|
|
@@ -76,6 +76,8 @@ import AddToListDialog from "./add-to-list-dialog.js";
|
|
|
76
76
|
import { CompanyPicker, useCompanyOptions, useCreateCompany } from "./company-picker.js";
|
|
77
77
|
import { CrmBulkBarFrame, CrmBulkValueDialog, countNoun } from "./crm-bulk-bar-frame.js";
|
|
78
78
|
import CrmExportAllButton from "./crm-export-all-button.js";
|
|
79
|
+
import { restampCrmListFields } from "../model/list-fields-api.js";
|
|
80
|
+
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
79
81
|
import { CrmSuiteLockedButton } from "./crm-suite-lock.js";
|
|
80
82
|
const NOUN = {
|
|
81
83
|
singular: 'contact',
|
|
@@ -303,6 +305,7 @@ function ContactsBulkBarBody(props) {
|
|
|
303
305
|
csv
|
|
304
306
|
]);
|
|
305
307
|
const handleDetach = useCallback(async ()=>{
|
|
308
|
+
var _ref;
|
|
306
309
|
if (!scope || !selectedRows.length || !consentGroup) return;
|
|
307
310
|
const count = selectedRows.length;
|
|
308
311
|
const confirmed = await confirm({
|
|
@@ -325,6 +328,10 @@ function ContactsBulkBarBody(props) {
|
|
|
325
328
|
// The rows that went are gone from the table; the refused ones stay
|
|
326
329
|
// selected, so the reader can see which they are and try again.
|
|
327
330
|
const refused = new Set(outcome.refused.map((row)=>row.label));
|
|
331
|
+
// A detached row still carries the facet keys and scoped search tokens
|
|
332
|
+
// of the holder that let it go; the rules keep a client from restamping
|
|
333
|
+
// them, so the route does (AGL-3321). A deleted row answers `missing`.
|
|
334
|
+
await restampCrmListFields(user, crmTaskCallScope(hostId, (_ref = scope == null ? void 0 : scope[1]) != null ? _ref : null), 'contacts', selectedRows.filter((row)=>!refused.has(contactBulkAddressOf(row))).map((row)=>row.$id));
|
|
328
335
|
onSelectedChange(selectedRows.filter((row)=>refused.has(contactBulkAddressOf(row))).map((row)=>row.$id));
|
|
329
336
|
}, [
|
|
330
337
|
scope,
|
|
@@ -333,7 +340,9 @@ function ContactsBulkBarBody(props) {
|
|
|
333
340
|
apply,
|
|
334
341
|
writers,
|
|
335
342
|
consentGroup,
|
|
336
|
-
onSelectedChange
|
|
343
|
+
onSelectedChange,
|
|
344
|
+
user,
|
|
345
|
+
hostId
|
|
337
346
|
]);
|
|
338
347
|
const emails = selectedRows.map((row)=>{
|
|
339
348
|
var _row_email;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is the one act written\n * client-direct, because a holder dropping its own facet is the one facet\n * change the rules leave the browser.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { deleteDoc, doc, updateDoc, writeBatch } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useOrgMemberOptions,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planDetach,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n type ContactBulkWrite,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\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 const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /**\n * The Firestore writers for the one act written client-direct: letting the\n * rows go, a batch at a time and a row at a time for a batch that failed.\n */\n const writers = useMemo(() => {\n const refFor = (id: string) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', 'contacts', id)\n return {\n commitBatch: async (writes: readonly ContactBulkWrite[]) => {\n const batch = writeBatch(firestore)\n for (const write of writes) {\n if (write.kind === 'delete') batch.delete(refFor(write.id))\n else batch.update(refFor(write.id), write.data)\n }\n await batch.commit()\n },\n commitOne: async (write: ContactBulkWrite) => {\n if (write.kind === 'delete') await deleteDoc(refFor(write.id))\n else await updateDoc(refFor(write.id), write.data)\n },\n }\n }, [firestore, scope])\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\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 plan = planDetach(selectedRows, consentGroup, Date.now())\n const outcome = await apply({\n attempted: plan.writes.length,\n skipped: plan.skipped.map(skippedLine),\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.email),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, writers, consentGroup, onSelectedChange])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\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={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\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 error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\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 helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\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=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmMemberPickerLabel","useConfirmationContext","Button","MenuItem","TextField","deleteDoc","doc","updateDoc","writeBatch","useCallback","useMemo","useState","useFirestore","useOrgMemberOptions","useUser","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planDetach","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","runCrmBulkWrites","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","firestore","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","writers","refFor","id","commitBatch","writes","batch","write","kind","delete","update","commit","commitOne","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","plan","Date","now","outcome","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","resource","orgId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED,SAEEA,8BAA8B,EAC9BC,wBAAwB,EAGxBC,oBAAoB,QACf,eAAc;AACrB,SAASC,sBAAsB,QAAQ,uBAAsB;AAC7D,SAASC,MAAM,EAAEC,QAAQ,EAAEC,SAAS,QAAQ,gBAAe;AAC3D,SAASC,SAAS,EAAEC,GAAG,EAAEC,SAAS,EAAEC,UAAU,QAAQ,qBAAoB;AAC1E,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SACEC,YAAY,EACZC,mBAAmB,EACnBC,OAAO,QACF,iCAAgC;AACvC,SAASC,gBAAgB,QAAQ,iCAA6B;AAC9D,SAASC,eAAe,QAAQ,iCAA6B;AAC7D,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,sBAAsB,QAAkC,6BAAyB;AAC1F,SAASC,eAAe,QAAQ,sBAAkB;AAClD,SACEC,oBAAoB,EACpBC,gBAAgB,EAChBC,UAAU,EACVC,UAAU,EACVC,aAAa,EACbC,cAAc,QAKT,mCAA+B;AACtC,SAGEC,WAAW,EACXC,gBAAgB,QACX,2BAAuB;AAC9B,SAEEC,eAAe,EACfC,eAAe,EACfC,gBAAgB,QACX,8BAA0B;AACjC,OAAOC,qBAAqB,0BAAsB;AAClD,SACEC,aAAa,EAEbC,iBAAiB,EACjBC,gBAAgB,QACX,sBAAkB;AACzB,SAEEC,eAAe,EACfC,kBAAkB,EAClBC,SAAS,QACJ,0BAAsB;AAC7B,OAAOC,wBAAwB,6BAAyB;AACxD,SAASC,oBAAoB,QAAQ,sBAAkB;AAmCvD,MAAMC,OAAoB;IAAEC,UAAU;IAAWC,QAAQ;AAAW;AAKpE,MAAMC,gBAA+C;IACnD,WAAW;IACX,cAAc;IACdC,OAAO;IACPC,OAAO;IACPC,SAAS;AACX;AAEA,iEAAiE,GACjE,MAAMC,eAAe,CAACC,QAAkCC;IACtD,MAAMC,SAASb,UAAUY,MAAMT;IAC/B,OAAQQ;QACN,KAAK;YACH,OAAO,CAAC,OAAO,EAAEE,QAAQ;QAC3B,KAAK;YACH,OAAO,CAAC,qBAAqB,EAAEA,QAAQ;QACzC,KAAK;YACH,OAAO,CAAC,aAAa,EAAEA,QAAQ;QACjC,KAAK;YACH,OAAO,CAAC,aAAa,EAAEA,QAAQ;QACjC,KAAK;YACH,OAAO,CAAC,eAAe,EAAEA,QAAQ;QACnC,KAAK;YACH,OAAO,GAAGA,OAAO,uBAAuB,CAAC;IAC7C;AACF;AAEA,4DAA4D,GAC5D,MAAMC,cAAc,CAACC,MAA0B,CAAA;QAAEC,OAAOD,IAAIE,KAAK;QAAEC,QAAQH,IAAIG,MAAM;IAAC,CAAA;AAEtF;;;;;;;CAOC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,IAAI,CAACA,MAAMC,QAAQ,CAACC,MAAM,EAAE,OAAO;IACnC,qBAAO,KAACC,kCAAwBH;AAClC;AACAD,gBAAgBK,WAAW,GAAG;AAE9B,SAASD,oBAAoBH,KAA2B;QAuQ5BK;IAtQ1B,MAAM,EACJC,MAAM,EACNC,GAAG,EACHC,KAAK,EACLC,YAAY,EACZC,IAAI,EACJT,QAAQ,EACRU,gBAAgB,EAChBC,GAAG,EACHC,cAAc,KAAK,EACpB,GAAGb;IACJ,MAAMc,YAAY3D;IAClB,MAAM,EAAE4D,MAAMC,IAAI,EAAE,GAAG3D;IACvB,MAAM,EAAE4D,OAAO,EAAE,GAAGzE;IACpB,MAAM,EAAE0E,IAAI,EAAEC,MAAM,EAAEC,KAAK,EAAEC,aAAa,EAAE,GAAG9D,gBAAgB;QAAE+D,YAAY;IAAU;IACvF,MAAMC,gBAAgBjE,iBAAiBgD;IAEvC,MAAMkB,eAAevE,QAAQ;QAC3B,MAAMwE,SAAS,IAAIC,IAAIzB;QACvB,OAAOS,KAAKiB,MAAM,CAAC,CAAChC,MAAQ8B,OAAOG,GAAG,CAACjC,IAAIkC,GAAG;IAChD,GAAG;QAACnB;QAAMT;KAAS;IAEnB,MAAM,CAAC6B,SAASC,WAAW,GAAG7E,SAA+B;IAC7D,MAAM,CAAC8E,OAAOC,SAAS,GAAG/E,SAAS;IACnC,MAAM,CAACmC,SAAS6C,WAAW,GAAGhF,SAA+B;IAC7D,MAAM,CAACiF,UAAUC,YAAY,GAAGlF,SAAS;IAEzC;;;;;GAKC,GACD,MAAMmD,OAAOjD,oBAAoBoD,yBAAAA,KAAO,CAAC,EAAE,EAAE;QAAE6B,SAASP,YAAY;IAAQ;IAC5E,MAAMQ,YAAY9D,kBAAkB;QAClC8B;QACAC;QACA8B,SAASP,YAAY;IACvB;IACA,MAAMS,gBAAgB9D,iBAAiB;QAAE6B;QAAQC;IAAI;IACrD,uEAAuE;IACvE,2DAA2D;IAC3D,MAAM,EAAEiC,YAAY,EAAE,GAAGhF,YAAY;QAAE8C;QAAQC;IAAI;IAEnD,MAAMkC,aAAa,CAAClD;QAClB0C,SAAS;QACTC,WAAW;QACXH,WAAWxC;IACb;IAEA;;;GAGC,GACD,MAAMmD,UAAUzF,QAAQ;QACtB,MAAM0F,SAAS,CAACC;;mBACd/F,IAAIiE,mBAAWN,yBAAAA,KAAO,CAAC,EAAE,mBAAI,iBAAQA,yBAAAA,KAAO,CAAC,EAAE,oBAAI,IAAI,YAAYoC;;QACrE,OAAO;YACLC,aAAa,OAAOC;gBAClB,MAAMC,QAAQhG,WAAW+D;gBACzB,KAAK,MAAMkC,SAASF,OAAQ;oBAC1B,IAAIE,MAAMC,IAAI,KAAK,UAAUF,MAAMG,MAAM,CAACP,OAAOK,MAAMJ,EAAE;yBACpDG,MAAMI,MAAM,CAACR,OAAOK,MAAMJ,EAAE,GAAGI,MAAMjC,IAAI;gBAChD;gBACA,MAAMgC,MAAMK,MAAM;YACpB;YACAC,WAAW,OAAOL;gBAChB,IAAIA,MAAMC,IAAI,KAAK,UAAU,MAAMrG,UAAU+F,OAAOK,MAAMJ,EAAE;qBACvD,MAAM9F,UAAU6F,OAAOK,MAAMJ,EAAE,GAAGI,MAAMjC,IAAI;YACnD;QACF;IACF,GAAG;QAACD;QAAWN;KAAM;IAErB,gFAAgF,GAChF,MAAM8C,mBAAmBtG,YACvB,CAACuG,SAAoCC,MACnCrF,gBACEoF,SACA,CAAC5D,MAAQA,IAAIkC,GAAG,EAChBlE,sBACA,CAAC8F,QAAUlC,cAAcmC,UAAU,CAACD,MAAME,GAAG,CAAC,CAAChE,MAAQA,IAAIkC,GAAG,GAAG2B,MACjE/F,yBAEJ;QAAC8D;KAAc;IAGjB,MAAMqC,cAAc5G,YAAY;QAC9B,IAAI,CAAC8E,WAAW,CAACtB,OAAO;QACxB;;;;;KAKC,GACD,MAAMqD,OAAyB,EAAE;QACjC,MAAMC,SAA4B,EAAE;QACpC,KAAK,MAAMnE,OAAO6B,aAAc;YAC9B,IAAIf,gBAAgBd,IAAIoE,OAAO,EAAEF,KAAKG,IAAI,CAACrE;iBACtC;gBACHmE,OAAOE,IAAI,CAAC;oBACVnE,OAAOlC,qBAAqBgC;oBAC5BG,QAAQ;gBACV;YACF;QACF;QAEA,MAAMmE,UAAU,AAAC,CAAA;YACf,IAAInC,YAAY,aAAaA,YAAY,cAAc;gBACrD,MAAMoC,MAAMtG,iBAAiBoE;gBAC7B,IAAI,CAACkC,KAAK,OAAO;gBACjB,MAAMC,YACJrC,YAAY,YAAYjE,WAAWgG,MAAMK,OAAOnG,cAAc8F,MAAMK;gBACtE,MAAMV,MACJ1B,YAAY,YAAY;oBAAEsC,QAAQF;gBAAI,IAAI;oBAAEG,WAAWH;gBAAI;gBAC7D,OAAO;oBAAEC;oBAAWG,KAAK,IAAMhB,iBAAiBa,UAAUzD,IAAI,EAAE8C;gBAAK;YACvE;YACA,IAAI1B,YAAY,SAAS;gBACvB,OAAO;oBACLqC,WAAW;wBAAEzD,MAAMmD;wBAAMU,SAAS,EAAE;oBAAC;oBACrCD,KAAK,IAAMhB,iBAAiBO,MAAM;4BAAEW,UAAUxC;wBAAM;gBACtD;YACF;YACA,IAAIF,YAAY,WAAW;;gBACzB,MAAM2C,oBAAYpF,2BAAAA,QAASuD,EAAE,mBAAI;gBACjC,MAAMuB,YAAYnG,eAAe6F,MAAMY;gBACvC,OAAO;oBAAEN;oBAAWG,KAAK,IAAMhB,iBAAiBa,UAAUzD,IAAI,EAAE;4BAAE+D;wBAAU;gBAAG;YACjF;YACA,IAAI,CAACzC,OAAO,OAAO;YACnB,MAAM5C,QAAQ4C;YACd,OAAO;gBACLmC,WAAW;oBAAEzD,MAAMmD;oBAAMU,SAAS,EAAE;gBAAC;gBACrC;;;;SAIC,GACDD,KAAK,IACHlG,gBAAgByF,MAAMlG,sBAAsB,OAAOgC;wBACjD,MAAM+E,OAAOpE,iBAAAA,SAAWX,IAAIgF,YAAY,IAAI;wBAC5C,IAAI,CAACD,MAAM,MAAM,IAAIE,MAAM;wBAC3B,MAAMlH,gBAAgBsD,MAAM0D,MAAM/E,IAAIkC,GAAG,EAAEzC;oBAC7C;YACJ;QACF,CAAA;QACA,IAAI,CAAC6E,SAAS;QACd,MAAM1E,SAASuC;QACfC,WAAW;QACX,MAAMX,MAAM;YACVyD,WAAWZ,QAAQE,SAAS,CAACzD,IAAI,CAACR,MAAM;YACxCqE,SAAS;mBAAIT;mBAAWG,QAAQE,SAAS,CAACI,OAAO;aAAC,CAACZ,GAAG,CAACjE;YACvD4E,KAAKL,QAAQK,GAAG;YAChB9E,MAAM,CAACsF,QAAUxF,aAAaC,QAAQuF;QACxC;IACF,GAAG;QACDhD;QACAtB;QACAgB;QACAf;QACAuB;QACA3C;QACAiE;QACAhD;QACAU;QACAI;KACD;IAED,MAAM2D,eAAe/H,YAAY;QAC/BkB,iBACE,yBACA,YACAD,YAAYuD,cAAcZ;IAE9B,GAAG;QAACY;QAAcZ;KAAI;IAEtB,MAAMoE,eAAehI,YAAY;QAC/B,IAAI,CAACwD,SAAS,CAACgB,aAAatB,MAAM,IAAI,CAACO,cAAc;QACrD,MAAMqE,QAAQtD,aAAatB,MAAM;QACjC,MAAM+E,YAAY,MAAMhE,QAAQ;YAC9BiE,OAAOJ,UAAU,IAAI,yBAAyB,CAAC,OAAO,EAAEA,MAAM,UAAU,CAAC;YACzEK,aACE,GAAGL,UAAU,IAAI,mBAAmB,CAAC,MAAM,EAAEA,MAAM,WAAW,CAAC,CAAC,CAAC,CAAC,GAClE,yEACA,wEACA,uEACA,qEACA;YACFM,kBAAkBN,UAAU,IAAI,mBAAmB;YACnDO,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,EACE,0DAA0D;SACzDC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,WAAW;QAChB,MAAMQ,OAAO3H,WAAW0D,cAAcf,cAAciF,KAAKC,GAAG;QAC5D,MAAMC,UAAU,MAAMxE,MAAM;YAC1ByD,WAAWY,KAAK3C,MAAM,CAAC5C,MAAM;YAC7BqE,SAASkB,KAAKlB,OAAO,CAACZ,GAAG,CAACjE;YAC1B4E,KAAK,IAAMjG,iBAAiBqE,SAAS+C,KAAK3C,MAAM,EAAE,CAACE,QAAUA,MAAMnD,KAAK;YACxEL,MAAM,CAACA,OAASF,aAAa,UAAUE;QACzC;QACA,oEAAoE;QACpE,gEAAgE;QAChE,MAAMqG,UAAU,IAAInE,IAAIkE,QAAQC,OAAO,CAAClC,GAAG,CAAC,CAAChE,MAAQA,IAAIC,KAAK;QAC9De,iBACEa,aACGG,MAAM,CAAC,CAAChC,MAAQkG,QAAQjE,GAAG,CAACjE,qBAAqBgC,OACjDgE,GAAG,CAAC,CAAChE,MAAQA,IAAIkC,GAAG;IAE3B,GAAG;QAACrB;QAAOgB;QAAcP;QAASG;QAAOsB;QAASjC;QAAcE;KAAiB;IAEjF,MAAMmF,SAAStE,aACZmC,GAAG,CAAC,CAAChE;YAAeA;eAAPoG,QAAOpG,aAAAA,IAAIE,KAAK,YAATF,aAAa,IAAIqG,IAAI;OACzCrE,MAAM,CAACsE;IAEV,qBACE,MAACvH;QACCoG,OAAO7E,SAASC,MAAM;QACtBgG,MAAMnH;QACNmC,MAAMA;QACNiF,SAAS,IAAMxF,iBAAiB,EAAE;QAClCQ,QAAQA;QACRiF,iBAAiB/E;QACjBgF,sBACE;;8BACE,KAAC1H;oBACC2H,MAAMxE,YAAY;oBAClBoD,OAAOpD,UAAU5C,aAAa,CAAC4C,QAAQ,GAAG;oBAC1CgD,OAAO7E,SAASC,MAAM;oBACtBgG,MAAMnH;oBACNmC,MAAMA;oBACNqF,UACE,CAAEzE,CAAAA,YAAY,WAAW,CAACE,KAAI,KAC9B,CACE,CAAA,AAACF,CAAAA,YAAY,aAAaA,YAAY,YAAW,KACjD,CAAClE,iBAAiBoE,MAAK;oBAG3BwE,SAAS,IAAMzE,WAAW;oBAC1B0E,SAAS,IAAM,KAAK7C;8BAEnB9B,YAAY,aAAaA,YAAY,6BACpC,KAACnF;wBACC+J,SAAS;wBACTC,MAAK;wBACL/G,OAAM;wBACNgH,aAAY;wBACZ5E,OAAOA;wBACP6E,UAAU,CAACC,QAAU7E,SAAS6E,MAAMC,MAAM,CAAC/E,KAAK;wBAChDgF,YACElF,YAAY,YACR,2CACA;yBAGNA,YAAY,wBACd,MAACnF;wBACCsK,MAAM;wBACNN,MAAK;wBACL/G,OAAM;wBACNoC,OAAOA;wBACP6E,UAAU,CAACC,QAAU7E,SAAS6E,MAAMC,MAAM,CAAC/E,KAAK;wBAChDkF,OAAOjB,QAAQ5F,KAAK6G,KAAK;wBACzBF,UAAU,GAAE3G,cAAAA,KAAK6G,KAAK,YAAV7G,cAAeA,KAAK8G,KAAK,GAAGC,YAAY;;0CAEpD,KAAC1K;gCAASsF,OAAM;0CAAI;;4BACnB3B,KAAKgH,OAAO,CAAC1D,GAAG,CAAC,CAAC2D,uBACjB,KAAC5K;oCAA0BsF,OAAOsF,OAAOC,GAAG;8CACzChL,qBAAqB+K;mCADTA,OAAOC,GAAG;;yBAK3BzF,YAAY,wBACd,KAACnF;wBACCsK,MAAM;wBACNN,MAAK;wBACL/G,OAAM;wBACNoC,OAAOA;wBACP6E,UAAU,CAACC,QAAU7E,SAAS6E,MAAMC,MAAM,CAAC/E,KAAK;kCAE/C1F,yBAAyBqH,GAAG,CAAC,CAACvE,sBAC7B,KAAC1C;gCAAqBsF,OAAO5C;0CAC1B/C,8BAA8B,CAAC+C,MAAM;+BADzBA;yBAKjB0C,YAAY,0BACd,KAACvD;wBACC8I,SAAS/E,UAAU+E,OAAO;wBAC1BF,OAAO7E,UAAU6E,KAAK;wBACtBK,WAAWlF,UAAUkF,SAAS;wBAC9BxF,KAAK,UAAE3C,2BAAAA,QAASuD,EAAE,mBAAI;wBACtBiE,UAAU,CAACY,KAAKC,SAAWxF,WAAWwF;wBACtCC,UAAUpF;wBACVyE,YAAW;yBAEX;;gBAEL7E,yBACC,KAAC7D;oBACCgI,IAAI;oBACJE,SAAS,IAAMpE,YAAY;oBAC3B9B,QAAQkC;oBACRhC,OAAOA;oBACPsF,QAAQA;qBAER;;;;0BAIR,KAACrJ;gBAAOkK,MAAK;gBAAQiB,UAAU1G,QAAQ,CAACV;gBAAOqH,SAAS,IAAMpF,WAAW;0BACtE;;0BAEH,KAAChG;gBACCkK,MAAK;gBACLiB,UAAU1G,QAAQ,CAACV;gBACnBqH,SAAS,IAAMpF,WAAW;0BAEzB;;YAEF5B,4BACC;;kCACE,KAAC/B;kCAAsB;;kCACvB,KAACA;kCAAsB;;kCACvB,KAACA;kCAAsB;;;+BAGzB;;kCACE,KAACrC;wBAAOkK,MAAK;wBAAQiB,UAAU1G,QAAQ,CAACV;wBAAOqH,SAAS,IAAMpF,WAAW;kCACtE;;kCAEH,KAAChG;wBAAOkK,MAAK;wBAAQiB,UAAU1G,QAAQ,CAACV;wBAAOqH,SAAS,IAAMpF,WAAW;kCACtE;;kCAEH,KAAChG;wBACCkK,MAAK;wBACLiB,UAAU1G,QAAQ,CAACV;wBACnBqH,SAAS,IAAMpF,WAAW;kCAEzB;;;;0BAIP,KAAChG;gBACCkK,MAAK;gBACLiB,UAAU1G,QAAQ,CAACV,SAAS,CAACsF,OAAO5F,MAAM,IAAI,CAACsC;gBAC/CqF,SAAS,IAAMzF,YAAY;0BAE1B;;0BAEH,KAAC3F;gBAAOkK,MAAK;gBAAQiB,UAAU1G;gBAAM2G,SAAS9C;0BAC3C;;0BAMH,KAAClG;gBACCiJ,UAAS;gBACTC,KAAK,WAAEvH,yBAAAA,KAAO,CAAC,EAAE,oBAAI;gBACrBF,QAAQA;gBACRsH,UAAU1G;;YAQXT,6BACC,KAAChE;gBACCkK,MAAK;gBACLrB,OAAM;gBACNsC,UAAU1G,QAAQ,CAACV;gBACnBqH,SAAS,IAAM,KAAK7C;0BAEnB;iBAED;;;AAGV;AACA7E,oBAAoBC,WAAW,GAAG;AAElC,eAAeL,gBAAe"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is the one act written\n * client-direct, because a holder dropping its own facet is the one facet\n * change the rules leave the browser.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { deleteDoc, doc, updateDoc, writeBatch } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useOrgMemberOptions,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planDetach,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n type ContactBulkWrite,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { restampCrmListFields } from '../model/list-fields-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\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 const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /**\n * The Firestore writers for the one act written client-direct: letting the\n * rows go, a batch at a time and a row at a time for a batch that failed.\n */\n const writers = useMemo(() => {\n const refFor = (id: string) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', 'contacts', id)\n return {\n commitBatch: async (writes: readonly ContactBulkWrite[]) => {\n const batch = writeBatch(firestore)\n for (const write of writes) {\n if (write.kind === 'delete') batch.delete(refFor(write.id))\n else batch.update(refFor(write.id), write.data)\n }\n await batch.commit()\n },\n commitOne: async (write: ContactBulkWrite) => {\n if (write.kind === 'delete') await deleteDoc(refFor(write.id))\n else await updateDoc(refFor(write.id), write.data)\n },\n }\n }, [firestore, scope])\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\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 plan = planDetach(selectedRows, consentGroup, Date.now())\n const outcome = await apply({\n attempted: plan.writes.length,\n skipped: plan.skipped.map(skippedLine),\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.email),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n // A detached row still carries the facet keys and scoped search tokens\n // of the holder that let it go; the rules keep a client from restamping\n // them, so the route does (AGL-3321). A deleted row answers `missing`.\n await restampCrmListFields(\n user,\n crmTaskCallScope(hostId, scope?.[1] ?? null),\n 'contacts',\n selectedRows\n .filter((row) => !refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, writers, consentGroup, onSelectedChange, user, hostId])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\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={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\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 error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\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 helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\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=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmMemberPickerLabel","useConfirmationContext","Button","MenuItem","TextField","deleteDoc","doc","updateDoc","writeBatch","useCallback","useMemo","useState","useFirestore","useOrgMemberOptions","useUser","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planDetach","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","runCrmBulkWrites","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","restampCrmListFields","crmTaskCallScope","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","firestore","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","writers","refFor","id","commitBatch","writes","batch","write","kind","delete","update","commit","commitOne","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","plan","Date","now","outcome","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","resource","orgId"],"mappings":"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"}
|