@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201
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/activity-list.d.ts +36 -5
- package/src/lib/components/activity-list.js +159 -137
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/activity-timeline.d.ts +84 -0
- package/src/lib/components/activity-timeline.js +224 -0
- package/src/lib/components/activity-timeline.js.map +1 -0
- 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/company-properties-card.js +53 -36
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-associations-card.d.ts +3 -2
- package/src/lib/components/contact-associations-card.js +12 -12
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -3
- package/src/lib/components/contact-detail-page.js +18 -12
- 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/contact-properties-card.d.ts +21 -4
- package/src/lib/components/contact-properties-card.js +129 -29
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.d.ts +12 -6
- package/src/lib/components/contact-timeline-card.js +132 -174
- package/src/lib/components/contact-timeline-card.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 +282 -331
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-state-chip.js +4 -1
- package/src/lib/components/crm-email-state-chip.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/deal-products-card.js +15 -13
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deal-properties-card.js +7 -0
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.js +31 -29
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +67 -97
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +4 -0
- package/src/lib/components/fields-section.js +368 -154
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +24 -20
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +1 -1
- package/src/lib/components/lead-campaigns-card.js +43 -38
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +14 -6
- package/src/lib/components/lead-properties-card.js +323 -311
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +13 -2
- package/src/lib/components/lead-surfaces-note.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/linked-deals-card.js +12 -10
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/recent-activity-feed.d.ts +12 -8
- package/src/lib/components/recent-activity-feed.js +54 -29
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/record-activity-card.d.ts +4 -2
- package/src/lib/components/record-activity-card.js +29 -11
- package/src/lib/components/record-activity-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +18 -12
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +47 -37
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +22 -16
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.d.ts +4 -11
- package/src/lib/components/reports/report-export.js +16 -33
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +18 -12
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +35 -25
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/tasks-section.js +151 -144
- 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/constants/field-list-query.d.ts +23 -0
- package/src/lib/constants/field-list-query.js +80 -0
- package/src/lib/constants/field-list-query.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 +50 -44
- 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/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.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/components/contact-custom-fields-card.d.ts +0 -39
- package/src/lib/components/contact-custom-fields-card.js +0 -204
- package/src/lib/components/contact-custom-fields-card.js.map +0 -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
|
@@ -166,21 +166,50 @@ import { useCrmApi } from "./use-crm-api.js";
|
|
|
166
166
|
{
|
|
167
167
|
label: 'Notes',
|
|
168
168
|
value: company.notes
|
|
169
|
-
}
|
|
170
|
-
...fields.active.map((definition)=>{
|
|
171
|
-
var _company_custom;
|
|
172
|
-
const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
|
|
173
|
-
return {
|
|
174
|
-
label: definition.label || definition.key,
|
|
175
|
-
value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
|
|
176
|
-
href: text,
|
|
177
|
-
target: "_blank",
|
|
178
|
-
rel: "noreferrer noopener",
|
|
179
|
-
children: text
|
|
180
|
-
}) : text
|
|
181
|
-
};
|
|
182
|
-
})
|
|
169
|
+
}
|
|
183
170
|
];
|
|
171
|
+
// The org's own company fields (AGL-2661), after the built-in ones under
|
|
172
|
+
// "More fields", as a record's Details keep them; none defined, none drawn.
|
|
173
|
+
const moreRows = fields.active.map((definition)=>{
|
|
174
|
+
var _company_custom;
|
|
175
|
+
const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
|
|
176
|
+
return {
|
|
177
|
+
label: definition.label || definition.key,
|
|
178
|
+
value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
|
|
179
|
+
href: text,
|
|
180
|
+
target: "_blank",
|
|
181
|
+
rel: "noreferrer noopener",
|
|
182
|
+
children: text
|
|
183
|
+
}) : text
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const factRows = (list)=>list.map((row)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
187
|
+
direction: "row",
|
|
188
|
+
spacing: 2,
|
|
189
|
+
sx: {
|
|
190
|
+
alignItems: 'baseline'
|
|
191
|
+
},
|
|
192
|
+
children: [
|
|
193
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
194
|
+
variant: "caption",
|
|
195
|
+
color: "text.secondary",
|
|
196
|
+
sx: {
|
|
197
|
+
minWidth: 96,
|
|
198
|
+
flexShrink: 0
|
|
199
|
+
},
|
|
200
|
+
children: row.label
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
203
|
+
variant: "body2",
|
|
204
|
+
color: row.value ? 'text.primary' : 'text.secondary',
|
|
205
|
+
sx: {
|
|
206
|
+
whiteSpace: 'pre-wrap',
|
|
207
|
+
overflowWrap: 'anywhere'
|
|
208
|
+
},
|
|
209
|
+
children: row.value || '—'
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
}, row.label));
|
|
184
213
|
return /*#__PURE__*/ _jsxs(CrmRecordHeader, {
|
|
185
214
|
kind: "Company",
|
|
186
215
|
title: String(company.name || company.$id),
|
|
@@ -240,35 +269,23 @@ import { useCrmApi } from "./use-crm-api.js";
|
|
|
240
269
|
]
|
|
241
270
|
}),
|
|
242
271
|
children: [
|
|
243
|
-
/*#__PURE__*/
|
|
272
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
244
273
|
spacing: 1,
|
|
245
|
-
children:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
sx: {
|
|
249
|
-
alignItems: 'baseline'
|
|
250
|
-
},
|
|
274
|
+
children: [
|
|
275
|
+
factRows(rows),
|
|
276
|
+
moreRows.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
251
277
|
children: [
|
|
252
278
|
/*#__PURE__*/ _jsx(Typography, {
|
|
253
|
-
variant: "
|
|
254
|
-
color: "text.secondary",
|
|
279
|
+
variant: "subtitle2",
|
|
255
280
|
sx: {
|
|
256
|
-
|
|
257
|
-
flexShrink: 0
|
|
281
|
+
pt: 1
|
|
258
282
|
},
|
|
259
|
-
children:
|
|
283
|
+
children: 'More fields'
|
|
260
284
|
}),
|
|
261
|
-
|
|
262
|
-
variant: "body2",
|
|
263
|
-
color: row.value ? 'text.primary' : 'text.secondary',
|
|
264
|
-
sx: {
|
|
265
|
-
whiteSpace: 'pre-wrap',
|
|
266
|
-
overflowWrap: 'anywhere'
|
|
267
|
-
},
|
|
268
|
-
children: row.value || '—'
|
|
269
|
-
})
|
|
285
|
+
factRows(moreRows)
|
|
270
286
|
]
|
|
271
|
-
}
|
|
287
|
+
}) : null
|
|
288
|
+
]
|
|
272
289
|
}),
|
|
273
290
|
editing ? /*#__PURE__*/ _jsx(CompanyEditDrawer, {
|
|
274
291
|
open: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ...fields.active.map((definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n }),\n ]\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {rows.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","active","definition","text","custom","key","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","spacing","row","direction","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","open","onClose","onSaved","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAIEA,cAAc,QACT,eAAc;AACrB,SAASC,gBAAgB,EAAEC,gBAAgB,QAAQ,yBAAwB;AAC3E,SAASC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AACtE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC/D,SAAyBC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AAC7D,SAASC,0BAA0B,QAAQ,4CAAwC;AACnF,SAASC,oBAAoB,QAAQ,sCAAkC;AAGvE,SAASC,oBAAoB,QAAQ,wBAAoB;AACzD,SACEC,2BAA2B,EAC3BC,yBAAyB,QACpB,6BAAyB;AAEhC,OAAOC,uBAAuB,2BAAuB;AACrD,SAASC,wBAAwB,QAAQ,8BAA0B;AACnE,SAASC,aAAa,EAAEC,YAAY,QAAQ,wBAAoB;AAChE,SAASC,aAAa,EAAEC,eAAe,QAAQ,yBAAqB;AACpE,SAASC,SAAS,QAAQ,mBAAe;AAgBzC,8CAA8C,GAC9C,SAASC,cAAcC,OAA8C;IACnE,IAAI,CAACA,SAAS,OAAO;IACrB,OAAO;QACLA,QAAQC,KAAK;QACbD,QAAQE,KAAK;QACb;YAACF,QAAQG,IAAI;YAAEH,QAAQI,KAAK;SAAC,CAACC,MAAM,CAACC,SAASC,IAAI,CAAC;QACnDP,QAAQQ,UAAU;QAClBR,QAAQS,OAAO;KAChB,CACEC,GAAG,CAAC,CAACC,OAASC,OAAOD,eAAAA,OAAQ,IAAIE,IAAI,IACrCR,MAAM,CAACC,SACPC,IAAI,CAAC;AACV;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASO,sBAAsBC,KAAiC;QA4F1CC;IA3F3B,MAAM,EACJA,OAAO,EACPC,IAAI,EACJC,MAAM,EACNC,GAAG,EACHC,QAAQ,EACRC,OAAO,EACPC,MAAM,EACNC,SAAS,EACV,GAAGR;IACJ,MAAM,EAAES,KAAK,EAAE,GAAGJ;IAClB,MAAMK,UAAU3B,UAAUoB;IAC1B,MAAM,EAAEQ,OAAO,EAAE,GAAG/C;IACpB,MAAM,EAAEgD,eAAe,EAAE,GAAG/C;IAC5B,uEAAuE;IACvE,yEAAyE;IACzE,yEAAyE;IACzE,6CAA6C;IAC7C,MAAMgD,cAAcxC,qBAAqB8B;IACzC,MAAM,CAACW,SAASC,WAAW,GAAG5C,SAAS;IACvC,MAAM,CAAC6C,UAAUC,YAAY,GAAG9C,SAAS;IACzC,oEAAoE;IACpE,2DAA2D;IAC3D,MAAM+C,SAAS9C,2BAA2BiC,SAASc,KAAK,EAAE;IAE1D,MAAMC,eAAelD,YAAY;YAEX+B;QADpB,IAAI,CAACQ,SAASO,UAAU;QACxB,MAAMK,OAAOxB,QAAOI,gBAAAA,QAAQoB,IAAI,YAAZpB,gBAAgBA,QAAQqB,GAAG;QAC/C,MAAMC,WAAW,MAAMZ,QAAQ;YAC7Ba,OAAO;YACPC,aACE,CAAC,CAAC,EAAEJ,KAAK,oDAAoD,CAAC,GAC9D,gEACA;YACFK,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,EACE,oEAAoE;QACpE,6DAA6D;SAC5DC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,UAAU;QACfN,YAAY;QACZ,IAAI;YACF,MAAMc,UAAU,MAAMvD,0BAA0BkC,SAAST,QAAQqB,GAAG;YACpE,IAAI,CAACS,QAAQC,OAAO,EAAE;gBACpBpB,gBACE,GAAGtC,qBAAqB2D,cAAc,GAAG,wBAAwB,CAAC,GAChE,CAAC,MAAM,EAAEZ,KAAK,4CAA4C,CAAC,EAC7D;oBAAEa,SAAS;gBAAU;gBAEvB;YACF;YACA,iEAAiE;YACjE,mEAAmE;YACnE,YAAY;YACZrB,YAAY,mBAAmB;gBAAEsB,MAAM;gBAAWC,IAAInC,QAAQqB,GAAG;gBAAED;YAAK;YACxET,gBACEmB,QAAQM,QAAQ,GACZ,CAAC,kCAAkC,EAAEN,QAAQM,QAAQ,CAACJ,cAAc,GAAG,CAAC,CAAC,GACvE,CAAC,OAAO,EAAEF,QAAQM,QAAQ,KAAK,IAAI,KAAK,KAAK,GAC/C,mBACJ;gBAAEH,SAAS;gBAAWI,SAAS;YAAM;YAEvC9B;QACF,EAAE,OAAO+B,OAAO;YACdC,QAAQD,KAAK,CAACA;YACd3B,gBAAgBrC,4BAA4BgE,QAAQ;gBAClDL,SAAS;gBACTO,gBAAgB;YAClB;QACF,SAAU;YACRxB,YAAY;QACd;IACF,GAAG;QAACR;QAAOO;QAAUf;QAASU;QAASD;QAASG;QAAaD;QAAiBJ;KAAU;IAExF,MAAMvB,UAAUD,cAAciB,QAAQhB,OAAO;IAC7C,kEAAkE;IAClE,6DAA6D;IAC7D,MAAMyD,OAAmD;QACvD;YACEC,OAAO;YACPC,OAAO3C,QAAQ4C,OAAO,iBACpB,KAAC9E;gBAAK+E,MAAM7C,QAAQ4C,OAAO;gBAAEE,QAAO;gBAASC,KAAI;0BAC9C/C,QAAQ4C,OAAO;iBAEhB;QACN;QACA,0EAA0E;QAC1E;YAAEF,OAAO;YAASC,qBAAO,KAAChE;gBAAaqE,OAAOhD,QAAQgD,KAAK;;QAAK;QAChE;YAAEN,OAAO;YAAWC,OAAO3D;QAAQ;QACnC;YAAE0D,OAAO;YAAQC,OAAO,EAAC3C,gBAAAA,QAAQiD,IAAI,YAAZjD,gBAAgB,EAAE,EAAET,IAAI,CAAC;QAAM;QACxD;YAAEmD,OAAO;YAASC,OAAO3C,QAAQkD,KAAK;QAAC;WACpCjC,OAAOkC,MAAM,CAACzD,GAAG,CAAC,CAAC0D;gBAC8BpD;YAAlD,MAAMqD,OAAO5E,yBAAyB2E,aAAYpD,kBAAAA,QAAQsD,MAAM,qBAAdtD,eAAgB,CAACoD,WAAWG,GAAG,CAAC;YAClF,OAAO;gBACLb,OAAOU,WAAWV,KAAK,IAAIU,WAAWG,GAAG;gBACzCZ,OACEU,QAAQD,WAAWlB,IAAI,KAAK,sBAC1B,KAACpE;oBAAK+E,MAAMQ;oBAAMP,QAAO;oBAASC,KAAI;8BACnCM;qBAGHA;YAEN;QACF;KACD;IAED,qBACE,MAACxE;QACC2E,MAAK;QACLjC,OAAO3B,OAAOI,QAAQoB,IAAI,IAAIpB,QAAQqB,GAAG;QACzCoC,UAAUzD,QAAQ0D,MAAM;QACxBC,MAAMpG,eAAe,aAAa;YAAEqG,QAAQ;QAAmB;QAC/DC,UAAUvD,OAAOwD,OAAO,CAAC;QACzBC,WAAU;QACVC,uBACE;;8BACE,KAACtF;oBACCwB,QAAQA;oBACRC,KAAKA;oBACL8D,MAAM;wBAAEC,WAAWlE,QAAQqB,GAAG;oBAAC;oBAC/B2B,OAAOhD,QAAQgD,KAAK;;8BAEtB,KAACnF;oBACCsG,MAAK;oBACLxC,OAAM;oBACNM,SAAQ;oBACRmC,yBAAW,KAAC1G;wBAAQ2G,MAAM5G,iBAAiB4G,IAAI;wBAAEF,MAAM;;oBACvDG,SAAS,IAAMxD,WAAW;8BAEzB;;;;QAIPyD,WAAW;YACT;gBACEhB,KAAK;gBACLb,OAAO3B,WAAW,cAAc;gBAChCyD,oBAAM,KAAC9G;oBAAQ2G,MAAM7G,iBAAiB6G,IAAI;oBAAEF,MAAM;;gBAClDM,aAAa;gBACbC,UAAU,CAAClE,SAASO;gBACpB4D,gBAAgB5D,WAAW,iCAAiC;gBAC5DuD,SAAS,IAAM,KAAKnD;YACtB;SACD;QACDyD,qBACE;;8BACE,KAAChG;oBAAc8D,OAAM;oBAAWC,OAAO3C,QAAQ6E,QAAQ;;8BACvD,KAACjG;oBACC8D,OAAM;oBACNC,OACE3C,QAAQ8E,QAAQ,GACZzE,QAAQ0E,KAAK,GACX1E,QAAQ2E,QAAQ,CAAChF,QAAQ8E,QAAQ,IACjC,MACFG;;;;;0BAMZ,KAAClH;gBAAMmH,SAAS;0BACbzC,KAAK/C,GAAG,CAAC,CAACyF,oBACT,MAACpH;wBAECqH,WAAU;wBACVF,SAAS;wBACTG,IAAI;4BAAEC,YAAY;wBAAW;;0CAE7B,KAACtH;gCACCiE,SAAQ;gCACRN,OAAM;gCACN0D,IAAI;oCAAEE,UAAU;oCAAIC,YAAY;gCAAE;0CAEjCL,IAAIzC,KAAK;;0CAEZ,KAAC1E;gCACCiE,SAAQ;gCACRN,OAAOwD,IAAIxC,KAAK,GAAG,iBAAiB;gCACpC0C,IAAI;oCAAEI,YAAY;oCAAYC,cAAc;gCAAW;0CAEtDP,IAAIxC,KAAK,IAAI;;;uBAjBXwC,IAAIzC,KAAK;;YAsBnB7B,wBACC,KAACrC;gBACCmH,IAAI;gBACJC,SAAS,IAAM9E,WAAW;gBAC1BZ,QAAQA;gBACRC,KAAKA;gBACLH,SAASA;gBACTC,MAAMA;gBACNI,SAASA;gBACTwF,SAAS,IAAM/E,WAAW;iBAE1B;;;AAGV;AACAhB,sBAAsBgG,WAAW,GAAG;AAEpC,eAAehG,sBAAqB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ]\n // The org's own company fields (AGL-2661), after the built-in ones under\n // \"More fields\", as a record's Details keep them; none defined, none drawn.\n const moreRows: Array<{ label: string; value: ReactNode }> = fields.active.map(\n (definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n },\n )\n const factRows = (list: Array<{ label: string; value: ReactNode }>) =>\n list.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {factRows(rows)}\n {moreRows.length ? (\n <>\n <Typography variant=\"subtitle2\" sx={{ pt: 1 }}>\n {'More fields'}\n </Typography>\n {factRows(moreRows)}\n </>\n ) : null}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","moreRows","active","definition","text","custom","key","factRows","list","row","direction","spacing","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","length","pt","open","onClose","onSaved","displayName"],"mappings":"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"}
|
|
@@ -45,8 +45,9 @@ export interface ContactAssociationsCardProps {
|
|
|
45
45
|
* lists and each email's own picker, and the helper says so where a
|
|
46
46
|
* reader would otherwise assume the opposite.
|
|
47
47
|
*
|
|
48
|
-
* The filing is the one editable thing here and has its own Save
|
|
49
|
-
* the same stale-seed guard the properties card
|
|
48
|
+
* The filing is the one editable thing here and has its own **Save filing**
|
|
49
|
+
* in the card header, through the same stale-seed guard the properties card
|
|
50
|
+
* uses: a campaign membership
|
|
50
51
|
* written over a cached read could revert somebody else's filing. The save
|
|
51
52
|
* goes to `crm/contact-update` (AGL-2804), because the filing is this
|
|
52
53
|
* holder's facet and a facet is the server's to write; like every field that
|
|
@@ -54,8 +54,9 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
54
54
|
* lists and each email's own picker, and the helper says so where a
|
|
55
55
|
* reader would otherwise assume the opposite.
|
|
56
56
|
*
|
|
57
|
-
* The filing is the one editable thing here and has its own Save
|
|
58
|
-
* the same stale-seed guard the properties card
|
|
57
|
+
* The filing is the one editable thing here and has its own **Save filing**
|
|
58
|
+
* in the card header, through the same stale-seed guard the properties card
|
|
59
|
+
* uses: a campaign membership
|
|
59
60
|
* written over a cached read could revert somebody else's filing. The save
|
|
60
61
|
* goes to `crm/contact-update` (AGL-2804), because the filing is this
|
|
61
62
|
* holder's facet and a facet is the server's to write; like every field that
|
|
@@ -181,6 +182,15 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
181
182
|
}),
|
|
182
183
|
contentGutterX: true,
|
|
183
184
|
contentGutterY: true,
|
|
185
|
+
HeaderProps: {
|
|
186
|
+
action: /*#__PURE__*/ _jsx(Button, {
|
|
187
|
+
size: "small",
|
|
188
|
+
variant: "outlined",
|
|
189
|
+
disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
|
|
190
|
+
onClick: ()=>void handleSaveFiling(),
|
|
191
|
+
children: saving ? 'Saving…' : 'Save filing'
|
|
192
|
+
})
|
|
193
|
+
},
|
|
184
194
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
185
195
|
spacing: 2,
|
|
186
196
|
children: [
|
|
@@ -296,16 +306,6 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
296
306
|
helperText: "Your own filing. It never adds anyone to a send — a campaign mails its lists.",
|
|
297
307
|
empty: siteCampaigns.ready && !siteCampaigns.options.length,
|
|
298
308
|
emptyText: hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'
|
|
299
|
-
}),
|
|
300
|
-
/*#__PURE__*/ _jsx(Stack, {
|
|
301
|
-
direction: "row",
|
|
302
|
-
children: /*#__PURE__*/ _jsx(Button, {
|
|
303
|
-
size: "small",
|
|
304
|
-
variant: "outlined",
|
|
305
|
-
disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
|
|
306
|
-
onClick: ()=>void handleSaveFiling(),
|
|
307
|
-
children: saving ? 'Saving…' : 'Save filing'
|
|
308
|
-
})
|
|
309
309
|
})
|
|
310
310
|
]
|
|
311
311
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own Save, through\n * the same stale-seed guard the properties card uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","spacing","length","direction","sx","flexWrap","rowGap","map","source","label","size","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","options","value","onChange","helperText","empty","ready","emptyText","disabled","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own **Save filing**\n * in the card header, through the same stale-seed guard the properties card\n * uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","ready","options","length","onClick","spacing","direction","sx","flexWrap","rowGap","map","source","label","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","value","onChange","helperText","empty","emptyText","displayName"],"mappings":"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"}
|
|
@@ -25,9 +25,10 @@ import { type CrmDetailPageProps } from '../model/crm-routes';
|
|
|
25
25
|
* ## The cards
|
|
26
26
|
*
|
|
27
27
|
* Each in its own file, so a card can grow without this page growing:
|
|
28
|
-
* properties (the editable profile
|
|
29
|
-
* consent, campaign filing) and the
|
|
30
|
-
* here by inserting one import and
|
|
28
|
+
* properties (the editable profile, with the org's custom fields under it),
|
|
29
|
+
* the relationship (sources, attribution, consent, campaign filing) and the
|
|
30
|
+
* timeline. Other CRM sections add a card here by inserting one import and
|
|
31
|
+
* one line.
|
|
31
32
|
*
|
|
32
33
|
* ## Delete is a detach
|
|
33
34
|
*
|
|
@@ -21,7 +21,9 @@ import { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from "@aglyn/aglyn";
|
|
|
21
21
|
import { mdiDeleteOutline, mdiMerge } from "@aglyn/shared-data-mdi";
|
|
22
22
|
import { AppLink, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
23
23
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
|
-
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
24
|
+
import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
|
|
25
|
+
import { restampCrmListFields } from "../model/list-fields-api.js";
|
|
26
|
+
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
25
27
|
import { Stack, Tooltip, Typography } from "@mui/material";
|
|
26
28
|
import { arrayRemove, deleteDoc, deleteField, doc, updateDoc } from "firebase/firestore";
|
|
27
29
|
import { useParams, useRouter } from "next/navigation";
|
|
@@ -32,7 +34,6 @@ import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
|
32
34
|
import { contactPrimaryGroup, contactRecordFromDoc } from "../model/contact-record.js";
|
|
33
35
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
34
36
|
import ContactAssociationsCard from "./contact-associations-card.js";
|
|
35
|
-
import ContactCustomFieldsCard from "./contact-custom-fields-card.js";
|
|
36
37
|
import ContactDuplicatesCard from "./contact-duplicates-card.js";
|
|
37
38
|
import ContactKnownByCard from "./contact-known-by-card.js";
|
|
38
39
|
import ContactMergeDialog, { useContactMergeDialog } from "./contact-merge-dialog.js";
|
|
@@ -82,9 +83,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
82
83
|
* ## The cards
|
|
83
84
|
*
|
|
84
85
|
* Each in its own file, so a card can grow without this page growing:
|
|
85
|
-
* properties (the editable profile
|
|
86
|
-
* consent, campaign filing) and the
|
|
87
|
-
* here by inserting one import and
|
|
86
|
+
* properties (the editable profile, with the org's custom fields under it),
|
|
87
|
+
* the relationship (sources, attribution, consent, campaign filing) and the
|
|
88
|
+
* timeline. Other CRM sections add a card here by inserting one import and
|
|
89
|
+
* one line.
|
|
88
90
|
*
|
|
89
91
|
* ## Delete is a detach
|
|
90
92
|
*
|
|
@@ -199,6 +201,7 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
199
201
|
emailsHub,
|
|
200
202
|
hostId
|
|
201
203
|
]);
|
|
204
|
+
const { data: user } = useUser();
|
|
202
205
|
const handleRemove = useCallback(async ()=>{
|
|
203
206
|
var _ref;
|
|
204
207
|
if (!row || !scope) return;
|
|
@@ -225,6 +228,12 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
225
228
|
capturedByHostIds: arrayRemove(...plan.removeHostIds),
|
|
226
229
|
updatedAt: new Date()
|
|
227
230
|
}));
|
|
231
|
+
// The facet and the scope just let go of are what the Contacts list
|
|
232
|
+
// filters and searches by; the rules keep a client from restamping
|
|
233
|
+
// them, so the route does (AGL-3321).
|
|
234
|
+
await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [
|
|
235
|
+
id
|
|
236
|
+
]);
|
|
228
237
|
}
|
|
229
238
|
// `siteLabel` and not "this site": at the organization level the line
|
|
230
239
|
// lands in a feed that spans every site, so it has to name the one.
|
|
@@ -257,7 +266,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
257
266
|
routes,
|
|
258
267
|
row,
|
|
259
268
|
scope,
|
|
260
|
-
siteLabel
|
|
269
|
+
siteLabel,
|
|
270
|
+
user,
|
|
271
|
+
hostId,
|
|
272
|
+
orgId
|
|
261
273
|
]);
|
|
262
274
|
const overflowItems = [
|
|
263
275
|
/*
|
|
@@ -456,12 +468,6 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
|
|
|
456
468
|
basePath: basePath,
|
|
457
469
|
bookingsHref: bookingsHref
|
|
458
470
|
}),
|
|
459
|
-
suiteIncluded ? /*#__PURE__*/ _jsx(ContactCustomFieldsCard, {
|
|
460
|
-
hostId: hostId,
|
|
461
|
-
org: org,
|
|
462
|
-
contactId: id,
|
|
463
|
-
basePath: basePath
|
|
464
|
-
}) : null,
|
|
465
471
|
/*#__PURE__*/ _jsx(ContactTimelineCard, {
|
|
466
472
|
hostId: hostId,
|
|
467
473
|
org: org,
|