@aglyn/plugins-crm 1.0.0-beta.200 → 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.
Files changed (78) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/company-properties-card.js +53 -36
  9. package/src/lib/components/company-properties-card.js.map +1 -1
  10. package/src/lib/components/contact-associations-card.d.ts +3 -2
  11. package/src/lib/components/contact-associations-card.js +12 -12
  12. package/src/lib/components/contact-associations-card.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -3
  14. package/src/lib/components/contact-detail-page.js +4 -10
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-properties-card.d.ts +21 -4
  17. package/src/lib/components/contact-properties-card.js +129 -29
  18. package/src/lib/components/contact-properties-card.js.map +1 -1
  19. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  20. package/src/lib/components/contact-timeline-card.js +132 -174
  21. package/src/lib/components/contact-timeline-card.js.map +1 -1
  22. package/src/lib/components/contacts-section.js +197 -198
  23. package/src/lib/components/contacts-section.js.map +1 -1
  24. package/src/lib/components/crm-email-state-chip.js +4 -1
  25. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  26. package/src/lib/components/deal-products-card.js +15 -13
  27. package/src/lib/components/deal-products-card.js.map +1 -1
  28. package/src/lib/components/deal-properties-card.js +7 -0
  29. package/src/lib/components/deal-properties-card.js.map +1 -1
  30. package/src/lib/components/deal-stage-card.js +31 -29
  31. package/src/lib/components/deal-stage-card.js.map +1 -1
  32. package/src/lib/components/deals-section.js +9 -14
  33. package/src/lib/components/deals-section.js.map +1 -1
  34. package/src/lib/components/fields-section.d.ts +4 -0
  35. package/src/lib/components/fields-section.js +368 -154
  36. package/src/lib/components/fields-section.js.map +1 -1
  37. package/src/lib/components/form-contact-fields-card.js +24 -20
  38. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  39. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +32 -35
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-properties-card.d.ts +14 -6
  43. package/src/lib/components/lead-properties-card.js +317 -313
  44. package/src/lib/components/lead-properties-card.js.map +1 -1
  45. package/src/lib/components/lead-surfaces-note.js +13 -2
  46. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  47. package/src/lib/components/linked-deals-card.js +12 -10
  48. package/src/lib/components/linked-deals-card.js.map +1 -1
  49. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  50. package/src/lib/components/recent-activity-feed.js +54 -29
  51. package/src/lib/components/recent-activity-feed.js.map +1 -1
  52. package/src/lib/components/record-activity-card.d.ts +4 -2
  53. package/src/lib/components/record-activity-card.js +29 -11
  54. package/src/lib/components/record-activity-card.js.map +1 -1
  55. package/src/lib/components/reports/activity-card.js +18 -12
  56. package/src/lib/components/reports/activity-card.js.map +1 -1
  57. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  58. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  59. package/src/lib/components/reports/pipeline-card.js +22 -16
  60. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  61. package/src/lib/components/reports/report-export.d.ts +4 -11
  62. package/src/lib/components/reports/report-export.js +16 -33
  63. package/src/lib/components/reports/report-export.js.map +1 -1
  64. package/src/lib/components/reports/source-conversion-card.js +18 -12
  65. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  66. package/src/lib/components/reports/tasks-card.js +35 -25
  67. package/src/lib/components/reports/tasks-card.js.map +1 -1
  68. package/src/lib/components/tasks-section.js +23 -23
  69. package/src/lib/components/tasks-section.js.map +1 -1
  70. package/src/lib/constants/field-list-query.d.ts +23 -0
  71. package/src/lib/constants/field-list-query.js +80 -0
  72. package/src/lib/constants/field-list-query.js.map +1 -0
  73. package/src/lib/model/lead-filters.d.ts +1 -1
  74. package/src/lib/server/capture-contact.js +7 -5
  75. package/src/lib/server/capture-contact.js.map +1 -1
  76. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  77. package/src/lib/components/contact-custom-fields-card.js +0 -204
  78. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
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 { mdiAccountCancelOutline } from "@aglyn/shared-data-mdi";
21
- import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
21
+ import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
22
22
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
23
  import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
24
24
  import { Alert, Button, FormControl, InputLabel, MenuItem, Select, Stack, TextField, Tooltip, Typography } from "@mui/material";
@@ -89,15 +89,23 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
89
89
  * What the team knows and decides about a lead: status, owner, notes, and
90
90
  * the identity and consent the capture recorded (AGL-2608).
91
91
  *
92
+ * Two cards. The record's header carries the identity, the status and owner,
93
+ * the consent the capture recorded and the record's actions. **Details**
94
+ * under it carries the profile, the org's own lead fields under **More
95
+ * fields**, and the notes — one card with one Save in its header, the way
96
+ * Salesforce keeps a record's standard and custom fields in one Details
97
+ * section.
98
+ *
92
99
  * Status and owner are single-field client writes — the rules let a site
93
100
  * admin, editor or author update `hosts/{hostId}/leads`, and a one-field
94
- * `update` cannot roll anything else back. Notes are a text field seeded
95
- * from the document, so that save goes through `writeGuardedBySeed`: a draft
96
- * edited over a cached read would otherwise overwrite a newer note with an
97
- * older one plus a sentence.
101
+ * `update` cannot roll anything else back. The details are drafts seeded
102
+ * from the document, so their save goes through `writeGuardedBySeed`: a
103
+ * draft edited over a cached read would otherwise overwrite a newer note or
104
+ * profile with an older one.
98
105
  *
99
- * Converted leads are read-only here. Their status is the conversion, and
100
- * the actions become links to what the conversion made.
106
+ * Converted leads are read-only here, but for the notes. Their status is the
107
+ * conversion, and the header's actions become links to what the conversion
108
+ * made.
101
109
  */ export function LeadPropertiesCard(props) {
102
110
  var _lead_phone, _lead_notes, _lead_email, _lead_email1, _lead_name, _lead_leadSource;
103
111
  const { hostId, consentGroup, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
@@ -121,16 +129,16 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
121
129
  // after the status it shows — see `LeadOwnerSelect`.
122
130
  const statusLabelId = useId();
123
131
  const [notesDirty, setNotesDirty] = useState(false);
124
- const [savingNotes, setSavingNotes] = useState(false);
125
132
  /*
126
133
  * THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,
127
- * address, tags, lead source — edited as one block with one Save, the
128
- * way the contact's Properties card saves. Seeded from the document and
129
- * guarded on save like the notes: a draft edited over a cached read must
130
- * not overwrite a newer profile with an older one.
134
+ * address, tags, lead source — edited in the Details card with the notes
135
+ * and the custom fields, under one Save, the way the contact's Properties
136
+ * card saves. Seeded from the document and guarded on save: a draft
137
+ * edited over a cached read must not overwrite a newer profile with an
138
+ * older one.
131
139
  */ const [profile, setProfile] = useState(()=>profileDraftFrom(lead));
132
140
  const [profileDirty, setProfileDirty] = useState(false);
133
- const [savingProfile, setSavingProfile] = useState(false);
141
+ const [saving, setSaving] = useState(false);
134
142
  const [profileErrors, setProfileErrors] = useState({});
135
143
  useEffect(()=>{
136
144
  if (!profileDirty) setProfile(profileDraftFrom(lead));
@@ -180,7 +188,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
180
188
  lead.notes,
181
189
  notesDirty
182
190
  ]);
183
- const write = async (fields, done)=>{
191
+ /** One update of the lead, and whether it landed. */ const write = async (fields, done)=>{
184
192
  try {
185
193
  const ref = refFor();
186
194
  if (!ref) {
@@ -188,7 +196,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
188
196
  variant: 'warning',
189
197
  persist: false
190
198
  });
191
- return;
199
+ return false;
192
200
  }
193
201
  await updateDoc(ref, _extends({}, fields, {
194
202
  updatedAt: serverTimestamp()
@@ -197,70 +205,76 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
197
205
  variant: 'success',
198
206
  persist: false
199
207
  });
208
+ return true;
200
209
  } catch (error) {
201
210
  enqueueSnackbar(error instanceof Error ? error.message : 'The lead could not be updated.', {
202
211
  variant: 'error'
203
212
  });
213
+ return false;
204
214
  }
205
215
  };
206
- const saveNotes = async ()=>{
207
- setSavingNotes(true);
208
- const verdict = await writeGuardedBySeed({
209
- subject: 'lead',
210
- fromCache,
211
- unreadable: leadStatus === 'error'
212
- }, ()=>write({
213
- notes: notes.trim().slice(0, NOTES_MAX)
214
- }, 'Notes saved'));
215
- setSavingNotes(false);
216
- if (!verdict.ok) {
217
- var _verdict_message;
218
- enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The notes could not be saved.', {
219
- variant: 'warning'
220
- });
221
- return;
222
- }
216
+ /*
217
+ * What Save is offered for: a profile field edited, a custom value that
218
+ * differs from the stored map, or the notes. Touched-and-put-back does
219
+ * not count for a custom value — the draft keeps the key either way, and
220
+ * a Save that wrote nothing would still bump `updatedAt`.
221
+ */ const customChanged = crmCustomDraftChanges(storedCustom, custom).length > 0;
222
+ const dirty = profileDirty || customChanged || notesDirty;
223
+ /** Every draft back to what the document holds. */ const discard = ()=>{
224
+ var _lead_notes;
225
+ setProfile(profileDraftFrom(lead));
226
+ setProfileDirty(false);
227
+ setProfileErrors({});
228
+ setCustom({});
229
+ setNotes(String((_lead_notes = lead.notes) != null ? _lead_notes : ''));
223
230
  setNotesDirty(false);
224
231
  };
225
232
  /*
226
- * What Save is offered for: a profile field edited, or a custom value
227
- * that differs from the stored map. Touched-and-put-back does not count
228
- * — the draft keeps the key either way, and a Save that wrote nothing
229
- * would still bump `updatedAt`.
230
- */ const dirty = profileDirty || crmCustomDraftChanges(storedCustom, custom).length > 0;
231
- const saveProfile = async ()=>{
232
- const { patch, errors } = Aglyn.normalizeCrmLeadProfile({
233
- company: profile.company,
234
- jobTitle: profile.jobTitle,
235
- phone: profile.phone,
236
- website: profile.website,
237
- leadSource: profile.leadSource,
238
- tags: profile.tags,
239
- address: profile.address
240
- });
241
- setProfileErrors(errors);
242
- if (Object.keys(errors).length) return;
243
- /*
244
- * A required field the reader CLEARED is refused; one the lead has
245
- * always lacked is not this save's to demand, or a field added after
246
- * the lead was captured would block every later edit to its company.
247
- */ const missing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, 'edit');
248
- if (missing.length) {
249
- enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {
250
- variant: 'warning'
233
+ * The Details card's one Save: the profile and the custom values while
234
+ * the lead is open, and the notes, in one update. A converted lead's
235
+ * profile is the contact's now, so only its notes are written.
236
+ */ const saveDetails = async ()=>{
237
+ const update = {};
238
+ if (!converted && (profileDirty || customChanged)) {
239
+ const { patch, errors } = Aglyn.normalizeCrmLeadProfile({
240
+ company: profile.company,
241
+ jobTitle: profile.jobTitle,
242
+ phone: profile.phone,
243
+ website: profile.website,
244
+ leadSource: profile.leadSource,
245
+ tags: profile.tags,
246
+ address: profile.address
251
247
  });
252
- return;
248
+ setProfileErrors(errors);
249
+ if (Object.keys(errors).length) return;
250
+ /*
251
+ * A required field the reader CLEARED is refused; one the lead has
252
+ * always lacked is not this save's to demand, or a field added after
253
+ * the lead was captured would block every later edit to its company.
254
+ */ const missing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, 'edit');
255
+ if (missing.length) {
256
+ enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {
257
+ variant: 'warning'
258
+ });
259
+ return;
260
+ }
261
+ const profileFields = profileWrite(patch);
262
+ Object.assign(update, profileFields, crmCustomDraftWrites(storedCustom, custom), // The company, title, tags and lead source are what the Leads list
263
+ // searches and filters by (AGL-3321).
264
+ crmClientListFields('leads', lead, profileFields, LEAD_CLIENT_LIST_FIELDS));
253
265
  }
254
- setSavingProfile(true);
266
+ if (notesDirty) update['notes'] = notes.trim().slice(0, NOTES_MAX);
267
+ if (!Object.keys(update).length) return;
268
+ setSaving(true);
269
+ let landed = false;
255
270
  const verdict = await writeGuardedBySeed({
256
271
  subject: 'lead',
257
272
  fromCache,
258
273
  unreadable: leadStatus === 'error'
259
- }, ()=>{
260
- const profileFields = profileWrite(patch);
261
- return write(_extends({}, profileFields, crmCustomDraftWrites(storedCustom, custom), crmClientListFields('leads', lead, profileFields, LEAD_CLIENT_LIST_FIELDS)), 'Lead saved');
274
+ }, async ()=>{
275
+ landed = await write(update, 'Lead saved');
262
276
  });
263
- setSavingProfile(false);
277
+ setSaving(false);
264
278
  if (!verdict.ok) {
265
279
  var _verdict_message;
266
280
  enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The lead could not be saved.', {
@@ -268,247 +282,271 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
268
282
  });
269
283
  return;
270
284
  }
285
+ if (!landed) return;
271
286
  setProfileDirty(false);
287
+ setNotesDirty(false);
272
288
  // The draft is spent: what it held is stored, and the controls read
273
289
  // the document again.
274
290
  setCustom({});
275
291
  };
276
292
  const consent = Aglyn.readMarketingBasis(lead, consentGroup);
277
293
  const consentLine = consent.basis === 'granted' ? `Opted in to marketing${consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''}` : consent.basis === 'declined' ? 'Declined marketing' : 'No marketing consent recorded — this lead cannot be emailed marketing';
278
- return /*#__PURE__*/ _jsx(CrmRecordHeader, {
279
- kind: "Lead",
280
- title: String(lead['name'] || lead['email'] || leadId),
281
- // The name is the heading; the address is the one line under it,
282
- // unless the address IS the name, in which case there is no second fact.
283
- subtitle: lead['name'] ? String((_lead_email = lead['email']) != null ? _lead_email : '') : undefined,
284
- help: Aglyn.pluginDocsHelp('crmLeads', {
285
- anchor: '#working-a-lead-from-the-row'
286
- }),
287
- backHref: routes.section('leads'),
288
- backLabel: "Back to leads",
289
- // The booking door (AGL-2660), while the lead is still the record
290
- // being worked: once converted, the contact is where a meeting is
291
- // booked from, and the links below lead there.
292
- booking: converted ? undefined : {
293
- hostId,
294
- org,
295
- kind: 'lead',
296
- recordId: leadId
297
- },
298
- actions: /*#__PURE__*/ _jsxs(_Fragment, {
299
- children: [
300
- converted ? null : erasurePending ? /*#__PURE__*/ _jsx(Tooltip, {
301
- title: CONVERT_PENDING_ERASURE_REASON,
302
- children: /*#__PURE__*/ _jsx("span", {
303
- children: /*#__PURE__*/ _jsx(Button, {
294
+ return /*#__PURE__*/ _jsxs(_Fragment, {
295
+ children: [
296
+ /*#__PURE__*/ _jsx(CrmRecordHeader, {
297
+ kind: "Lead",
298
+ title: String(lead['name'] || lead['email'] || leadId),
299
+ // The name is the heading; the address is the one line under it,
300
+ // unless the address IS the name, in which case there is no second fact.
301
+ subtitle: lead['name'] ? String((_lead_email = lead['email']) != null ? _lead_email : '') : undefined,
302
+ help: Aglyn.pluginDocsHelp('crmLeads', {
303
+ anchor: '#working-a-lead-from-the-row'
304
+ }),
305
+ backHref: routes.section('leads'),
306
+ backLabel: "Back to leads",
307
+ // The booking door (AGL-2660), while the lead is still the record
308
+ // being worked: once converted, the contact is where a meeting is
309
+ // booked from, and the links below lead there.
310
+ booking: converted ? undefined : {
311
+ hostId,
312
+ org,
313
+ kind: 'lead',
314
+ recordId: leadId
315
+ },
316
+ actions: /*#__PURE__*/ _jsxs(_Fragment, {
317
+ children: [
318
+ converted ? /*#__PURE__*/ _jsxs(_Fragment, {
319
+ children: [
320
+ /*#__PURE__*/ _jsx(Button, _extends({
321
+ component: AppLink
322
+ }, {
323
+ componentVariant: 'naked',
324
+ nativeButton: false
325
+ }, {
326
+ href: routes.contact(String(lead.convertedContactId)),
327
+ size: "small",
328
+ variant: "outlined",
329
+ children: 'Open contact'
330
+ })),
331
+ lead.companyId ? /*#__PURE__*/ _jsx(Button, _extends({
332
+ component: AppLink
333
+ }, {
334
+ componentVariant: 'naked',
335
+ nativeButton: false
336
+ }, {
337
+ href: routes.company(lead.companyId),
338
+ size: "small",
339
+ variant: "outlined",
340
+ children: 'Open company'
341
+ })) : null,
342
+ lead.dealId ? /*#__PURE__*/ _jsx(Button, _extends({
343
+ component: AppLink
344
+ }, {
345
+ componentVariant: 'naked',
346
+ nativeButton: false
347
+ }, {
348
+ href: routes.deal(lead.dealId),
349
+ size: "small",
350
+ variant: "outlined",
351
+ children: 'Open deal'
352
+ })) : null
353
+ ]
354
+ }) : erasurePending ? /*#__PURE__*/ _jsx(Tooltip, {
355
+ title: CONVERT_PENDING_ERASURE_REASON,
356
+ children: /*#__PURE__*/ _jsx("span", {
357
+ children: /*#__PURE__*/ _jsx(Button, {
358
+ size: "small",
359
+ variant: "contained",
360
+ disabled: true,
361
+ children: 'Convert'
362
+ })
363
+ })
364
+ }) : /*#__PURE__*/ _jsx(Button, {
304
365
  size: "small",
305
366
  variant: "contained",
306
- disabled: true,
367
+ onClick: onConvert,
307
368
  children: 'Convert'
369
+ }),
370
+ /*#__PURE__*/ _jsx(CrmCallButton, {
371
+ hostId: hostId,
372
+ org: org,
373
+ link: {
374
+ leadId
375
+ },
376
+ phone: leadPhone
377
+ }),
378
+ /*#__PURE__*/ _jsx(CrmSendEmailButton, {
379
+ hostId: hostId,
380
+ leadId: leadId,
381
+ email: String((_lead_email1 = lead['email']) != null ? _lead_email1 : ''),
382
+ name: String((_lead_name = lead['name']) != null ? _lead_name : ''),
383
+ emailState: emailState
308
384
  })
309
- })
310
- }) : /*#__PURE__*/ _jsx(Button, {
311
- size: "small",
312
- variant: "contained",
313
- onClick: onConvert,
314
- children: 'Convert'
315
- }),
316
- /*#__PURE__*/ _jsx(CrmCallButton, {
317
- hostId: hostId,
318
- org: org,
319
- link: {
320
- leadId
321
- },
322
- phone: leadPhone
323
- }),
324
- /*#__PURE__*/ _jsx(CrmSendEmailButton, {
325
- hostId: hostId,
326
- leadId: leadId,
327
- email: String((_lead_email1 = lead['email']) != null ? _lead_email1 : ''),
328
- name: String((_lead_name = lead['name']) != null ? _lead_name : ''),
329
- emailState: emailState
330
- })
331
- ]
332
- }),
333
- menuItems: [
334
- ...open ? [
335
- {
336
- key: 'unqualify',
337
- label: 'Unqualify',
338
- icon: /*#__PURE__*/ _jsx(MdiIcon, {
339
- path: mdiAccountCancelOutline.path,
340
- size: 0.8
341
- }),
342
- destructive: true,
343
- onClick: onUnqualify
344
- }
345
- ] : [],
346
- ...extraMenuItems
347
- ],
348
- chips: /*#__PURE__*/ _jsxs(_Fragment, {
349
- children: [
350
- /*#__PURE__*/ _jsx(LeadStatusChip, {
351
- lead: lead
352
- }),
353
- /*#__PURE__*/ _jsx(CrmEmailStateChip, {
354
- state: emailState
355
- }),
356
- /*#__PURE__*/ _jsx(CrmRecordChip, {
357
- label: "Owner",
358
- value: lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined
385
+ ]
359
386
  }),
360
- extraChips
361
- ]
362
- }),
363
- children: /*#__PURE__*/ _jsxs(Stack, {
364
- spacing: 3,
365
- children: [
366
- banner,
367
- leadPhone ? /*#__PURE__*/ _jsx(Fact, {
368
- label: "Phone",
369
- children: /*#__PURE__*/ _jsx(CrmPhoneLink, {
370
- phone: leadPhone
371
- })
372
- }) : null,
373
- /*#__PURE__*/ _jsx(Fact, {
374
- label: "Marketing consent",
375
- children: consentLine
387
+ menuItems: [
388
+ ...open ? [
389
+ {
390
+ key: 'unqualify',
391
+ label: 'Unqualify',
392
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
393
+ path: mdiAccountCancelOutline.path,
394
+ size: 0.8
395
+ }),
396
+ destructive: true,
397
+ onClick: onUnqualify
398
+ }
399
+ ] : [],
400
+ ...extraMenuItems
401
+ ],
402
+ chips: /*#__PURE__*/ _jsxs(_Fragment, {
403
+ children: [
404
+ /*#__PURE__*/ _jsx(LeadStatusChip, {
405
+ lead: lead
406
+ }),
407
+ /*#__PURE__*/ _jsx(CrmEmailStateChip, {
408
+ state: emailState
409
+ }),
410
+ /*#__PURE__*/ _jsx(CrmRecordChip, {
411
+ label: "Owner",
412
+ value: lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined
413
+ }),
414
+ extraChips
415
+ ]
376
416
  }),
377
- /*#__PURE__*/ _jsxs(Stack, {
378
- direction: {
379
- xs: 'column',
380
- md: 'row'
381
- },
382
- spacing: 2,
417
+ children: /*#__PURE__*/ _jsxs(Stack, {
418
+ spacing: 3,
383
419
  children: [
384
- converted ? /*#__PURE__*/ _jsx(Fact, {
385
- label: "Status",
386
- children: /*#__PURE__*/ _jsxs(Stack, {
387
- direction: "row",
388
- spacing: 1,
389
- sx: {
390
- alignItems: 'center'
391
- },
392
- children: [
393
- /*#__PURE__*/ _jsx(LeadStatusChip, {
394
- lead: lead
395
- }),
396
- /*#__PURE__*/ _jsx(Typography, {
397
- variant: "body2",
398
- color: "text.secondary",
399
- children: lead.convertedAtMs ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}` : 'Converted'
400
- })
401
- ]
420
+ banner,
421
+ leadPhone ? /*#__PURE__*/ _jsx(Fact, {
422
+ label: "Phone",
423
+ children: /*#__PURE__*/ _jsx(CrmPhoneLink, {
424
+ phone: leadPhone
402
425
  })
403
- }) : /*#__PURE__*/ _jsxs(FormControl, {
404
- size: "small",
405
- sx: {
406
- minWidth: 200
426
+ }) : null,
427
+ /*#__PURE__*/ _jsx(Fact, {
428
+ label: "Marketing consent",
429
+ children: consentLine
430
+ }),
431
+ /*#__PURE__*/ _jsxs(Stack, {
432
+ direction: {
433
+ xs: 'column',
434
+ md: 'row'
407
435
  },
436
+ spacing: 2,
408
437
  children: [
409
- /*#__PURE__*/ _jsx(InputLabel, {
410
- id: statusLabelId,
411
- children: 'Status'
412
- }),
413
- /*#__PURE__*/ _jsxs(Select, {
414
- labelId: statusLabelId,
438
+ converted ? /*#__PURE__*/ _jsx(Fact, {
415
439
  label: "Status",
416
- value: status === 'unqualified' ? 'unqualified' : status,
417
- onChange: (event)=>{
418
- const next = String(event.target.value);
419
- if (next === 'unqualified') {
420
- onUnqualify();
421
- return;
422
- }
423
- // Reopening drops the reason with the closed state: a lead
424
- // being worked again is not "unqualified because …".
425
- void write(_extends({
426
- status: next
427
- }, status === 'unqualified' ? {
428
- unqualifiedReason: deleteField()
429
- } : {}), 'Status updated');
440
+ children: /*#__PURE__*/ _jsxs(Stack, {
441
+ direction: "row",
442
+ spacing: 1,
443
+ sx: {
444
+ alignItems: 'center'
445
+ },
446
+ children: [
447
+ /*#__PURE__*/ _jsx(LeadStatusChip, {
448
+ lead: lead
449
+ }),
450
+ /*#__PURE__*/ _jsx(Typography, {
451
+ variant: "body2",
452
+ color: "text.secondary",
453
+ children: lead.convertedAtMs ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}` : 'Converted'
454
+ })
455
+ ]
456
+ })
457
+ }) : /*#__PURE__*/ _jsxs(FormControl, {
458
+ size: "small",
459
+ sx: {
460
+ minWidth: 200
430
461
  },
431
462
  children: [
432
- /*#__PURE__*/ _jsx(MenuItem, {
433
- value: "new",
434
- children: Aglyn.CRM_LEAD_STATUS_LABELS.new
435
- }),
436
- /*#__PURE__*/ _jsx(MenuItem, {
437
- value: "working",
438
- children: Aglyn.CRM_LEAD_STATUS_LABELS.working
463
+ /*#__PURE__*/ _jsx(InputLabel, {
464
+ id: statusLabelId,
465
+ children: 'Status'
439
466
  }),
440
- /*#__PURE__*/ _jsx(MenuItem, {
441
- value: "unqualified",
442
- children: `${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`
467
+ /*#__PURE__*/ _jsxs(Select, {
468
+ labelId: statusLabelId,
469
+ label: "Status",
470
+ value: status === 'unqualified' ? 'unqualified' : status,
471
+ onChange: (event)=>{
472
+ const next = String(event.target.value);
473
+ if (next === 'unqualified') {
474
+ onUnqualify();
475
+ return;
476
+ }
477
+ // Reopening drops the reason with the closed state: a lead
478
+ // being worked again is not "unqualified because …".
479
+ void write(_extends({
480
+ status: next
481
+ }, status === 'unqualified' ? {
482
+ unqualifiedReason: deleteField()
483
+ } : {}), 'Status updated');
484
+ },
485
+ children: [
486
+ /*#__PURE__*/ _jsx(MenuItem, {
487
+ value: "new",
488
+ children: Aglyn.CRM_LEAD_STATUS_LABELS.new
489
+ }),
490
+ /*#__PURE__*/ _jsx(MenuItem, {
491
+ value: "working",
492
+ children: Aglyn.CRM_LEAD_STATUS_LABELS.working
493
+ }),
494
+ /*#__PURE__*/ _jsx(MenuItem, {
495
+ value: "unqualified",
496
+ children: `${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`
497
+ })
498
+ ]
443
499
  })
444
500
  ]
501
+ }),
502
+ /*#__PURE__*/ _jsx(LeadOwnerSelect, {
503
+ value: lead.ownerUid,
504
+ roster: roster,
505
+ fullWidth: false,
506
+ onChange: (uid)=>void write({
507
+ ownerUid: uid || deleteField()
508
+ }, uid ? 'Owner assigned' : 'Owner cleared')
445
509
  })
446
510
  ]
447
511
  }),
448
- /*#__PURE__*/ _jsx(LeadOwnerSelect, {
449
- value: lead.ownerUid,
450
- roster: roster,
451
- fullWidth: false,
452
- onChange: (uid)=>void write({
453
- ownerUid: uid || deleteField()
454
- }, uid ? 'Owner assigned' : 'Owner cleared')
455
- })
512
+ status === 'unqualified' && lead.unqualifiedReason ? /*#__PURE__*/ _jsx(Alert, {
513
+ severity: "info",
514
+ children: `Unqualified: ${lead.unqualifiedReason}`
515
+ }) : null
456
516
  ]
517
+ })
518
+ }),
519
+ /*#__PURE__*/ _jsx(CardDisplay, {
520
+ header: 'Details',
521
+ help: Aglyn.pluginDocsHelp('crmLeads', {
522
+ anchor: '#a-leads-page'
457
523
  }),
458
- status === 'unqualified' && lead.unqualifiedReason ? /*#__PURE__*/ _jsx(Alert, {
459
- severity: "info",
460
- children: `Unqualified: ${lead.unqualifiedReason}`
461
- }) : null,
462
- converted ? /*#__PURE__*/ _jsxs(Stack, {
463
- direction: "row",
464
- spacing: 1,
465
- sx: {
466
- flexWrap: 'wrap',
467
- rowGap: 1
468
- },
469
- children: [
470
- /*#__PURE__*/ _jsx(Button, _extends({
471
- component: AppLink
472
- }, {
473
- componentVariant: 'naked',
474
- nativeButton: false
475
- }, {
476
- href: routes.contact(String(lead.convertedContactId)),
477
- size: "small",
478
- variant: "outlined",
479
- children: 'Open contact'
480
- })),
481
- lead.companyId ? /*#__PURE__*/ _jsx(Button, _extends({
482
- component: AppLink
483
- }, {
484
- componentVariant: 'naked',
485
- nativeButton: false
486
- }, {
487
- href: routes.company(lead.companyId),
488
- size: "small",
489
- variant: "outlined",
490
- children: 'Open company'
491
- })) : null,
492
- lead.dealId ? /*#__PURE__*/ _jsx(Button, _extends({
493
- component: AppLink
494
- }, {
495
- componentVariant: 'naked',
496
- nativeButton: false
497
- }, {
498
- href: routes.deal(lead.dealId),
499
- size: "small",
500
- variant: "outlined",
501
- children: 'Open deal'
502
- })) : null
503
- ]
504
- }) : null,
505
- /*#__PURE__*/ _jsxs(Stack, {
524
+ contentGutterX: true,
525
+ contentGutterY: true,
526
+ HeaderProps: {
527
+ action: /*#__PURE__*/ _jsxs(Stack, {
528
+ direction: "row",
529
+ spacing: 1,
530
+ children: [
531
+ dirty ? /*#__PURE__*/ _jsx(Button, {
532
+ size: "small",
533
+ disabled: saving,
534
+ onClick: discard,
535
+ children: 'Discard changes'
536
+ }) : null,
537
+ /*#__PURE__*/ _jsx(Button, {
538
+ size: "small",
539
+ variant: "contained",
540
+ onClick: ()=>void saveDetails(),
541
+ disabled: !dirty || saving,
542
+ children: saving ? 'Saving…' : 'Save'
543
+ })
544
+ ]
545
+ })
546
+ },
547
+ children: /*#__PURE__*/ _jsxs(Stack, {
506
548
  spacing: 2,
507
549
  children: [
508
- /*#__PURE__*/ _jsx(Typography, {
509
- variant: "subtitle2",
510
- children: 'Profile'
511
- }),
512
550
  /*#__PURE__*/ _jsxs(Stack, {
513
551
  direction: {
514
552
  xs: 'column',
@@ -611,9 +649,8 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
611
649
  fields.active.length ? /*#__PURE__*/ _jsxs(_Fragment, {
612
650
  children: [
613
651
  /*#__PURE__*/ _jsx(Typography, {
614
- variant: "caption",
615
- color: "text.secondary",
616
- children: 'Custom fields'
652
+ variant: "subtitle2",
653
+ children: 'More fields'
617
654
  }),
618
655
  fields.active.map((definition)=>/*#__PURE__*/ _jsx(CrmCustomFieldControl, {
619
656
  definition: definition,
@@ -621,29 +658,10 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
621
658
  onChange: (value)=>setCustom((current)=>_extends({}, current, {
622
659
  [definition.key]: value
623
660
  })),
624
- disabled: converted || savingProfile
661
+ disabled: converted || saving
625
662
  }, definition.$id))
626
663
  ]
627
664
  }) : null,
628
- converted ? null : /*#__PURE__*/ _jsx(Stack, {
629
- direction: "row",
630
- spacing: 1,
631
- sx: {
632
- justifyContent: 'flex-end'
633
- },
634
- children: /*#__PURE__*/ _jsx(Button, {
635
- size: "small",
636
- variant: "contained",
637
- onClick: ()=>void saveProfile(),
638
- disabled: !dirty || savingProfile,
639
- children: 'Save'
640
- })
641
- })
642
- ]
643
- }),
644
- /*#__PURE__*/ _jsxs(Stack, {
645
- spacing: 1,
646
- children: [
647
665
  /*#__PURE__*/ _jsx(TextField, {
648
666
  size: "small",
649
667
  label: "Notes",
@@ -660,25 +678,11 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
660
678
  maxLength: NOTES_MAX
661
679
  }
662
680
  }
663
- }),
664
- /*#__PURE__*/ _jsx(Stack, {
665
- direction: "row",
666
- spacing: 1,
667
- sx: {
668
- justifyContent: 'flex-end'
669
- },
670
- children: /*#__PURE__*/ _jsx(Button, {
671
- size: "small",
672
- variant: "contained",
673
- onClick: ()=>void saveNotes(),
674
- disabled: !notesDirty || savingNotes,
675
- children: 'Save notes'
676
- })
677
681
  })
678
682
  ]
679
683
  })
680
- ]
681
- })
684
+ })
685
+ ]
682
686
  });
683
687
  }
684
688
  LeadPropertiesCard.displayName = 'LeadPropertiesCard';