@aglyn/plugins-crm 1.0.0-beta.200 → 1.0.0-beta.202
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/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 +4 -10
- package/src/lib/components/contact-detail-page.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-section.js +197 -198
- 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/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.js +9 -14
- 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 +32 -35
- 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 +317 -313
- 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/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 +23 -23
- package/src/lib/components/tasks-section.js.map +1 -1
- 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/model/lead-filters.d.ts +1 -1
- package/src/lib/server/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import * as Aglyn from "@aglyn/aglyn";
|
|
20
20
|
import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, crmTelHref, normalizePhone } from "@aglyn/aglyn";
|
|
21
21
|
import { mdiPhoneOutline } from "@aglyn/shared-data-mdi";
|
|
@@ -23,19 +23,37 @@ import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
|
23
23
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
24
|
import { useUser, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
25
25
|
import { Button, Grid, IconButton, InputAdornment, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
|
26
|
-
import { useCallback, useEffect, useState } from "react";
|
|
26
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
27
|
+
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
27
28
|
import { useContactUpdate } from "../hooks/use-contact-update.js";
|
|
28
29
|
import { useCrmActivityLogger } from "../hooks/use-crm-activity-logger.js";
|
|
29
30
|
import { useLeadSourcePicklist } from "../hooks/use-lead-source-picklist.js";
|
|
30
31
|
import { parseContactTags } from "../model/contact-record.js";
|
|
31
32
|
import { setContactStage } from "../model/crm-api.js";
|
|
33
|
+
import { crmCustomDraftChanges, crmCustomDraftMissingRequired, crmCustomDraftValue } from "../model/crm-custom-draft.js";
|
|
32
34
|
import { CompanyPicker, useCompanyOptions, useCreateCompany } from "./company-picker.js";
|
|
33
35
|
import { addressDraftFrom, ContactAddressFields } from "./contact-address-fields.js";
|
|
36
|
+
import { CrmCustomFieldControl } from "./crm-custom-field-control.js";
|
|
34
37
|
import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
35
38
|
import { LeadSourceSelect } from "./lead-source-select.js";
|
|
36
39
|
/**
|
|
37
40
|
* THE RECORD ITSELF: every field a team keeps on a person, in one card with
|
|
38
|
-
* one Save (AGL-2596).
|
|
41
|
+
* one Save in its header (AGL-2596).
|
|
42
|
+
*
|
|
43
|
+
* ## Custom fields sit with the built-in ones
|
|
44
|
+
*
|
|
45
|
+
* The org's own contact fields (AGL-2601) are drawn under **More fields**,
|
|
46
|
+
* after the built-in properties, the way Salesforce keeps custom fields in
|
|
47
|
+
* the record's Details beside the standard ones — and they save with them.
|
|
48
|
+
* An org with no active field draws no subsection at all.
|
|
49
|
+
*
|
|
50
|
+
* A custom value is THIS holder's (`facets.{group}.custom.{key}`), so the
|
|
51
|
+
* draft holds only the keys the reader touched and the save sends only the
|
|
52
|
+
* keys that changed: the route writes each at its own dotted path, and a
|
|
53
|
+
* `custom` map sent whole would take every key this card did not touch out
|
|
54
|
+
* with it. A cleared control sends `null`, the explicit "cleared" the model
|
|
55
|
+
* keeps the key present with, and a required field left empty is refused
|
|
56
|
+
* before anything is sent.
|
|
39
57
|
*
|
|
40
58
|
* ## One save, one request, one guard
|
|
41
59
|
*
|
|
@@ -123,13 +141,27 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
123
141
|
const [address, setAddress] = useState(addressDraftFrom(record.address));
|
|
124
142
|
const [phoneError, setPhoneError] = useState('');
|
|
125
143
|
const [saving, setSaving] = useState(false);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
/** The custom values the reader touched; every other key reads from the record. */ const [custom, setCustom] = useState({});
|
|
145
|
+
/** Something was edited since the card was seeded or last saved. */ const [edited, setEdited] = useState(false);
|
|
146
|
+
const fields = useContactFieldDefinitions(suiteLocked ? null : orgId);
|
|
147
|
+
// The custom fields are the CRM's; a plan without it draws and sends none.
|
|
148
|
+
const customFields = suiteLocked ? [] : fields.active;
|
|
149
|
+
const storedCustom = useMemo(()=>{
|
|
150
|
+
var _record_custom;
|
|
151
|
+
return (_record_custom = record.custom) != null ? _record_custom : {};
|
|
152
|
+
}, [
|
|
153
|
+
record.custom
|
|
154
|
+
]);
|
|
155
|
+
const customChanges = useMemo(()=>crmCustomDraftChanges(storedCustom, custom), [
|
|
156
|
+
storedCustom,
|
|
157
|
+
custom
|
|
158
|
+
]);
|
|
159
|
+
const clearingRequired = crmCustomDraftMissingRequired(customFields, storedCustom, custom, 'edit').length > 0;
|
|
160
|
+
/** One field's edit: the value, and the card now has something to save. */ const change = useCallback((set, value)=>{
|
|
161
|
+
set(value);
|
|
162
|
+
setEdited(true);
|
|
163
|
+
}, []);
|
|
164
|
+
/** Every draft back to what the record holds — the seed, and Discard. */ const reseed = ()=>{
|
|
133
165
|
setNameOverride(record.nameOverride);
|
|
134
166
|
setPhone(record.phone);
|
|
135
167
|
setJobTitle(record.jobTitle);
|
|
@@ -142,11 +174,23 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
142
174
|
setNotes(record.notes);
|
|
143
175
|
setAddress(addressDraftFrom(record.address));
|
|
144
176
|
setPhoneError('');
|
|
177
|
+
setCustom({});
|
|
178
|
+
setEdited(false);
|
|
179
|
+
};
|
|
180
|
+
/*
|
|
181
|
+
* Re-seeded when the RECORD changes, not when it re-renders. The listener
|
|
182
|
+
* delivers a fresh row on every snapshot, including the one this card's
|
|
183
|
+
* own save produces; re-seeding on each would overwrite what somebody is
|
|
184
|
+
* in the middle of typing with what the server last confirmed.
|
|
185
|
+
*/ const recordId = record.$id;
|
|
186
|
+
useEffect(()=>{
|
|
187
|
+
reseed();
|
|
145
188
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
189
|
}, [
|
|
147
190
|
recordId
|
|
148
191
|
]);
|
|
149
192
|
const handleSave = useCallback(async ()=>{
|
|
193
|
+
if (clearingRequired) return;
|
|
150
194
|
const trimmedPhone = phone.trim();
|
|
151
195
|
const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : '';
|
|
152
196
|
if (trimmedPhone && !normalizedPhone) {
|
|
@@ -171,7 +215,9 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
171
215
|
ownerUid,
|
|
172
216
|
companyId,
|
|
173
217
|
companyName: companyName.trim().slice(0, 120)
|
|
174
|
-
}
|
|
218
|
+
}, !suiteLocked && customChanges.length ? {
|
|
219
|
+
custom: Object.fromEntries(customChanges)
|
|
220
|
+
} : {});
|
|
175
221
|
setSaving(true);
|
|
176
222
|
try {
|
|
177
223
|
const verdict = await writeGuardedBySeed({
|
|
@@ -190,6 +236,8 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
190
236
|
id: record.$id,
|
|
191
237
|
name: record.name || record.email
|
|
192
238
|
});
|
|
239
|
+
// The custom values are stored; the controls read the record again.
|
|
240
|
+
setCustom({});
|
|
193
241
|
if (stageChanged && siteHostId) {
|
|
194
242
|
try {
|
|
195
243
|
await setContactStage(user, siteHostId, record.$id, lifecycleStage || null);
|
|
@@ -202,6 +250,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
202
250
|
});
|
|
203
251
|
}
|
|
204
252
|
}
|
|
253
|
+
setEdited(false);
|
|
205
254
|
enqueueSnackbar('Contact saved', {
|
|
206
255
|
variant: 'success',
|
|
207
256
|
persist: false
|
|
@@ -217,9 +266,11 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
217
266
|
}
|
|
218
267
|
}, [
|
|
219
268
|
address,
|
|
269
|
+
clearingRequired,
|
|
220
270
|
companyId,
|
|
221
271
|
companyName,
|
|
222
272
|
contactUpdate,
|
|
273
|
+
customChanges,
|
|
223
274
|
enqueueSnackbar,
|
|
224
275
|
siteHostId,
|
|
225
276
|
jobTitle,
|
|
@@ -256,13 +307,25 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
256
307
|
contentGutterX: true,
|
|
257
308
|
contentGutterY: true,
|
|
258
309
|
HeaderProps: {
|
|
259
|
-
action: /*#__PURE__*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
310
|
+
action: /*#__PURE__*/ _jsxs(Stack, {
|
|
311
|
+
direction: "row",
|
|
312
|
+
spacing: 1,
|
|
313
|
+
children: [
|
|
314
|
+
edited ? /*#__PURE__*/ _jsx(Button, {
|
|
315
|
+
size: "small",
|
|
316
|
+
disabled: saving,
|
|
317
|
+
onClick: reseed,
|
|
318
|
+
children: 'Discard changes'
|
|
319
|
+
}) : null,
|
|
320
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
321
|
+
variant: "contained",
|
|
322
|
+
color: "primary",
|
|
323
|
+
size: "small",
|
|
324
|
+
disabled: saving || !edited || clearingRequired,
|
|
325
|
+
onClick: ()=>void handleSave(),
|
|
326
|
+
children: saving ? 'Saving…' : 'Save'
|
|
327
|
+
})
|
|
328
|
+
]
|
|
266
329
|
})
|
|
267
330
|
},
|
|
268
331
|
children: /*#__PURE__*/ _jsxs(Grid, {
|
|
@@ -296,7 +359,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
296
359
|
size: "small",
|
|
297
360
|
label: "Name",
|
|
298
361
|
value: nameOverride,
|
|
299
|
-
onChange: (event)=>setNameOverride
|
|
362
|
+
onChange: (event)=>change(setNameOverride, event.target.value),
|
|
300
363
|
slotProps: {
|
|
301
364
|
htmlInput: {
|
|
302
365
|
maxLength: 120
|
|
@@ -315,7 +378,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
315
378
|
size: "small",
|
|
316
379
|
label: "Phone",
|
|
317
380
|
value: phone,
|
|
318
|
-
onChange: (event)=>setPhone
|
|
381
|
+
onChange: (event)=>change(setPhone, event.target.value),
|
|
319
382
|
error: Boolean(phoneError),
|
|
320
383
|
helperText: phoneError || 'With the country code, like +1 512 555 0107',
|
|
321
384
|
fullWidth: true,
|
|
@@ -356,7 +419,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
356
419
|
size: "small",
|
|
357
420
|
label: "Job title",
|
|
358
421
|
value: jobTitle,
|
|
359
|
-
onChange: (event)=>setJobTitle
|
|
422
|
+
onChange: (event)=>change(setJobTitle, event.target.value),
|
|
360
423
|
slotProps: {
|
|
361
424
|
htmlInput: {
|
|
362
425
|
maxLength: 120
|
|
@@ -374,7 +437,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
374
437
|
picklist: leadSources.picklist,
|
|
375
438
|
value: leadSource,
|
|
376
439
|
stored: record.leadSource,
|
|
377
|
-
onChange: setLeadSource
|
|
440
|
+
onChange: (value)=>change(setLeadSource, value)
|
|
378
441
|
})
|
|
379
442
|
}),
|
|
380
443
|
/*#__PURE__*/ _jsx(Grid, {
|
|
@@ -388,7 +451,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
388
451
|
truncated: companies.truncated,
|
|
389
452
|
value: companyId,
|
|
390
453
|
onChange: (id, company)=>{
|
|
391
|
-
setCompanyId
|
|
454
|
+
change(setCompanyId, id);
|
|
392
455
|
// The label follows the link: the picked name, or nothing
|
|
393
456
|
// once the link is cleared. A company the list cannot name
|
|
394
457
|
// keeps whatever label the record already had.
|
|
@@ -411,7 +474,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
411
474
|
size: "small",
|
|
412
475
|
label: "Lifecycle stage",
|
|
413
476
|
value: lifecycleStage,
|
|
414
|
-
onChange: (event)=>setLifecycleStage
|
|
477
|
+
onChange: (event)=>change(setLifecycleStage, event.target.value),
|
|
415
478
|
disabled: suiteLocked,
|
|
416
479
|
helperText: suiteLocked ? crmSuiteLockedReason() : undefined,
|
|
417
480
|
fullWidth: true,
|
|
@@ -437,7 +500,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
437
500
|
size: "small",
|
|
438
501
|
label: "Owner",
|
|
439
502
|
value: ownerUid,
|
|
440
|
-
onChange: (event)=>setOwnerUid
|
|
503
|
+
onChange: (event)=>change(setOwnerUid, event.target.value),
|
|
441
504
|
disabled: suiteLocked,
|
|
442
505
|
helperText: suiteLocked ? crmSuiteLockedReason() : members.ready && !members.options.length ? 'The team roster could not be read, so nobody can be picked yet.' : 'The team member responsible for this relationship',
|
|
443
506
|
fullWidth: true,
|
|
@@ -468,7 +531,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
468
531
|
placeholder: "vip, beta",
|
|
469
532
|
helperText: "Comma-separated",
|
|
470
533
|
value: tags,
|
|
471
|
-
onChange: (event)=>setTags
|
|
534
|
+
onChange: (event)=>change(setTags, event.target.value),
|
|
472
535
|
fullWidth: true
|
|
473
536
|
})
|
|
474
537
|
}),
|
|
@@ -485,7 +548,7 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
485
548
|
}),
|
|
486
549
|
/*#__PURE__*/ _jsx(ContactAddressFields, {
|
|
487
550
|
value: address,
|
|
488
|
-
onChange: setAddress
|
|
551
|
+
onChange: (value)=>change(setAddress, value)
|
|
489
552
|
})
|
|
490
553
|
]
|
|
491
554
|
})
|
|
@@ -498,13 +561,50 @@ import { LeadSourceSelect } from "./lead-source-select.js";
|
|
|
498
561
|
size: "small",
|
|
499
562
|
label: "About",
|
|
500
563
|
value: notes,
|
|
501
|
-
onChange: (event)=>setNotes
|
|
564
|
+
onChange: (event)=>change(setNotes, event.target.value),
|
|
502
565
|
helperText: "Notes for your team. Nobody outside this site's group can read them.",
|
|
503
566
|
multiline: true,
|
|
504
567
|
minRows: 3,
|
|
505
568
|
fullWidth: true
|
|
506
569
|
})
|
|
507
|
-
})
|
|
570
|
+
}),
|
|
571
|
+
customFields.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
572
|
+
children: [
|
|
573
|
+
/*#__PURE__*/ _jsx(Grid, {
|
|
574
|
+
size: {
|
|
575
|
+
xs: 12
|
|
576
|
+
},
|
|
577
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
578
|
+
variant: "subtitle2",
|
|
579
|
+
children: 'More fields'
|
|
580
|
+
})
|
|
581
|
+
}),
|
|
582
|
+
customFields.map((definition)=>/*#__PURE__*/ _jsx(Grid, {
|
|
583
|
+
size: {
|
|
584
|
+
xs: 12,
|
|
585
|
+
sm: 6
|
|
586
|
+
},
|
|
587
|
+
children: /*#__PURE__*/ _jsx(CrmCustomFieldControl, {
|
|
588
|
+
definition: definition,
|
|
589
|
+
value: crmCustomDraftValue(storedCustom, custom, definition.key),
|
|
590
|
+
onChange: (value)=>change(setCustom, _extends({}, custom, {
|
|
591
|
+
[definition.key]: value
|
|
592
|
+
})),
|
|
593
|
+
disabled: saving
|
|
594
|
+
})
|
|
595
|
+
}, definition.$id)),
|
|
596
|
+
clearingRequired ? /*#__PURE__*/ _jsx(Grid, {
|
|
597
|
+
size: {
|
|
598
|
+
xs: 12
|
|
599
|
+
},
|
|
600
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
601
|
+
variant: "caption",
|
|
602
|
+
color: "error",
|
|
603
|
+
children: 'A required field cannot be left empty.'
|
|
604
|
+
})
|
|
605
|
+
}) : null
|
|
606
|
+
]
|
|
607
|
+
}) : null
|
|
508
608
|
]
|
|
509
609
|
})
|
|
510
610
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 * as Aglyn from '@aglyn/aglyn'\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport { LeadSourceSelect } from './lead-source-select'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record 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 /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /**\n * The org the record belongs to, as the page resolved it — for the org's\n * lead source values (AGL-3298). Null while it settles, when the select\n * offers the starter list and the save still sends only what changed.\n */\n orgId?: string | null\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\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 lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save (AGL-2596).\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n orgId = null,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n // Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.\n const [leadSource, setLeadSource] = useState(record.leadSource)\n const leadSources = useLeadSourcePicklist(orgId)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setLeadSource(record.leadSource)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n // Sent only when changed: the route judges a sent value against the\n // org's list, and an untouched one needs no judging.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n }\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 () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n enqueueSnackbar('Contact 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 }, [\n address,\n companyId,\n companyName,\n contactUpdate,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => setNameOverride(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={leadSource}\n stored={record.leadSource}\n onChange={setLeadSource}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n setCompanyId(id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useState","useContactUpdate","useCrmActivityLogger","useLeadSourcePicklist","parseContactTags","setContactStage","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","crmSuiteLockedReason","LeadSourceSelect","ContactPropertiesCard","props","hostId","org","record","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","set","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","color","size","disabled","onClick","container","spacing","xs","sm","label","value","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","picklist","stored","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","length","memberName","owner","crmMemberPickerLabel","placeholder","multiline","minRows","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 * as Aglyn from '@aglyn/aglyn'\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n type CrmCustomDraft,\n crmCustomDraftChanges,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n} from '../model/crm-custom-draft'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport { LeadSourceSelect } from './lead-source-select'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record 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 /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /**\n * The org the record belongs to, as the page resolved it — for the org's\n * lead source values (AGL-3298) and its custom contact fields (AGL-2601).\n * Null while it settles, when the select offers the starter list, no\n * custom field is drawn yet, and the save still sends only what changed.\n */\n orgId?: string | null\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\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 lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too. The\n * custom fields are the CRM's as well, and are not drawn.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save in its header (AGL-2596).\n *\n * ## Custom fields sit with the built-in ones\n *\n * The org's own contact fields (AGL-2601) are drawn under **More fields**,\n * after the built-in properties, the way Salesforce keeps custom fields in\n * the record's Details beside the standard ones — and they save with them.\n * An org with no active field draws no subsection at all.\n *\n * A custom value is THIS holder's (`facets.{group}.custom.{key}`), so the\n * draft holds only the keys the reader touched and the save sends only the\n * keys that changed: the route writes each at its own dotted path, and a\n * `custom` map sent whole would take every key this card did not touch out\n * with it. A cleared control sends `null`, the explicit \"cleared\" the model\n * keeps the key present with, and a required field left empty is refused\n * before anything is sent.\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n orgId = null,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n // Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.\n const [leadSource, setLeadSource] = useState(record.leadSource)\n const leadSources = useLeadSourcePicklist(orgId)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n /** The custom values the reader touched; every other key reads from the record. */\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /** Something was edited since the card was seeded or last saved. */\n const [edited, setEdited] = useState(false)\n const fields = useContactFieldDefinitions(suiteLocked ? null : orgId)\n // The custom fields are the CRM's; a plan without it draws and sends none.\n const customFields = suiteLocked ? [] : fields.active\n const storedCustom = useMemo(() => record.custom ?? {}, [record.custom])\n const customChanges = useMemo(\n () => crmCustomDraftChanges(storedCustom, custom),\n [storedCustom, custom],\n )\n const clearingRequired =\n crmCustomDraftMissingRequired(customFields, storedCustom, custom, 'edit').length > 0\n\n /** One field's edit: the value, and the card now has something to save. */\n const change = useCallback(<T,>(set: (value: T) => void, value: T) => {\n set(value)\n setEdited(true)\n }, [])\n\n /** Every draft back to what the record holds — the seed, and Discard. */\n const reseed = () => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setLeadSource(record.leadSource)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n setCustom({})\n setEdited(false)\n }\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n reseed()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n if (clearingRequired) return\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n // Sent only when changed: the route judges a sent value against the\n // org's list, and an untouched one needs no judging.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n // Only the custom keys that changed — see the note above.\n ...(!suiteLocked && customChanges.length\n ? { custom: Object.fromEntries(customChanges) }\n : {}),\n }\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 () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n // The custom values are stored; the controls read the record again.\n setCustom({})\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n setEdited(false)\n enqueueSnackbar('Contact 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 }, [\n address,\n clearingRequired,\n companyId,\n companyName,\n contactUpdate,\n customChanges,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {edited ? (\n <Button size=\"small\" disabled={saving} onClick={reseed}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving || !edited || clearingRequired}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => change(setNameOverride, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => change(setPhone, event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => change(setJobTitle, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={leadSource}\n stored={record.leadSource}\n onChange={(value) => change(setLeadSource, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n change(setCompanyId, id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n change(setLifecycleStage, event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => change(setOwnerUid, event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => change(setTags, event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields\n value={address}\n onChange={(value) => change(setAddress, value)}\n />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => change(setNotes, event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n {/*\n The org's own fields (AGL-2601), after the built-in ones and saved\n with them. Absent, not an empty box, while the org defines none.\n */}\n {customFields.length ? (\n <>\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n </Grid>\n {customFields.map((definition) => (\n <Grid key={definition.$id} size={{ xs: 12, sm: 6 }}>\n <CrmCustomFieldControl\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n change(setCustom, { ...custom, [definition.key]: value })\n }\n disabled={saving}\n />\n </Grid>\n ))}\n {clearingRequired ? (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"caption\" color=\"error\">\n {'A required field cannot be left empty.'}\n </Typography>\n </Grid>\n ) : null}\n </>\n ) : null}\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useContactUpdate","useCrmActivityLogger","useLeadSourcePicklist","parseContactTags","setContactStage","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","CrmCustomFieldControl","crmSuiteLockedReason","LeadSourceSelect","ContactPropertiesCard","props","hostId","org","record","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","custom","setCustom","edited","setEdited","fields","customFields","active","storedCustom","customChanges","clearingRequired","length","change","set","value","reseed","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","name","slice","Object","fromEntries","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","size","disabled","onClick","color","container","xs","sm","label","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","picklist","stored","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","memberName","owner","crmMemberPickerLabel","placeholder","multiline","minRows","definition","key","displayName"],"mappings":"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"}
|
|
@@ -54,12 +54,18 @@ export interface ContactTimelineCardProps {
|
|
|
54
54
|
* — THIS group's facet, filtered to the sites the group may see, the same
|
|
55
55
|
* projection the contacts list makes — and costs no read. The logged side is
|
|
56
56
|
* the bounded listener every activity surface uses, filtered to this
|
|
57
|
-
* contact,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* contact, a hundred at a time. The campaign side is one request to
|
|
58
|
+
* `crm/contact-email-history`, which reads down this person's own delivery
|
|
59
|
+
* log, capped, and never a campaign's recipients. The facet's history is
|
|
60
|
+
* capped at fifty by the platform, so past the window the stream is the
|
|
61
|
+
* logged side alone.
|
|
62
|
+
*
|
|
63
|
+
* ## How it reads
|
|
64
|
+
*
|
|
65
|
+
* Every entry is a collapsed row that opens in place, paged by the
|
|
66
|
+
* console's footer over the merged stream; a page turned past the logged
|
|
67
|
+
* window widens it by the next hundred. **Expand all** and **Log activity**
|
|
68
|
+
* sit in the card header, and edit and delete stay on the entry they act on.
|
|
63
69
|
*/
|
|
64
70
|
export declare function ContactTimelineCard(props: ContactTimelineCardProps): import("react").JSX.Element;
|
|
65
71
|
export declare namespace ContactTimelineCard {
|