@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201

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