@aglyn/plugins-crm 1.0.0-beta.296 → 1.0.0
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 +14 -14
- package/src/lib/components/contact-properties-card.js +8 -0
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +26 -14
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +7 -4
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +7 -5
- package/src/lib/server/task-reminders-route.js +82 -15
- package/src/lib/server/task-reminders-route.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0
|
|
28
|
+
"@aglyn/aglyn": "1.0.0",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -334,6 +334,14 @@ export function ContactPropertiesCard(props) {
|
|
|
334
334
|
});
|
|
335
335
|
// The custom values are stored; the controls read the record again.
|
|
336
336
|
setCustom({});
|
|
337
|
+
if (stageChanged && !siteHostId) {
|
|
338
|
+
// The stage lives on a site's facet, and this person has none. Never
|
|
339
|
+
// "Contact saved" over a change that went nowhere (AGL-4120).
|
|
340
|
+
return void enqueueSnackbar('Saved, but the stage could not be changed: no site has captured this contact yet.', {
|
|
341
|
+
variant: 'warning',
|
|
342
|
+
persist: false
|
|
343
|
+
});
|
|
344
|
+
}
|
|
337
345
|
if (stageChanged && siteHostId) {
|
|
338
346
|
try {
|
|
339
347
|
await setContactStage(user, siteHostId, record.$id, lifecycleStage || null);
|
|
@@ -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 composeContactName,\n CONTACT_BIRTHDATE_REFUSAL,\n CONTACT_EXTRA_PHONE_FIELDS,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_NAME_PART_MAX,\n CONTACT_PHONE_FIELD_LABELS,\n type ConsentGroup,\n type ContactLifecycleStage,\n CRM_SALUTATION_PICKLIST,\n crmTelHref,\n normalizeContactBirthdate,\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 Checkbox,\n FormControlLabel,\n FormHelperText,\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 { useCrmPicklist } from '../hooks/use-crm-picklist'\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 { ContactReportsToField } from './contact-reports-to-field'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmPicklistSelect } from './picklist-select'\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 * ## Salesforce's sections (AGL-3515)\n *\n * The fields are grouped the way Salesforce's contact Details groups them —\n * Contact information, Phones, Addresses, Additional information — and every\n * one is still one draft and one Save. While the holder keeps a first or\n * last name the Name field shows their composition and is read-only; the\n * save sends the parts, and the route composes the name from them. The\n * salutation and the reports-to are sent only when they change, for the\n * lead source's reason: the route judges a sent one. Do not call is a hint\n * on every phone's call link, never a block.\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 */\n/** The phone fields the card edits, in the order the Phones section lists them. */\nconst PHONE_FIELDS = ['phone', ...CONTACT_EXTRA_PHONE_FIELDS] as const\ntype PhoneField = (typeof PHONE_FIELDS)[number]\ntype PhoneDrafts = Record<PhoneField, string>\n\nconst phoneDraftsFrom = (record: ContactRecord): PhoneDrafts =>\n Object.fromEntries(\n PHONE_FIELDS.map((field) => [field, String(record[field] ?? '')]),\n ) as PhoneDrafts\n\n/** The phones as the route stores them, or the fields that cannot be read as numbers. */\nfunction readPhones(\n drafts: PhoneDrafts,\n): { ok: true; phones: PhoneDrafts } | { ok: false; errors: Partial<PhoneDrafts> } {\n const phones = {} as PhoneDrafts\n const errors: Partial<PhoneDrafts> = {}\n for (const field of PHONE_FIELDS) {\n const typed = drafts[field].trim()\n const normalized = typed ? normalizePhone(typed) : ''\n if (normalized === null) errors[field] = PHONE_HELP_ERROR\n else phones[field] = normalized\n }\n return Object.keys(errors).length ? { ok: false, errors } : { ok: true, phones }\n}\n\nconst PHONE_HELP = 'With the country code, like +1 512 555 0107'\nconst PHONE_HELP_ERROR = 'Enter it with its country code, like +1 512 555 0107.'\n\n/** A section heading inside the card — Salesforce's Details groups its fields the same way. */\nfunction SectionHeading(props: { children: string }) {\n return (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\" color=\"text.secondary\">\n {props.children}\n </Typography>\n </Grid>\n )\n}\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 [salutation, setSalutation] = useState(record.salutation ?? '')\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, orgId)\n const [firstName, setFirstName] = useState(record.firstName ?? '')\n const [lastName, setLastName] = useState(record.lastName ?? '')\n const [phones, setPhones] = useState<PhoneDrafts>(() => phoneDraftsFrom(record))\n const [doNotCall, setDoNotCall] = useState(record.doNotCall === true)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [department, setDepartment] = useState(record.department ?? '')\n const [birthdate, setBirthdate] = useState(record.birthdate ?? '')\n const [assistantName, setAssistantName] = useState(record.assistantName ?? '')\n const [reportsTo, setReportsTo] = useState(record.reportsToContactId ?? '')\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 [otherAddress, setOtherAddress] = useState<AddressDraft>(\n addressDraftFrom(record.otherAddress),\n )\n const [phoneErrors, setPhoneErrors] = useState<Partial<PhoneDrafts>>({})\n const [birthdateError, setBirthdateError] = 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 * The name the first and last names make (AGL-3515). While there is one,\n * it IS the name — the Name field shows it, read-only, and the save sends\n * the parts rather than a name the route would have to reconcile.\n */\n const composedName = composeContactName(firstName, lastName)\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 setSalutation(record.salutation ?? '')\n setFirstName(record.firstName ?? '')\n setLastName(record.lastName ?? '')\n setPhones(phoneDraftsFrom(record))\n setDoNotCall(record.doNotCall === true)\n setJobTitle(record.jobTitle)\n setDepartment(record.department ?? '')\n setBirthdate(record.birthdate ?? '')\n setAssistantName(record.assistantName ?? '')\n setReportsTo(record.reportsToContactId ?? '')\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 setOtherAddress(addressDraftFrom(record.otherAddress))\n setPhoneErrors({})\n setBirthdateError('')\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 readPhonesResult = readPhones(phones)\n const storedBirthdate = normalizeContactBirthdate(birthdate)\n setPhoneErrors('errors' in readPhonesResult ? readPhonesResult.errors : {})\n setBirthdateError(storedBirthdate === null ? CONTACT_BIRTHDATE_REFUSAL : '')\n if ('errors' in readPhonesResult || storedBirthdate === null) return\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 // The name is the parts' while there are parts — see the note above.\n ...(composedName ? {} : { name: nameOverride.trim().slice(0, 120) }),\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n ...readPhonesResult.phones,\n // Sent only when it moves: the route stores the flag as a fact about\n // the person, and an untouched card should not re-assert it.\n ...(doNotCall !== (record.doNotCall === true) ? { doNotCall } : {}),\n jobTitle: jobTitle.trim().slice(0, 120),\n department: department.trim().slice(0, 120),\n birthdate: storedBirthdate,\n assistantName: assistantName.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. The reports-to\n // likewise, because the route checks a sent one for a loop.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n ...(salutation !== (record.salutation ?? '') ? { salutation } : {}),\n ...(reportsTo !== (record.reportsToContactId ?? '')\n ? { reportsToContactId: reportsTo || null }\n : {}),\n address,\n otherAddress,\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 assistantName,\n birthdate,\n clearingRequired,\n companyId,\n companyName,\n composedName,\n contactUpdate,\n customChanges,\n department,\n doNotCall,\n enqueueSnackbar,\n firstName,\n siteHostId,\n jobTitle,\n lastName,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n otherAddress,\n ownerUid,\n phones,\n record.$id,\n record.doNotCall,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n record.reportsToContactId,\n record.salutation,\n reportsTo,\n salutation,\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 /** One phone field, with click-to-call off what is IN the box (AGL-2661). */\n const phoneField = (field: PhoneField) => {\n const value = phones[field]\n const telHref = crmTelHref(value)\n const callLabel = doNotCall\n ? `Call ${value.trim()} — marked do not call`\n : `Call ${value.trim()}`\n return (\n <Grid key={field} size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label={CONTACT_PHONE_FIELD_LABELS[field]}\n value={value}\n onChange={(event) => change(setPhones, { ...phones, [field]: event.target.value })}\n error={Boolean(phoneErrors[field])}\n helperText={phoneErrors[field] || (field === 'phone' ? PHONE_HELP : undefined)}\n fullWidth\n slotProps={{\n input: {\n /*\n * The number on screen is the one a reader means to ring, and\n * `crmTelHref` withholds the link from anything half-typed. A\n * do-not-call person keeps the link — the rep decides — with\n * the request said in the tooltip and the icon's color.\n */\n endAdornment:\n telHref && field !== 'fax' ? (\n <InputAdornment position=\"end\">\n <Tooltip title={callLabel}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n color={doNotCall ? 'warning' : 'default'}\n aria-label={callLabel}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n )\n }\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#contact-properties' })}\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 <SectionHeading>{'Contact information'}</SectionHeading>\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 <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n stored={record.salutation ?? ''}\n onChange={(value) => change(setSalutation, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => change(setFirstName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => change(setLastName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || nameOverride}\n onChange={(event) => change(setNameOverride, event.target.value)}\n slotProps={{\n htmlInput: { maxLength: 120 },\n input: { readOnly: Boolean(composedName) },\n }}\n helperText={\n composedName\n ? 'Made of the first and last name.'\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=\"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 <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => change(setDepartment, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\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 <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 <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\n <SectionHeading>{'Phones'}</SectionHeading>\n {PHONE_FIELDS.filter((field) => field !== 'assistantPhone').map(phoneField)}\n <Grid size={{ xs: 12, sm: 6 }}>\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => change(setDoNotCall, event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <FormHelperText>\n {'They asked not to be phoned. Every number still dials; the Call button says so.'}\n </FormHelperText>\n </Grid>\n\n <SectionHeading>{'Addresses'}</SectionHeading>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields\n value={address}\n onChange={(value) => change(setAddress, value)}\n />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Other address'}</Typography>\n <ContactAddressFields\n value={otherAddress}\n onChange={(value) => change(setOtherAddress, value)}\n />\n </Stack>\n </Grid>\n\n <SectionHeading>{'Additional information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n type=\"date\"\n label=\"Birthdate\"\n value={birthdate}\n onChange={(event) => change(setBirthdate, event.target.value)}\n error={Boolean(birthdateError)}\n helperText={birthdateError || undefined}\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <ContactReportsToField\n hostId={hostId}\n org={org}\n contactId={record.$id}\n groupId={consentGroup.groupId}\n value={reportsTo}\n onChange={(value) => change(setReportsTo, value)}\n disabled={saving}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Assistant\"\n value={assistantName}\n onChange={(event) => change(setAssistantName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n {phoneField('assistantPhone')}\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","composeContactName","CONTACT_BIRTHDATE_REFUSAL","CONTACT_EXTRA_PHONE_FIELDS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_NAME_PART_MAX","CONTACT_PHONE_FIELD_LABELS","CRM_SALUTATION_PICKLIST","crmTelHref","normalizeContactBirthdate","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Checkbox","FormControlLabel","FormHelperText","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useContactUpdate","useCrmActivityLogger","useCrmPicklist","useLeadSourcePicklist","parseContactTags","setContactStage","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","ContactReportsToField","CrmCustomFieldControl","CrmPicklistSelect","crmSuiteLockedReason","LeadSourceSelect","PHONE_FIELDS","phoneDraftsFrom","record","Object","fromEntries","map","field","String","readPhones","drafts","phones","errors","typed","trim","normalized","PHONE_HELP_ERROR","keys","length","ok","PHONE_HELP","SectionHeading","props","size","xs","variant","color","children","ContactPropertiesCard","hostId","org","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","salutation","setSalutation","salutations","firstName","setFirstName","lastName","setLastName","setPhones","doNotCall","setDoNotCall","jobTitle","setJobTitle","department","setDepartment","birthdate","setBirthdate","assistantName","setAssistantName","reportsTo","setReportsTo","reportsToContactId","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","otherAddress","setOtherAddress","phoneErrors","setPhoneErrors","birthdateError","setBirthdateError","saving","setSaving","custom","setCustom","edited","setEdited","fields","customFields","active","storedCustom","customChanges","clearingRequired","composedName","change","set","value","reseed","recordId","$id","handleSave","readPhonesResult","storedBirthdate","stageChanged","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","message","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","phoneField","telHref","callLabel","sm","label","onChange","event","target","Boolean","helperText","undefined","fullWidth","slotProps","input","endAdornment","position","title","component","href","edge","aria-label","path","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","disabled","onClick","container","readOnly","picklistId","picklist","stored","htmlInput","maxLength","canonicalName","ready","truncated","company","onCreate","fallbackName","select","stage","memberName","owner","crmMemberPickerLabel","placeholder","filter","control","checked","inputLabel","shrink","contactId","groupId","multiline","minRows","definition","key","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 composeContactName,\n CONTACT_BIRTHDATE_REFUSAL,\n CONTACT_EXTRA_PHONE_FIELDS,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_NAME_PART_MAX,\n CONTACT_PHONE_FIELD_LABELS,\n type ConsentGroup,\n type ContactLifecycleStage,\n CRM_SALUTATION_PICKLIST,\n crmTelHref,\n normalizeContactBirthdate,\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 Checkbox,\n FormControlLabel,\n FormHelperText,\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 { useCrmPicklist } from '../hooks/use-crm-picklist'\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 { ContactReportsToField } from './contact-reports-to-field'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmPicklistSelect } from './picklist-select'\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 * ## Salesforce's sections (AGL-3515)\n *\n * The fields are grouped the way Salesforce's contact Details groups them —\n * Contact information, Phones, Addresses, Additional information — and every\n * one is still one draft and one Save. While the holder keeps a first or\n * last name the Name field shows their composition and is read-only; the\n * save sends the parts, and the route composes the name from them. The\n * salutation and the reports-to are sent only when they change, for the\n * lead source's reason: the route judges a sent one. Do not call is a hint\n * on every phone's call link, never a block.\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 */\n/** The phone fields the card edits, in the order the Phones section lists them. */\nconst PHONE_FIELDS = ['phone', ...CONTACT_EXTRA_PHONE_FIELDS] as const\ntype PhoneField = (typeof PHONE_FIELDS)[number]\ntype PhoneDrafts = Record<PhoneField, string>\n\nconst phoneDraftsFrom = (record: ContactRecord): PhoneDrafts =>\n Object.fromEntries(\n PHONE_FIELDS.map((field) => [field, String(record[field] ?? '')]),\n ) as PhoneDrafts\n\n/** The phones as the route stores them, or the fields that cannot be read as numbers. */\nfunction readPhones(\n drafts: PhoneDrafts,\n): { ok: true; phones: PhoneDrafts } | { ok: false; errors: Partial<PhoneDrafts> } {\n const phones = {} as PhoneDrafts\n const errors: Partial<PhoneDrafts> = {}\n for (const field of PHONE_FIELDS) {\n const typed = drafts[field].trim()\n const normalized = typed ? normalizePhone(typed) : ''\n if (normalized === null) errors[field] = PHONE_HELP_ERROR\n else phones[field] = normalized\n }\n return Object.keys(errors).length ? { ok: false, errors } : { ok: true, phones }\n}\n\nconst PHONE_HELP = 'With the country code, like +1 512 555 0107'\nconst PHONE_HELP_ERROR = 'Enter it with its country code, like +1 512 555 0107.'\n\n/** A section heading inside the card — Salesforce's Details groups its fields the same way. */\nfunction SectionHeading(props: { children: string }) {\n return (\n <Grid size={{ xs: 12 }}>\n <Typography variant=\"subtitle2\" color=\"text.secondary\">\n {props.children}\n </Typography>\n </Grid>\n )\n}\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 [salutation, setSalutation] = useState(record.salutation ?? '')\n const salutations = useCrmPicklist(CRM_SALUTATION_PICKLIST, orgId)\n const [firstName, setFirstName] = useState(record.firstName ?? '')\n const [lastName, setLastName] = useState(record.lastName ?? '')\n const [phones, setPhones] = useState<PhoneDrafts>(() => phoneDraftsFrom(record))\n const [doNotCall, setDoNotCall] = useState(record.doNotCall === true)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [department, setDepartment] = useState(record.department ?? '')\n const [birthdate, setBirthdate] = useState(record.birthdate ?? '')\n const [assistantName, setAssistantName] = useState(record.assistantName ?? '')\n const [reportsTo, setReportsTo] = useState(record.reportsToContactId ?? '')\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 [otherAddress, setOtherAddress] = useState<AddressDraft>(\n addressDraftFrom(record.otherAddress),\n )\n const [phoneErrors, setPhoneErrors] = useState<Partial<PhoneDrafts>>({})\n const [birthdateError, setBirthdateError] = 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 * The name the first and last names make (AGL-3515). While there is one,\n * it IS the name — the Name field shows it, read-only, and the save sends\n * the parts rather than a name the route would have to reconcile.\n */\n const composedName = composeContactName(firstName, lastName)\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 setSalutation(record.salutation ?? '')\n setFirstName(record.firstName ?? '')\n setLastName(record.lastName ?? '')\n setPhones(phoneDraftsFrom(record))\n setDoNotCall(record.doNotCall === true)\n setJobTitle(record.jobTitle)\n setDepartment(record.department ?? '')\n setBirthdate(record.birthdate ?? '')\n setAssistantName(record.assistantName ?? '')\n setReportsTo(record.reportsToContactId ?? '')\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 setOtherAddress(addressDraftFrom(record.otherAddress))\n setPhoneErrors({})\n setBirthdateError('')\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 readPhonesResult = readPhones(phones)\n const storedBirthdate = normalizeContactBirthdate(birthdate)\n setPhoneErrors('errors' in readPhonesResult ? readPhonesResult.errors : {})\n setBirthdateError(storedBirthdate === null ? CONTACT_BIRTHDATE_REFUSAL : '')\n if ('errors' in readPhonesResult || storedBirthdate === null) return\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 // The name is the parts' while there are parts — see the note above.\n ...(composedName ? {} : { name: nameOverride.trim().slice(0, 120) }),\n firstName: firstName.trim(),\n lastName: lastName.trim(),\n ...readPhonesResult.phones,\n // Sent only when it moves: the route stores the flag as a fact about\n // the person, and an untouched card should not re-assert it.\n ...(doNotCall !== (record.doNotCall === true) ? { doNotCall } : {}),\n jobTitle: jobTitle.trim().slice(0, 120),\n department: department.trim().slice(0, 120),\n birthdate: storedBirthdate,\n assistantName: assistantName.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. The reports-to\n // likewise, because the route checks a sent one for a loop.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n ...(salutation !== (record.salutation ?? '') ? { salutation } : {}),\n ...(reportsTo !== (record.reportsToContactId ?? '')\n ? { reportsToContactId: reportsTo || null }\n : {}),\n address,\n otherAddress,\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 // The stage lives on a site's facet, and this person has none. Never\n // \"Contact saved\" over a change that went nowhere (AGL-4120).\n return void enqueueSnackbar(\n 'Saved, but the stage could not be changed: no site has captured this contact yet.',\n { variant: 'warning', persist: false },\n )\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 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 assistantName,\n birthdate,\n clearingRequired,\n companyId,\n companyName,\n composedName,\n contactUpdate,\n customChanges,\n department,\n doNotCall,\n enqueueSnackbar,\n firstName,\n siteHostId,\n jobTitle,\n lastName,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n otherAddress,\n ownerUid,\n phones,\n record.$id,\n record.doNotCall,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n record.reportsToContactId,\n record.salutation,\n reportsTo,\n salutation,\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 /** One phone field, with click-to-call off what is IN the box (AGL-2661). */\n const phoneField = (field: PhoneField) => {\n const value = phones[field]\n const telHref = crmTelHref(value)\n const callLabel = doNotCall\n ? `Call ${value.trim()} — marked do not call`\n : `Call ${value.trim()}`\n return (\n <Grid key={field} size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label={CONTACT_PHONE_FIELD_LABELS[field]}\n value={value}\n onChange={(event) => change(setPhones, { ...phones, [field]: event.target.value })}\n error={Boolean(phoneErrors[field])}\n helperText={phoneErrors[field] || (field === 'phone' ? PHONE_HELP : undefined)}\n fullWidth\n slotProps={{\n input: {\n /*\n * The number on screen is the one a reader means to ring, and\n * `crmTelHref` withholds the link from anything half-typed. A\n * do-not-call person keeps the link — the rep decides — with\n * the request said in the tooltip and the icon's color.\n */\n endAdornment:\n telHref && field !== 'fax' ? (\n <InputAdornment position=\"end\">\n <Tooltip title={callLabel}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n color={doNotCall ? 'warning' : 'default'}\n aria-label={callLabel}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n )\n }\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#contact-properties' })}\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 <SectionHeading>{'Contact information'}</SectionHeading>\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 <CrmPicklistSelect\n picklistId={CRM_SALUTATION_PICKLIST}\n picklist={salutations.picklist}\n value={salutation}\n stored={record.salutation ?? ''}\n onChange={(value) => change(setSalutation, value)}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"First name\"\n value={firstName}\n onChange={(event) => change(setFirstName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Last name\"\n value={lastName}\n onChange={(event) => change(setLastName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: CONTACT_NAME_PART_MAX } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={composedName || nameOverride}\n onChange={(event) => change(setNameOverride, event.target.value)}\n slotProps={{\n htmlInput: { maxLength: 120 },\n input: { readOnly: Boolean(composedName) },\n }}\n helperText={\n composedName\n ? 'Made of the first and last name.'\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=\"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 <TextField\n size=\"small\"\n label=\"Department\"\n value={department}\n onChange={(event) => change(setDepartment, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\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 <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 <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\n <SectionHeading>{'Phones'}</SectionHeading>\n {PHONE_FIELDS.filter((field) => field !== 'assistantPhone').map(phoneField)}\n <Grid size={{ xs: 12, sm: 6 }}>\n <FormControlLabel\n control={\n <Checkbox\n checked={doNotCall}\n onChange={(event) => change(setDoNotCall, event.target.checked)}\n />\n }\n label=\"Do not call\"\n />\n <FormHelperText>\n {'They asked not to be phoned. Every number still dials; the Call button says so.'}\n </FormHelperText>\n </Grid>\n\n <SectionHeading>{'Addresses'}</SectionHeading>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Mailing address'}</Typography>\n <ContactAddressFields\n value={address}\n onChange={(value) => change(setAddress, value)}\n />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Other address'}</Typography>\n <ContactAddressFields\n value={otherAddress}\n onChange={(value) => change(setOtherAddress, value)}\n />\n </Stack>\n </Grid>\n\n <SectionHeading>{'Additional information'}</SectionHeading>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n type=\"date\"\n label=\"Birthdate\"\n value={birthdate}\n onChange={(event) => change(setBirthdate, event.target.value)}\n error={Boolean(birthdateError)}\n helperText={birthdateError || undefined}\n slotProps={{ inputLabel: { shrink: true } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <ContactReportsToField\n hostId={hostId}\n org={org}\n contactId={record.$id}\n groupId={consentGroup.groupId}\n value={reportsTo}\n onChange={(value) => change(setReportsTo, value)}\n disabled={saving}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Assistant\"\n value={assistantName}\n onChange={(event) => change(setAssistantName, event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n {phoneField('assistantPhone')}\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","composeContactName","CONTACT_BIRTHDATE_REFUSAL","CONTACT_EXTRA_PHONE_FIELDS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_NAME_PART_MAX","CONTACT_PHONE_FIELD_LABELS","CRM_SALUTATION_PICKLIST","crmTelHref","normalizeContactBirthdate","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Checkbox","FormControlLabel","FormHelperText","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useContactUpdate","useCrmActivityLogger","useCrmPicklist","useLeadSourcePicklist","parseContactTags","setContactStage","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","ContactReportsToField","CrmCustomFieldControl","CrmPicklistSelect","crmSuiteLockedReason","LeadSourceSelect","PHONE_FIELDS","phoneDraftsFrom","record","Object","fromEntries","map","field","String","readPhones","drafts","phones","errors","typed","trim","normalized","PHONE_HELP_ERROR","keys","length","ok","PHONE_HELP","SectionHeading","props","size","xs","variant","color","children","ContactPropertiesCard","hostId","org","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","salutation","setSalutation","salutations","firstName","setFirstName","lastName","setLastName","setPhones","doNotCall","setDoNotCall","jobTitle","setJobTitle","department","setDepartment","birthdate","setBirthdate","assistantName","setAssistantName","reportsTo","setReportsTo","reportsToContactId","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","otherAddress","setOtherAddress","phoneErrors","setPhoneErrors","birthdateError","setBirthdateError","saving","setSaving","custom","setCustom","edited","setEdited","fields","customFields","active","storedCustom","customChanges","clearingRequired","composedName","change","set","value","reseed","recordId","$id","handleSave","readPhonesResult","storedBirthdate","stageChanged","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","message","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","phoneField","telHref","callLabel","sm","label","onChange","event","target","Boolean","helperText","undefined","fullWidth","slotProps","input","endAdornment","position","title","component","href","edge","aria-label","path","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","disabled","onClick","container","readOnly","picklistId","picklist","stored","htmlInput","maxLength","canonicalName","ready","truncated","company","onCreate","fallbackName","select","stage","memberName","owner","crmMemberPickerLabel","placeholder","filter","control","checked","inputLabel","shrink","contactId","groupId","multiline","minRows","definition","key","displayName"],"mappings":"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"}
|
|
@@ -102,20 +102,32 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
102
102
|
}
|
|
103
103
|
setSaving(true);
|
|
104
104
|
const next = containerMembershipValue(selected);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
let verdict;
|
|
106
|
+
try {
|
|
107
|
+
verdict = await writeGuardedBySeed({
|
|
108
|
+
subject: 'lead',
|
|
109
|
+
fromCache,
|
|
110
|
+
unreadable: leadStatus === 'error'
|
|
111
|
+
}, async ()=>{
|
|
112
|
+
await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), _extends({
|
|
113
|
+
campaignIds: next
|
|
114
|
+
}, crmClientListFields('leads', lead, {
|
|
115
|
+
campaignIds: next
|
|
116
|
+
}, LEAD_CAMPAIGN_LIST_FIELDS), {
|
|
117
|
+
updatedAt: serverTimestamp()
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// A refused write has to say so, not leave the button spinning (AGL-4120).
|
|
122
|
+
console.error(error);
|
|
123
|
+
enqueueSnackbar('The filing could not be saved.', {
|
|
124
|
+
variant: 'error',
|
|
125
|
+
allowDuplicate: true
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
} finally{
|
|
129
|
+
setSaving(false);
|
|
130
|
+
}
|
|
119
131
|
// A rule may share by campaign (AGL-3336).
|
|
120
132
|
if (verdict.ok) followUpSharing('leads', [
|
|
121
133
|
leadId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n containerMembershipUnchanged,\n containerMembershipValue,\n pluginDocsHelp,\n readContainerIds,\n} from '@aglyn/aglyn'\nimport ContainerPicker, {\n type ContainerPickerOption,\n} from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly ContainerPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readContainerIds(lead, 'campaign')\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly ContainerPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `containerMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readContainerIds(lead, 'campaign')\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = containerMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = containerMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#lead-campaigns' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\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 setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <ContainerPicker\n kind=\"campaign\"\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["containerMembershipUnchanged","containerMembershipValue","pluginDocsHelp","readContainerIds","ContainerPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","kind","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n containerMembershipUnchanged,\n containerMembershipValue,\n pluginDocsHelp,\n readContainerIds,\n} from '@aglyn/aglyn'\nimport ContainerPicker, {\n type ContainerPickerOption,\n} from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly ContainerPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readContainerIds(lead, 'campaign')\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly ContainerPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `containerMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readContainerIds(lead, 'campaign')\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = containerMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = containerMembershipValue(selected)\n let verdict: Awaited<ReturnType<typeof writeGuardedBySeed>>\n try {\n verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n } catch (error) {\n // A refused write has to say so, not leave the button spinning (AGL-4120).\n console.error(error)\n enqueueSnackbar('The filing could not be saved.', {\n variant: 'error',\n allowDuplicate: true,\n })\n return\n } finally {\n setSaving(false)\n }\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#lead-campaigns' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\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 setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <ContainerPicker\n kind=\"campaign\"\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["containerMembershipUnchanged","containerMembershipValue","pluginDocsHelp","readContainerIds","ContainerPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","error","console","allowDuplicate","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","Error","spacing","kind","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
@@ -109,7 +109,7 @@ import { crmTaskCallScope } from "../model/task-routes.js";
|
|
|
109
109
|
const failed = `The ${definition.plural} could not be saved.`;
|
|
110
110
|
const isStandard = (value)=>isStandardPicklistValueId(definition, value.id);
|
|
111
111
|
/** Write the whole list — the list-only moves above. */ const save = useCallback(async (next, done)=>{
|
|
112
|
-
if (!orgId || busy) return;
|
|
112
|
+
if (!orgId || busy) return false;
|
|
113
113
|
setBusy(true);
|
|
114
114
|
try {
|
|
115
115
|
const verdict = await writeGuardedBySeed({
|
|
@@ -132,18 +132,20 @@ import { crmTaskCallScope } from "../model/task-routes.js";
|
|
|
132
132
|
variant: 'warning',
|
|
133
133
|
persist: false
|
|
134
134
|
});
|
|
135
|
-
return;
|
|
135
|
+
return false;
|
|
136
136
|
}
|
|
137
137
|
enqueueSnackbar(done, {
|
|
138
138
|
variant: 'success',
|
|
139
139
|
persist: false
|
|
140
140
|
});
|
|
141
|
+
return true;
|
|
141
142
|
} catch (error) {
|
|
142
143
|
console.error(error);
|
|
143
144
|
enqueueSnackbar(failed, {
|
|
144
145
|
variant: 'error',
|
|
145
146
|
allowDuplicate: true
|
|
146
147
|
});
|
|
148
|
+
return false;
|
|
147
149
|
} finally{
|
|
148
150
|
setBusy(false);
|
|
149
151
|
}
|
|
@@ -255,8 +257,9 @@ import { crmTaskCallScope } from "../model/task-routes.js";
|
|
|
255
257
|
});
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
// The dialog holds what was typed until the list has really saved
|
|
261
|
+
// (AGL-4120): closing first made a refused add read as done.
|
|
262
|
+
if (await save(move.picklist, `“${draftName.trim()}” added`)) setNameDialog(null);
|
|
260
263
|
return;
|
|
261
264
|
}
|
|
262
265
|
const result = await post({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/picklist-values-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_LEAD_TEXT_MAX,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistValue,\n isStandardPicklistValueId,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n} from '@aglyn/aglyn'\nimport {\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiDragVertical,\n mdiEyeOffOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n mdiStarOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n IconButton,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, serverTimestamp, setDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport {\n addPicklistValue,\n crmPicklistValuesRouteUrl,\n movePicklistValue,\n picklistAddableMeanings,\n picklistKeepsGroups,\n picklistMeaningLabel,\n picklistReplacements,\n type PicklistValuesAction,\n type PicklistValuesRequest,\n type PicklistValuesResponse,\n setPicklistDefault,\n setPicklistValueActive,\n setPicklistValueGroup,\n sortPicklistValues,\n} from '../model/picklist-values'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport interface PicklistValuesCardProps {\n /** Which of the CRM's standard picklists this card manages. */\n picklistId: CrmPicklistId\n /** The mounted site, or `null` at the organization level. */\n hostId: string | null\n /** The org whose list this is; `null` while it settles. */\n orgId: string | null\n /** The site a newly written list records as its definer — the Fields page's own. */\n createHostId: string | null\n}\n\n/** Each object's noun, singular and plural, for the card's sentences. */\nconst OBJECT_NOUNS: Record<CrmPicklistObject, [string, string]> = {\n contact: ['contact', 'contacts'],\n company: ['company', 'companies'],\n deal: ['deal', 'deals'],\n lead: ['lead', 'leads'],\n task: ['task', 'tasks'],\n}\n\n/** \"a\", \"a and b\", \"a, b and c\". */\nfunction sentenceList(words: readonly string[]): string {\n return words.length < 2\n ? (words[0] ?? '')\n : `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`\n}\n\n/** The objects whose records hold the field's label, in the definition's order. */\nfunction targetObjects(definition: CrmPicklistDefinition): CrmPicklistObject[] {\n return [...new Set(definition.targets.map((target) => target.object))]\n}\n\n/** The add and rename dialog's state: which value, if any, is being renamed. */\ntype NameDialog = { mode: 'add' } | { mode: 'rename'; value: CrmPicklistValue } | null\n\n/**\n * ONE STANDARD PICKLIST'S VALUES (AGL-3298, AGL-3510) — Salesforce's\n * picklist value set for a standard field, managed on the Fields tab of the\n * object the field belongs to.\n *\n * Standard values are marked, and cannot be deleted — every org has them —\n * but rename, reorder, regroup, deactivate and default are theirs as much\n * as an added value's. Every move that touches only the LIST — add,\n * reorder by drag or arrows, sort A–Z, activate, deactivate, group,\n * default — is one client write of the whole document, guarded against a\n * cached read like every seeded save on the Fields page. The ones that\n * change RECORDS — rename, and delete with a replacement, and on a list\n * whose records keep the value's group (Lead source's direction, AGL-3577)\n * group and add — go to `crm/picklist-values`, which rewrites the list and\n * every record the definition's targets name together; see that route.\n *\n * An org that has never edited the list reads the standard values, and the\n * first move here writes them down as the org's own.\n */\nexport function PicklistValuesCard(props: PicklistValuesCardProps) {\n const { picklistId, hostId, orgId, createHostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { definition, picklist, stored, ready, fromCache } = useCrmPicklist(picklistId, orgId)\n const [busy, setBusy] = useState(false)\n const [nameDialog, setNameDialog] = useState<NameDialog>(null)\n const [draftName, setDraftName] = useState('')\n const [draftGroup, setDraftGroup] = useState('')\n const [draftMeaning, setDraftMeaning] = useState('')\n const [nameError, setNameError] = useState('')\n const [deleting, setDeleting] = useState<CrmPicklistValue | null>(null)\n const [replaceWith, setReplaceWith] = useState('')\n const [dragFrom, setDragFrom] = useState<number | null>(null)\n\n const groups = definition.groups ?? []\n // Whether records keep a value's group, so a regroup and an add change them too.\n const keepsGroups = picklistKeepsGroups(definition)\n const meanings = definition.meanings ?? []\n // An added value may not take a meaning only the platform sets (AGL-3512).\n const addableMeanings = picklistAddableMeanings(definition)\n const [noun, nouns] = OBJECT_NOUNS[definition.object]\n const holders = targetObjects(definition).map((object) => OBJECT_NOUNS[object])\n // A list of suggestions only — a task's subjects (AGL-3517) — that no record holds.\n const suggestions = holders.length === 0\n const field = definition.label.toLowerCase()\n const failed = `The ${definition.plural} could not be saved.`\n const isStandard = (value: CrmPicklistValue) => isStandardPicklistValueId(definition, value.id)\n\n /** Write the whole list — the list-only moves above. */\n const save = useCallback(\n async (next: CrmPicklist, done: string) => {\n if (!orgId || busy) return\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed({ subject: `${field} list`, fromCache }, () =>\n setDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.picklists, picklistId),\n {\n values: next.values,\n defaultValueId: next.defaultValueId,\n updatedAt: serverTimestamp(),\n // The org-wide stamp the rules require of a new CRM document,\n // the same one a field definition carries.\n ...(stored\n ? {}\n : {\n hostId: createHostId,\n createdAt: serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }),\n },\n { merge: true },\n ),\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failed, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(false)\n }\n },\n [orgId, busy, field, fromCache, firestore, picklistId, stored, createHostId, enqueueSnackbar, failed],\n )\n\n /** A rename or a delete — the moves that change records too. */\n const post = useCallback(\n async (request: PicklistValuesAction): Promise<PicklistValuesResponse | null> => {\n const scope = crmTaskCallScope(hostId, orgId)\n if (!scope || busy) return null\n setBusy(true)\n try {\n const response = await authorizedFetch(user, crmPicklistValuesRouteUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...request, picklistId, ...scope } satisfies PicklistValuesRequest),\n })\n const payload = (await response.json().catch(() => ({}))) as { error?: string }\n if (!response.ok) {\n enqueueSnackbar(payload.error || failed, { variant: 'warning', persist: false })\n return null\n }\n return payload as PicklistValuesResponse\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failed, { variant: 'error', allowDuplicate: true })\n return null\n } finally {\n setBusy(false)\n }\n },\n [hostId, orgId, busy, user, picklistId, enqueueSnackbar, failed],\n )\n\n /** \" — 2 leads and 1 contact updated\", or nothing when no record held the value. */\n const moved = (result: PicklistValuesResponse) => {\n const counts = targetObjects(definition).map((object) => {\n const count = result.updated?.[object] ?? 0\n const [one, many] = OBJECT_NOUNS[object]\n return { count, text: `${count.toLocaleString()} ${count === 1 ? one : many}` }\n })\n return counts.some((entry) => entry.count)\n ? ` — ${sentenceList(counts.map((entry) => entry.text))} updated`\n : ''\n }\n\n const openAdd = () => {\n setDraftName('')\n setDraftGroup('')\n setDraftMeaning('')\n setNameError('')\n setNameDialog({ mode: 'add' })\n }\n const openRename = (value: CrmPicklistValue) => {\n setDraftName(value.label)\n setNameError('')\n setNameDialog({ mode: 'rename', value })\n }\n\n const submitName = async () => {\n if (!nameDialog) return\n if (nameDialog.mode === 'add') {\n const move = addPicklistValue(definition, picklist, draftName, {\n group: draftGroup || null,\n meaning: draftMeaning || null,\n })\n if (move.ok === false) return void setNameError(move.error)\n if (keepsGroups) {\n // Records may already hold the label, and keep its group (AGL-3577).\n const result = await post({\n action: 'add',\n label: draftName,\n group: draftGroup || null,\n meaning: draftMeaning || null,\n })\n if (!result) return\n setNameDialog(null)\n enqueueSnackbar(`“${draftName.trim()}” added${moved(result)}`, {\n variant: 'success',\n persist: false,\n })\n return\n }\n setNameDialog(null)\n await save(move.picklist, `“${draftName.trim()}” added`)\n return\n }\n const result = await post({ action: 'rename', valueId: nameDialog.value.id, label: draftName })\n if (!result) return\n setNameDialog(null)\n enqueueSnackbar(`Renamed${moved(result)}`, { variant: 'success', persist: false })\n }\n\n const openDelete = (value: CrmPicklistValue) => {\n setReplaceWith('')\n setDeleting(value)\n }\n const submitDelete = async () => {\n if (!deleting) return\n const result = await post({\n action: 'delete',\n valueId: deleting.id,\n replaceWith: replaceWith || null,\n })\n if (!result) return\n setDeleting(null)\n enqueueSnackbar(`“${deleting.label}” deleted${moved(result)}`, {\n variant: 'success',\n persist: false,\n })\n }\n\n const move = (from: number, to: number) =>\n void save(movePicklistValue(picklist, from, to), 'Order saved')\n\n const rowActions = (value: CrmPicklistValue): RowActionsMenuItem[] => {\n const isDefault = picklist.defaultValueId === value.id\n const items: RowActionsMenuItem[] = [\n {\n key: 'rename',\n label: 'Rename',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n disabled: busy,\n onClick: () => openRename(value),\n },\n {\n key: 'default',\n label: isDefault ? 'Clear default' : 'Make default',\n icon: <MdiIcon path={mdiStarOutline.path} size={0.8} />,\n disabled: busy || (!isDefault && !value.active),\n disabledReason: !value.active ? 'An inactive value cannot be the default' : undefined,\n onClick: () =>\n void save(\n setPicklistDefault(picklist, isDefault ? null : value.id),\n isDefault ? 'Default cleared' : `New ${nouns} start as “${value.label}”`,\n ),\n },\n {\n key: 'active',\n label: value.active ? 'Deactivate' : 'Activate',\n icon: (\n <MdiIcon path={value.active ? mdiEyeOffOutline.path : mdiEyeOutline.path} size={0.8} />\n ),\n disabled: busy,\n onClick: () =>\n void save(\n setPicklistValueActive(picklist, value.id, !value.active),\n value.active ? `“${value.label}” deactivated` : `“${value.label}” activated`,\n ),\n },\n ]\n // A standard value is every organization's: it is deactivated, never deleted.\n if (!isStandard(value)) {\n items.push({\n key: 'delete',\n label: 'Delete…',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: busy,\n onClick: () => openDelete(value),\n })\n }\n return items\n }\n\n const replacements = deleting ? picklistReplacements(definition, picklist, deleting.id) : []\n const columns = 4 + (groups.length ? 1 : 0) + (meanings.length ? 1 : 0)\n const heldBy = sentenceList(holders.map(([, many]) => many)).replace(/^./, (letter) =>\n letter.toUpperCase(),\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'space-between', alignItems: { sm: 'center' } }}\n >\n <Stack spacing={0.25}>\n <Typography variant=\"subtitle1\">{`${definition.label} values`}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(suggestions\n ? `The suggestions every ${noun}’s ${definition.label} offers, in this order. ` +\n 'Any text may still be typed, and renaming or deleting a suggestion ' +\n `changes no ${noun}. `\n : `The choices in every ${noun}’s ${definition.label} select, in this order. ` +\n (definition.restricted\n ? 'An import or an API write naming anything else is refused. '\n : '') +\n 'A deactivated value stays on the records that hold it. ') +\n 'Standard values come with every organization and cannot be deleted; ' +\n 'add your own beside them.'}\n </Typography>\n </Stack>\n {orgId ? (\n <Stack direction=\"row\" spacing={1} sx={{ flexShrink: 0 }}>\n <Button\n size=\"small\"\n disabled={busy || picklist.values.length < 2}\n onClick={() => void save(sortPicklistValues(picklist), 'Sorted A–Z')}\n >\n {'Sort A–Z'}\n </Button>\n <Button size=\"small\" variant=\"outlined\" disabled={busy} onClick={openAdd}>\n {'Add value'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n {!ready ? null : (\n <ScrollTable size=\"small\" aria-label={`${definition.label} values`}>\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'Value'}</TableCell>\n {groups.length ? <TableCell>{'Group'}</TableCell> : null}\n {meanings.length ? <TableCell>{'Means'}</TableCell> : null}\n <TableCell>{'Status'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {picklist.values.length === 0 ? (\n <TableRow>\n <TableCell colSpan={columns}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No values yet. Add one, and every ${noun} can be given it.`}\n </Typography>\n </TableCell>\n </TableRow>\n ) : null}\n {picklist.values.map((value, index) => (\n <TableRow\n key={value.id}\n hover\n // Native drag and drop for a pointer; the arrows beside the\n // handle are the same move for a keyboard or a touch screen.\n draggable={!busy}\n onDragStart={(event) => {\n setDragFrom(index)\n event.dataTransfer.effectAllowed = 'move'\n }}\n onDragOver={(event) => {\n if (dragFrom === null) return\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }}\n onDrop={(event) => {\n event.preventDefault()\n if (dragFrom !== null && dragFrom !== index) move(dragFrom, index)\n setDragFrom(null)\n }}\n onDragEnd={() => setDragFrom(null)}\n sx={{\n ...(value.active ? {} : { opacity: 0.6 }),\n ...(dragFrom === index ? { outline: 1, outlineColor: 'primary.main' } : {}),\n }}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <MdiIcon\n path={mdiDragVertical.path}\n size={0.8}\n style={{ cursor: busy ? 'default' : 'grab' }}\n aria-hidden\n />\n <IconButton\n size=\"small\"\n disabled={index === 0 || busy}\n onClick={() => move(index, index - 1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${value.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === picklist.values.length - 1 || busy}\n onClick={() => move(index, index + 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${value.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">{value.label}</Typography>\n {isStandard(value) ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Standard\" />\n ) : null}\n </Stack>\n </TableCell>\n {groups.length ? (\n <TableCell sx={{ minWidth: 150 }}>\n <TextField\n select\n size=\"small\"\n variant=\"standard\"\n value={value.group ?? ''}\n disabled={busy || !orgId}\n onChange={(event) => {\n const group = String(event.target.value) || null\n const name = groups.find((entry) => entry.id === group)?.label\n const done = name ? `“${value.label}” is ${name}` : `“${value.label}” has no group`\n if (!keepsGroups) {\n void save(setPicklistValueGroup(definition, picklist, value.id, group), done)\n return\n }\n // Its records keep the group too (AGL-3577): the route moves both.\n void post({ action: 'group', valueId: value.id, group }).then((result) => {\n if (result) {\n enqueueSnackbar(`${done}${moved(result)}`, { variant: 'success', persist: false })\n }\n })\n }}\n fullWidth\n slotProps={{\n select: { displayEmpty: true, 'aria-label': `Group of ${value.label}` },\n }}\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {groups.map((group) => (\n <MenuItem key={group.id} value={group.id}>\n {group.label}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n ) : null}\n {meanings.length ? (\n // What the value means to the CRM: fixed for a standard value, and\n // chosen when an added one is made.\n <TableCell>\n <Typography variant=\"body2\" color={value.meaning ? 'text.primary' : 'text.secondary'}>\n {value.meaning ? picklistMeaningLabel(definition, value.meaning) : '—'}\n </Typography>\n </TableCell>\n ) : null}\n <TableCell>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>\n {value.active ? null : (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Inactive\" />\n )}\n {picklist.defaultValueId === value.id ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"primary\" label=\"Default\" />\n ) : null}\n {value.active && picklist.defaultValueId !== value.id ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Active'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={value.label} items={rowActions(value)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {ready && !stored ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These are the standard values. Your first change here makes the list your ' +\n 'organization’s own.'}\n </Typography>\n ) : null}\n\n <Dialog open={nameDialog !== null} onClose={() => setNameDialog(null)} fullWidth maxWidth=\"xs\">\n <DialogTitle>\n {nameDialog?.mode === 'rename' ? `Rename “${nameDialog.value.label}”` : `Add a ${field}`}\n </DialogTitle>\n <DialogContent>\n <Stack spacing={1.5} sx={{ pt: 1 }}>\n {nameDialog?.mode === 'rename' && !suggestions ? (\n <DialogContentText variant=\"body2\">\n {`Every ${sentenceList(holders.map(([one]) => one))} holding this value ` +\n 'is updated to the new name.'}\n </DialogContentText>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Value\"\n value={draftName}\n onChange={(event) => {\n setDraftName(event.target.value)\n setNameError('')\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter') void submitName()\n }}\n error={Boolean(nameError)}\n helperText={nameError || ' '}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n autoFocus\n fullWidth\n />\n {nameDialog?.mode === 'add' && groups.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Group\"\n value={draftGroup}\n onChange={(event) => setDraftGroup(String(event.target.value))}\n fullWidth\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {groups.map((group) => (\n <MenuItem key={group.id} value={group.id}>\n {group.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {nameDialog?.mode === 'add' && meanings.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Means\"\n value={draftMeaning}\n onChange={(event) => {\n setDraftMeaning(String(event.target.value))\n setNameError('')\n }}\n fullWidth\n >\n {addableMeanings.map((meaning) => (\n <MenuItem key={meaning} value={meaning}>\n {picklistMeaningLabel(definition, meaning)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setNameDialog(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={busy || !draftName.trim()}\n onClick={() => void submitName()}\n >\n {nameDialog?.mode === 'rename' ? 'Rename' : 'Add'}\n </Button>\n </DialogActions>\n </Dialog>\n\n <Dialog open={deleting !== null} onClose={() => setDeleting(null)} fullWidth maxWidth=\"xs\">\n <DialogTitle>{`Delete “${deleting?.label ?? ''}”?`}</DialogTitle>\n <DialogContent>\n <Stack spacing={1.5} sx={{ pt: 1 }}>\n <DialogContentText variant=\"body2\">\n {suggestions\n ? `The suggestion leaves the list for good. No ${noun} changes.`\n : `The value leaves the list for good. ${heldBy} that hold it ` +\n (meanings.length\n ? 'are moved to the value you pick here, which means the same. '\n : 'are moved to the value you pick here, or cleared. ') +\n 'To keep it on those records instead, deactivate it.'}\n </DialogContentText>\n {suggestions ? null : (\n <TextField\n select\n size=\"small\"\n label=\"Replace it on existing records with\"\n value={replaceWith}\n onChange={(event) => setReplaceWith(String(event.target.value))}\n fullWidth\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n >\n {/* A value with a meaning moves its records to another of that meaning, never off. */}\n {meanings.length ? null : <MenuItem value=\"\">{'None — clear it'}</MenuItem>}\n {replacements.map((value) => (\n <MenuItem key={value.id} value={value.label}>\n {value.label}\n </MenuItem>\n ))}\n </TextField>\n )}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDeleting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"error\"\n disabled={busy || (meanings.length > 0 && !replaceWith)}\n onClick={() => void submitDelete()}\n >\n {'Delete value'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nPicklistValuesCard.displayName = 'PicklistValuesCard'\n\nexport default PicklistValuesCard\n"],"names":["CRM_COLLECTIONS","CRM_LEAD_TEXT_MAX","isStandardPicklistValueId","newResourceScopeFields","ORG_SCOPE_TOKEN","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiDragVertical","mdiEyeOffOutline","mdiEyeOutline","mdiPencilOutline","mdiStarOutline","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","authorizedFetch","useFirestore","useUser","writeGuardedBySeed","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","IconButton","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","doc","serverTimestamp","setDoc","useCallback","useState","useCrmPicklist","addPicklistValue","crmPicklistValuesRouteUrl","movePicklistValue","picklistAddableMeanings","picklistKeepsGroups","picklistMeaningLabel","picklistReplacements","setPicklistDefault","setPicklistValueActive","setPicklistValueGroup","sortPicklistValues","crmTaskCallScope","OBJECT_NOUNS","contact","company","deal","lead","task","sentenceList","words","length","slice","join","targetObjects","definition","Set","targets","map","target","object","PicklistValuesCard","props","picklistId","hostId","orgId","createHostId","firestore","data","user","enqueueSnackbar","picklist","stored","ready","fromCache","busy","setBusy","nameDialog","setNameDialog","draftName","setDraftName","draftGroup","setDraftGroup","draftMeaning","setDraftMeaning","nameError","setNameError","deleting","setDeleting","replaceWith","setReplaceWith","dragFrom","setDragFrom","groups","keepsGroups","meanings","addableMeanings","noun","nouns","holders","suggestions","field","label","toLowerCase","failed","plural","isStandard","value","id","save","next","done","verdict","subject","picklists","values","defaultValueId","updatedAt","createdAt","merge","ok","message","variant","persist","error","console","allowDuplicate","post","request","scope","response","method","headers","body","JSON","stringify","payload","json","catch","moved","result","counts","count","updated","one","many","text","toLocaleString","some","entry","openAdd","mode","openRename","submitName","move","group","meaning","action","trim","valueId","openDelete","submitDelete","from","to","rowActions","isDefault","items","key","icon","path","size","disabled","onClick","active","disabledReason","undefined","push","destructive","replacements","columns","heldBy","replace","letter","toUpperCase","spacing","direction","xs","sm","sx","justifyContent","alignItems","color","restricted","flexShrink","aria-label","width","align","colSpan","index","hover","draggable","onDragStart","event","dataTransfer","effectAllowed","onDragOver","preventDefault","dropEffect","onDrop","onDragEnd","opacity","outline","outlineColor","style","cursor","aria-hidden","minWidth","select","onChange","String","name","find","then","fullWidth","slotProps","displayEmpty","useFlexGap","flexWrap","open","onClose","maxWidth","pt","onKeyDown","Boolean","helperText","htmlInput","maxLength","autoFocus","inputLabel","shrink","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/picklist-values-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_LEAD_TEXT_MAX,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistValue,\n isStandardPicklistValueId,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n} from '@aglyn/aglyn'\nimport {\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiDragVertical,\n mdiEyeOffOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n mdiStarOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n IconButton,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, serverTimestamp, setDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useCrmPicklist } from '../hooks/use-crm-picklist'\nimport {\n addPicklistValue,\n crmPicklistValuesRouteUrl,\n movePicklistValue,\n picklistAddableMeanings,\n picklistKeepsGroups,\n picklistMeaningLabel,\n picklistReplacements,\n type PicklistValuesAction,\n type PicklistValuesRequest,\n type PicklistValuesResponse,\n setPicklistDefault,\n setPicklistValueActive,\n setPicklistValueGroup,\n sortPicklistValues,\n} from '../model/picklist-values'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport interface PicklistValuesCardProps {\n /** Which of the CRM's standard picklists this card manages. */\n picklistId: CrmPicklistId\n /** The mounted site, or `null` at the organization level. */\n hostId: string | null\n /** The org whose list this is; `null` while it settles. */\n orgId: string | null\n /** The site a newly written list records as its definer — the Fields page's own. */\n createHostId: string | null\n}\n\n/** Each object's noun, singular and plural, for the card's sentences. */\nconst OBJECT_NOUNS: Record<CrmPicklistObject, [string, string]> = {\n contact: ['contact', 'contacts'],\n company: ['company', 'companies'],\n deal: ['deal', 'deals'],\n lead: ['lead', 'leads'],\n task: ['task', 'tasks'],\n}\n\n/** \"a\", \"a and b\", \"a, b and c\". */\nfunction sentenceList(words: readonly string[]): string {\n return words.length < 2\n ? (words[0] ?? '')\n : `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`\n}\n\n/** The objects whose records hold the field's label, in the definition's order. */\nfunction targetObjects(definition: CrmPicklistDefinition): CrmPicklistObject[] {\n return [...new Set(definition.targets.map((target) => target.object))]\n}\n\n/** The add and rename dialog's state: which value, if any, is being renamed. */\ntype NameDialog = { mode: 'add' } | { mode: 'rename'; value: CrmPicklistValue } | null\n\n/**\n * ONE STANDARD PICKLIST'S VALUES (AGL-3298, AGL-3510) — Salesforce's\n * picklist value set for a standard field, managed on the Fields tab of the\n * object the field belongs to.\n *\n * Standard values are marked, and cannot be deleted — every org has them —\n * but rename, reorder, regroup, deactivate and default are theirs as much\n * as an added value's. Every move that touches only the LIST — add,\n * reorder by drag or arrows, sort A–Z, activate, deactivate, group,\n * default — is one client write of the whole document, guarded against a\n * cached read like every seeded save on the Fields page. The ones that\n * change RECORDS — rename, and delete with a replacement, and on a list\n * whose records keep the value's group (Lead source's direction, AGL-3577)\n * group and add — go to `crm/picklist-values`, which rewrites the list and\n * every record the definition's targets name together; see that route.\n *\n * An org that has never edited the list reads the standard values, and the\n * first move here writes them down as the org's own.\n */\nexport function PicklistValuesCard(props: PicklistValuesCardProps) {\n const { picklistId, hostId, orgId, createHostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { definition, picklist, stored, ready, fromCache } = useCrmPicklist(picklistId, orgId)\n const [busy, setBusy] = useState(false)\n const [nameDialog, setNameDialog] = useState<NameDialog>(null)\n const [draftName, setDraftName] = useState('')\n const [draftGroup, setDraftGroup] = useState('')\n const [draftMeaning, setDraftMeaning] = useState('')\n const [nameError, setNameError] = useState('')\n const [deleting, setDeleting] = useState<CrmPicklistValue | null>(null)\n const [replaceWith, setReplaceWith] = useState('')\n const [dragFrom, setDragFrom] = useState<number | null>(null)\n\n const groups = definition.groups ?? []\n // Whether records keep a value's group, so a regroup and an add change them too.\n const keepsGroups = picklistKeepsGroups(definition)\n const meanings = definition.meanings ?? []\n // An added value may not take a meaning only the platform sets (AGL-3512).\n const addableMeanings = picklistAddableMeanings(definition)\n const [noun, nouns] = OBJECT_NOUNS[definition.object]\n const holders = targetObjects(definition).map((object) => OBJECT_NOUNS[object])\n // A list of suggestions only — a task's subjects (AGL-3517) — that no record holds.\n const suggestions = holders.length === 0\n const field = definition.label.toLowerCase()\n const failed = `The ${definition.plural} could not be saved.`\n const isStandard = (value: CrmPicklistValue) => isStandardPicklistValueId(definition, value.id)\n\n /** Write the whole list — the list-only moves above. */\n const save = useCallback(\n async (next: CrmPicklist, done: string): Promise<boolean> => {\n if (!orgId || busy) return false\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed({ subject: `${field} list`, fromCache }, () =>\n setDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.picklists, picklistId),\n {\n values: next.values,\n defaultValueId: next.defaultValueId,\n updatedAt: serverTimestamp(),\n // The org-wide stamp the rules require of a new CRM document,\n // the same one a field definition carries.\n ...(stored\n ? {}\n : {\n hostId: createHostId,\n createdAt: serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }),\n },\n { merge: true },\n ),\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failed, { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [orgId, busy, field, fromCache, firestore, picklistId, stored, createHostId, enqueueSnackbar, failed],\n )\n\n /** A rename or a delete — the moves that change records too. */\n const post = useCallback(\n async (request: PicklistValuesAction): Promise<PicklistValuesResponse | null> => {\n const scope = crmTaskCallScope(hostId, orgId)\n if (!scope || busy) return null\n setBusy(true)\n try {\n const response = await authorizedFetch(user, crmPicklistValuesRouteUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...request, picklistId, ...scope } satisfies PicklistValuesRequest),\n })\n const payload = (await response.json().catch(() => ({}))) as { error?: string }\n if (!response.ok) {\n enqueueSnackbar(payload.error || failed, { variant: 'warning', persist: false })\n return null\n }\n return payload as PicklistValuesResponse\n } catch (error) {\n console.error(error)\n enqueueSnackbar(failed, { variant: 'error', allowDuplicate: true })\n return null\n } finally {\n setBusy(false)\n }\n },\n [hostId, orgId, busy, user, picklistId, enqueueSnackbar, failed],\n )\n\n /** \" — 2 leads and 1 contact updated\", or nothing when no record held the value. */\n const moved = (result: PicklistValuesResponse) => {\n const counts = targetObjects(definition).map((object) => {\n const count = result.updated?.[object] ?? 0\n const [one, many] = OBJECT_NOUNS[object]\n return { count, text: `${count.toLocaleString()} ${count === 1 ? one : many}` }\n })\n return counts.some((entry) => entry.count)\n ? ` — ${sentenceList(counts.map((entry) => entry.text))} updated`\n : ''\n }\n\n const openAdd = () => {\n setDraftName('')\n setDraftGroup('')\n setDraftMeaning('')\n setNameError('')\n setNameDialog({ mode: 'add' })\n }\n const openRename = (value: CrmPicklistValue) => {\n setDraftName(value.label)\n setNameError('')\n setNameDialog({ mode: 'rename', value })\n }\n\n const submitName = async () => {\n if (!nameDialog) return\n if (nameDialog.mode === 'add') {\n const move = addPicklistValue(definition, picklist, draftName, {\n group: draftGroup || null,\n meaning: draftMeaning || null,\n })\n if (move.ok === false) return void setNameError(move.error)\n if (keepsGroups) {\n // Records may already hold the label, and keep its group (AGL-3577).\n const result = await post({\n action: 'add',\n label: draftName,\n group: draftGroup || null,\n meaning: draftMeaning || null,\n })\n if (!result) return\n setNameDialog(null)\n enqueueSnackbar(`“${draftName.trim()}” added${moved(result)}`, {\n variant: 'success',\n persist: false,\n })\n return\n }\n // The dialog holds what was typed until the list has really saved\n // (AGL-4120): closing first made a refused add read as done.\n if (await save(move.picklist, `“${draftName.trim()}” added`)) setNameDialog(null)\n return\n }\n const result = await post({ action: 'rename', valueId: nameDialog.value.id, label: draftName })\n if (!result) return\n setNameDialog(null)\n enqueueSnackbar(`Renamed${moved(result)}`, { variant: 'success', persist: false })\n }\n\n const openDelete = (value: CrmPicklistValue) => {\n setReplaceWith('')\n setDeleting(value)\n }\n const submitDelete = async () => {\n if (!deleting) return\n const result = await post({\n action: 'delete',\n valueId: deleting.id,\n replaceWith: replaceWith || null,\n })\n if (!result) return\n setDeleting(null)\n enqueueSnackbar(`“${deleting.label}” deleted${moved(result)}`, {\n variant: 'success',\n persist: false,\n })\n }\n\n const move = (from: number, to: number) =>\n void save(movePicklistValue(picklist, from, to), 'Order saved')\n\n const rowActions = (value: CrmPicklistValue): RowActionsMenuItem[] => {\n const isDefault = picklist.defaultValueId === value.id\n const items: RowActionsMenuItem[] = [\n {\n key: 'rename',\n label: 'Rename',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n disabled: busy,\n onClick: () => openRename(value),\n },\n {\n key: 'default',\n label: isDefault ? 'Clear default' : 'Make default',\n icon: <MdiIcon path={mdiStarOutline.path} size={0.8} />,\n disabled: busy || (!isDefault && !value.active),\n disabledReason: !value.active ? 'An inactive value cannot be the default' : undefined,\n onClick: () =>\n void save(\n setPicklistDefault(picklist, isDefault ? null : value.id),\n isDefault ? 'Default cleared' : `New ${nouns} start as “${value.label}”`,\n ),\n },\n {\n key: 'active',\n label: value.active ? 'Deactivate' : 'Activate',\n icon: (\n <MdiIcon path={value.active ? mdiEyeOffOutline.path : mdiEyeOutline.path} size={0.8} />\n ),\n disabled: busy,\n onClick: () =>\n void save(\n setPicklistValueActive(picklist, value.id, !value.active),\n value.active ? `“${value.label}” deactivated` : `“${value.label}” activated`,\n ),\n },\n ]\n // A standard value is every organization's: it is deactivated, never deleted.\n if (!isStandard(value)) {\n items.push({\n key: 'delete',\n label: 'Delete…',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: busy,\n onClick: () => openDelete(value),\n })\n }\n return items\n }\n\n const replacements = deleting ? picklistReplacements(definition, picklist, deleting.id) : []\n const columns = 4 + (groups.length ? 1 : 0) + (meanings.length ? 1 : 0)\n const heldBy = sentenceList(holders.map(([, many]) => many)).replace(/^./, (letter) =>\n letter.toUpperCase(),\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'space-between', alignItems: { sm: 'center' } }}\n >\n <Stack spacing={0.25}>\n <Typography variant=\"subtitle1\">{`${definition.label} values`}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(suggestions\n ? `The suggestions every ${noun}’s ${definition.label} offers, in this order. ` +\n 'Any text may still be typed, and renaming or deleting a suggestion ' +\n `changes no ${noun}. `\n : `The choices in every ${noun}’s ${definition.label} select, in this order. ` +\n (definition.restricted\n ? 'An import or an API write naming anything else is refused. '\n : '') +\n 'A deactivated value stays on the records that hold it. ') +\n 'Standard values come with every organization and cannot be deleted; ' +\n 'add your own beside them.'}\n </Typography>\n </Stack>\n {orgId ? (\n <Stack direction=\"row\" spacing={1} sx={{ flexShrink: 0 }}>\n <Button\n size=\"small\"\n disabled={busy || picklist.values.length < 2}\n onClick={() => void save(sortPicklistValues(picklist), 'Sorted A–Z')}\n >\n {'Sort A–Z'}\n </Button>\n <Button size=\"small\" variant=\"outlined\" disabled={busy} onClick={openAdd}>\n {'Add value'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n {!ready ? null : (\n <ScrollTable size=\"small\" aria-label={`${definition.label} values`}>\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'Value'}</TableCell>\n {groups.length ? <TableCell>{'Group'}</TableCell> : null}\n {meanings.length ? <TableCell>{'Means'}</TableCell> : null}\n <TableCell>{'Status'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {picklist.values.length === 0 ? (\n <TableRow>\n <TableCell colSpan={columns}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No values yet. Add one, and every ${noun} can be given it.`}\n </Typography>\n </TableCell>\n </TableRow>\n ) : null}\n {picklist.values.map((value, index) => (\n <TableRow\n key={value.id}\n hover\n // Native drag and drop for a pointer; the arrows beside the\n // handle are the same move for a keyboard or a touch screen.\n draggable={!busy}\n onDragStart={(event) => {\n setDragFrom(index)\n event.dataTransfer.effectAllowed = 'move'\n }}\n onDragOver={(event) => {\n if (dragFrom === null) return\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }}\n onDrop={(event) => {\n event.preventDefault()\n if (dragFrom !== null && dragFrom !== index) move(dragFrom, index)\n setDragFrom(null)\n }}\n onDragEnd={() => setDragFrom(null)}\n sx={{\n ...(value.active ? {} : { opacity: 0.6 }),\n ...(dragFrom === index ? { outline: 1, outlineColor: 'primary.main' } : {}),\n }}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <MdiIcon\n path={mdiDragVertical.path}\n size={0.8}\n style={{ cursor: busy ? 'default' : 'grab' }}\n aria-hidden\n />\n <IconButton\n size=\"small\"\n disabled={index === 0 || busy}\n onClick={() => move(index, index - 1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${value.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === picklist.values.length - 1 || busy}\n onClick={() => move(index, index + 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${value.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">{value.label}</Typography>\n {isStandard(value) ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Standard\" />\n ) : null}\n </Stack>\n </TableCell>\n {groups.length ? (\n <TableCell sx={{ minWidth: 150 }}>\n <TextField\n select\n size=\"small\"\n variant=\"standard\"\n value={value.group ?? ''}\n disabled={busy || !orgId}\n onChange={(event) => {\n const group = String(event.target.value) || null\n const name = groups.find((entry) => entry.id === group)?.label\n const done = name ? `“${value.label}” is ${name}` : `“${value.label}” has no group`\n if (!keepsGroups) {\n void save(setPicklistValueGroup(definition, picklist, value.id, group), done)\n return\n }\n // Its records keep the group too (AGL-3577): the route moves both.\n void post({ action: 'group', valueId: value.id, group }).then((result) => {\n if (result) {\n enqueueSnackbar(`${done}${moved(result)}`, { variant: 'success', persist: false })\n }\n })\n }}\n fullWidth\n slotProps={{\n select: { displayEmpty: true, 'aria-label': `Group of ${value.label}` },\n }}\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {groups.map((group) => (\n <MenuItem key={group.id} value={group.id}>\n {group.label}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n ) : null}\n {meanings.length ? (\n // What the value means to the CRM: fixed for a standard value, and\n // chosen when an added one is made.\n <TableCell>\n <Typography variant=\"body2\" color={value.meaning ? 'text.primary' : 'text.secondary'}>\n {value.meaning ? picklistMeaningLabel(definition, value.meaning) : '—'}\n </Typography>\n </TableCell>\n ) : null}\n <TableCell>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>\n {value.active ? null : (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Inactive\" />\n )}\n {picklist.defaultValueId === value.id ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"primary\" label=\"Default\" />\n ) : null}\n {value.active && picklist.defaultValueId !== value.id ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Active'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={value.label} items={rowActions(value)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {ready && !stored ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These are the standard values. Your first change here makes the list your ' +\n 'organization’s own.'}\n </Typography>\n ) : null}\n\n <Dialog open={nameDialog !== null} onClose={() => setNameDialog(null)} fullWidth maxWidth=\"xs\">\n <DialogTitle>\n {nameDialog?.mode === 'rename' ? `Rename “${nameDialog.value.label}”` : `Add a ${field}`}\n </DialogTitle>\n <DialogContent>\n <Stack spacing={1.5} sx={{ pt: 1 }}>\n {nameDialog?.mode === 'rename' && !suggestions ? (\n <DialogContentText variant=\"body2\">\n {`Every ${sentenceList(holders.map(([one]) => one))} holding this value ` +\n 'is updated to the new name.'}\n </DialogContentText>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Value\"\n value={draftName}\n onChange={(event) => {\n setDraftName(event.target.value)\n setNameError('')\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter') void submitName()\n }}\n error={Boolean(nameError)}\n helperText={nameError || ' '}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n autoFocus\n fullWidth\n />\n {nameDialog?.mode === 'add' && groups.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Group\"\n value={draftGroup}\n onChange={(event) => setDraftGroup(String(event.target.value))}\n fullWidth\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {groups.map((group) => (\n <MenuItem key={group.id} value={group.id}>\n {group.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {nameDialog?.mode === 'add' && meanings.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Means\"\n value={draftMeaning}\n onChange={(event) => {\n setDraftMeaning(String(event.target.value))\n setNameError('')\n }}\n fullWidth\n >\n {addableMeanings.map((meaning) => (\n <MenuItem key={meaning} value={meaning}>\n {picklistMeaningLabel(definition, meaning)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setNameDialog(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={busy || !draftName.trim()}\n onClick={() => void submitName()}\n >\n {nameDialog?.mode === 'rename' ? 'Rename' : 'Add'}\n </Button>\n </DialogActions>\n </Dialog>\n\n <Dialog open={deleting !== null} onClose={() => setDeleting(null)} fullWidth maxWidth=\"xs\">\n <DialogTitle>{`Delete “${deleting?.label ?? ''}”?`}</DialogTitle>\n <DialogContent>\n <Stack spacing={1.5} sx={{ pt: 1 }}>\n <DialogContentText variant=\"body2\">\n {suggestions\n ? `The suggestion leaves the list for good. No ${noun} changes.`\n : `The value leaves the list for good. ${heldBy} that hold it ` +\n (meanings.length\n ? 'are moved to the value you pick here, which means the same. '\n : 'are moved to the value you pick here, or cleared. ') +\n 'To keep it on those records instead, deactivate it.'}\n </DialogContentText>\n {suggestions ? null : (\n <TextField\n select\n size=\"small\"\n label=\"Replace it on existing records with\"\n value={replaceWith}\n onChange={(event) => setReplaceWith(String(event.target.value))}\n fullWidth\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n >\n {/* A value with a meaning moves its records to another of that meaning, never off. */}\n {meanings.length ? null : <MenuItem value=\"\">{'None — clear it'}</MenuItem>}\n {replacements.map((value) => (\n <MenuItem key={value.id} value={value.label}>\n {value.label}\n </MenuItem>\n ))}\n </TextField>\n )}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDeleting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"error\"\n disabled={busy || (meanings.length > 0 && !replaceWith)}\n onClick={() => void submitDelete()}\n >\n {'Delete value'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nPicklistValuesCard.displayName = 'PicklistValuesCard'\n\nexport default PicklistValuesCard\n"],"names":["CRM_COLLECTIONS","CRM_LEAD_TEXT_MAX","isStandardPicklistValueId","newResourceScopeFields","ORG_SCOPE_TOKEN","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiDragVertical","mdiEyeOffOutline","mdiEyeOutline","mdiPencilOutline","mdiStarOutline","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","authorizedFetch","useFirestore","useUser","writeGuardedBySeed","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","IconButton","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","doc","serverTimestamp","setDoc","useCallback","useState","useCrmPicklist","addPicklistValue","crmPicklistValuesRouteUrl","movePicklistValue","picklistAddableMeanings","picklistKeepsGroups","picklistMeaningLabel","picklistReplacements","setPicklistDefault","setPicklistValueActive","setPicklistValueGroup","sortPicklistValues","crmTaskCallScope","OBJECT_NOUNS","contact","company","deal","lead","task","sentenceList","words","length","slice","join","targetObjects","definition","Set","targets","map","target","object","PicklistValuesCard","props","picklistId","hostId","orgId","createHostId","firestore","data","user","enqueueSnackbar","picklist","stored","ready","fromCache","busy","setBusy","nameDialog","setNameDialog","draftName","setDraftName","draftGroup","setDraftGroup","draftMeaning","setDraftMeaning","nameError","setNameError","deleting","setDeleting","replaceWith","setReplaceWith","dragFrom","setDragFrom","groups","keepsGroups","meanings","addableMeanings","noun","nouns","holders","suggestions","field","label","toLowerCase","failed","plural","isStandard","value","id","save","next","done","verdict","subject","picklists","values","defaultValueId","updatedAt","createdAt","merge","ok","message","variant","persist","error","console","allowDuplicate","post","request","scope","response","method","headers","body","JSON","stringify","payload","json","catch","moved","result","counts","count","updated","one","many","text","toLocaleString","some","entry","openAdd","mode","openRename","submitName","move","group","meaning","action","trim","valueId","openDelete","submitDelete","from","to","rowActions","isDefault","items","key","icon","path","size","disabled","onClick","active","disabledReason","undefined","push","destructive","replacements","columns","heldBy","replace","letter","toUpperCase","spacing","direction","xs","sm","sx","justifyContent","alignItems","color","restricted","flexShrink","aria-label","width","align","colSpan","index","hover","draggable","onDragStart","event","dataTransfer","effectAllowed","onDragOver","preventDefault","dropEffect","onDrop","onDragEnd","opacity","outline","outlineColor","style","cursor","aria-hidden","minWidth","select","onChange","String","name","find","then","fullWidth","slotProps","displayEmpty","useFlexGap","flexWrap","open","onClose","maxWidth","pt","onKeyDown","Boolean","helperText","htmlInput","maxLength","autoFocus","inputLabel","shrink","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,eAAe,EACfC,iBAAiB,EAMjBC,yBAAyB,EACzBC,sBAAsB,EACtBC,eAAe,QACV,eAAc;AACrB,SACEC,YAAY,EACZC,UAAU,EACVC,gBAAgB,EAChBC,eAAe,EACfC,gBAAgB,EAChBC,aAAa,EACbC,gBAAgB,EAChBC,cAAc,QACT,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACtD,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SACEC,YAAY,EACZC,OAAO,EACPC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,MAAM,EACNC,IAAI,EACJC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,iBAAiB,EACjBC,WAAW,EACXC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,GAAG,EAAEC,eAAe,EAAEC,MAAM,QAAQ,qBAAoB;AACjE,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AAC7C,SAASC,cAAc,QAAQ,+BAA2B;AAC1D,SACEC,gBAAgB,EAChBC,yBAAyB,EACzBC,iBAAiB,EACjBC,uBAAuB,EACvBC,mBAAmB,EACnBC,oBAAoB,EACpBC,oBAAoB,EAIpBC,kBAAkB,EAClBC,sBAAsB,EACtBC,qBAAqB,EACrBC,kBAAkB,QACb,8BAA0B;AACjC,SAASC,gBAAgB,QAAQ,0BAAsB;AAavD,uEAAuE,GACvE,MAAMC,eAA4D;IAChEC,SAAS;QAAC;QAAW;KAAW;IAChCC,SAAS;QAAC;QAAW;KAAY;IACjCC,MAAM;QAAC;QAAQ;KAAQ;IACvBC,MAAM;QAAC;QAAQ;KAAQ;IACvBC,MAAM;QAAC;QAAQ;KAAQ;AACzB;AAEA,kCAAkC,GAClC,SAASC,aAAaC,KAAwB;QAEvCA;IADL,OAAOA,MAAMC,MAAM,GAAG,KACjBD,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,KACb,GAAGA,MAAME,KAAK,CAAC,GAAG,CAAC,GAAGC,IAAI,CAAC,MAAM,KAAK,EAAEH,KAAK,CAACA,MAAMC,MAAM,GAAG,EAAE,EAAE;AACvE;AAEA,iFAAiF,GACjF,SAASG,cAAcC,UAAiC;IACtD,OAAO;WAAI,IAAIC,IAAID,WAAWE,OAAO,CAACC,GAAG,CAAC,CAACC,SAAWA,OAAOC,MAAM;KAAG;AACxE;AAKA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;QAgBhDP,oBAGEA;IAlBjB,MAAM,EAAEQ,UAAU,EAAEC,MAAM,EAAEC,KAAK,EAAEC,YAAY,EAAE,GAAGJ;IACpD,MAAMK,YAAY7D;IAClB,MAAM,EAAE8D,MAAMC,IAAI,EAAE,GAAG9D;IACvB,MAAM,EAAE+D,eAAe,EAAE,GAAGlE;IAC5B,MAAM,EAAEmD,UAAU,EAAEgB,QAAQ,EAAEC,MAAM,EAAEC,KAAK,EAAEC,SAAS,EAAE,GAAG5C,eAAeiC,YAAYE;IACtF,MAAM,CAACU,MAAMC,QAAQ,GAAG/C,SAAS;IACjC,MAAM,CAACgD,YAAYC,cAAc,GAAGjD,SAAqB;IACzD,MAAM,CAACkD,WAAWC,aAAa,GAAGnD,SAAS;IAC3C,MAAM,CAACoD,YAAYC,cAAc,GAAGrD,SAAS;IAC7C,MAAM,CAACsD,cAAcC,gBAAgB,GAAGvD,SAAS;IACjD,MAAM,CAACwD,WAAWC,aAAa,GAAGzD,SAAS;IAC3C,MAAM,CAAC0D,UAAUC,YAAY,GAAG3D,SAAkC;IAClE,MAAM,CAAC4D,aAAaC,eAAe,GAAG7D,SAAS;IAC/C,MAAM,CAAC8D,UAAUC,YAAY,GAAG/D,SAAwB;IAExD,MAAMgE,UAAStC,qBAAAA,WAAWsC,MAAM,YAAjBtC,qBAAqB,EAAE;IACtC,iFAAiF;IACjF,MAAMuC,cAAc3D,oBAAoBoB;IACxC,MAAMwC,YAAWxC,uBAAAA,WAAWwC,QAAQ,YAAnBxC,uBAAuB,EAAE;IAC1C,2EAA2E;IAC3E,MAAMyC,kBAAkB9D,wBAAwBqB;IAChD,MAAM,CAAC0C,MAAMC,MAAM,GAAGvD,YAAY,CAACY,WAAWK,MAAM,CAAC;IACrD,MAAMuC,UAAU7C,cAAcC,YAAYG,GAAG,CAAC,CAACE,SAAWjB,YAAY,CAACiB,OAAO;IAC9E,oFAAoF;IACpF,MAAMwC,cAAcD,QAAQhD,MAAM,KAAK;IACvC,MAAMkD,QAAQ9C,WAAW+C,KAAK,CAACC,WAAW;IAC1C,MAAMC,SAAS,CAAC,IAAI,EAAEjD,WAAWkD,MAAM,CAAC,oBAAoB,CAAC;IAC7D,MAAMC,aAAa,CAACC,QAA4BtH,0BAA0BkE,YAAYoD,MAAMC,EAAE;IAE9F,sDAAsD,GACtD,MAAMC,OAAOjF,YACX,OAAOkF,MAAmBC;QACxB,IAAI,CAAC9C,SAASU,MAAM,OAAO;QAC3BC,QAAQ;QACR,IAAI;YACF,MAAMoC,UAAU,MAAMxG,mBAAmB;gBAAEyG,SAAS,GAAGZ,MAAM,KAAK,CAAC;gBAAE3B;YAAU,GAAG,IAChF/C,OACEF,IAAI0C,WAAW,QAAQF,OAAO9E,gBAAgB+H,SAAS,EAAEnD,aACzD;oBACEoD,QAAQL,KAAKK,MAAM;oBACnBC,gBAAgBN,KAAKM,cAAc;oBACnCC,WAAW3F;mBAGP8C,SACA,CAAC,IACD;oBACER,QAAQE;oBACRoD,WAAW5F;mBACRpC,uBAAuB;oBAACC;iBAAgB,KAGnD;oBAAEgI,OAAO;gBAAK;YAGlB,IAAI,CAACP,QAAQQ,EAAE,EAAE;gBACflD,gBAAgB0C,QAAQS,OAAO,EAAE;oBAAEC,SAAS;oBAAWC,SAAS;gBAAM;gBACtE,OAAO;YACT;YACArD,gBAAgByC,MAAM;gBAAEW,SAAS;gBAAWC,SAAS;YAAM;YAC3D,OAAO;QACT,EAAE,OAAOC,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdtD,gBAAgBkC,QAAQ;gBAAEkB,SAAS;gBAASI,gBAAgB;YAAK;YACjE,OAAO;QACT,SAAU;YACRlD,QAAQ;QACV;IACF,GACA;QAACX;QAAOU;QAAM0B;QAAO3B;QAAWP;QAAWJ;QAAYS;QAAQN;QAAcI;QAAiBkC;KAAO;IAGvG,8DAA8D,GAC9D,MAAMuB,OAAOnG,YACX,OAAOoG;QACL,MAAMC,QAAQvF,iBAAiBsB,QAAQC;QACvC,IAAI,CAACgE,SAAStD,MAAM,OAAO;QAC3BC,QAAQ;QACR,IAAI;YACF,MAAMsD,WAAW,MAAM7H,gBAAgBgE,MAAMrC,6BAA6B;gBACxEmG,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAAC,aAAKP;oBAASjE;mBAAekE;YACpD;YACA,MAAMO,UAAW,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;YACrD,IAAI,CAACR,SAASV,EAAE,EAAE;gBAChBlD,gBAAgBkE,QAAQZ,KAAK,IAAIpB,QAAQ;oBAAEkB,SAAS;oBAAWC,SAAS;gBAAM;gBAC9E,OAAO;YACT;YACA,OAAOa;QACT,EAAE,OAAOZ,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdtD,gBAAgBkC,QAAQ;gBAAEkB,SAAS;gBAASI,gBAAgB;YAAK;YACjE,OAAO;QACT,SAAU;YACRlD,QAAQ;QACV;IACF,GACA;QAACZ;QAAQC;QAAOU;QAAMN;QAAMN;QAAYO;QAAiBkC;KAAO;IAGlE,kFAAkF,GAClF,MAAMmC,QAAQ,CAACC;QACb,MAAMC,SAASvF,cAAcC,YAAYG,GAAG,CAAC,CAACE;;gBAC9BgF;YAAd,MAAME,iBAAQF,kBAAAA,OAAOG,OAAO,qBAAdH,eAAgB,CAAChF,OAAO,mBAAI;YAC1C,MAAM,CAACoF,KAAKC,KAAK,GAAGtG,YAAY,CAACiB,OAAO;YACxC,OAAO;gBAAEkF;gBAAOI,MAAM,GAAGJ,MAAMK,cAAc,GAAG,CAAC,EAAEL,UAAU,IAAIE,MAAMC,MAAM;YAAC;QAChF;QACA,OAAOJ,OAAOO,IAAI,CAAC,CAACC,QAAUA,MAAMP,KAAK,IACrC,CAAC,GAAG,EAAE7F,aAAa4F,OAAOnF,GAAG,CAAC,CAAC2F,QAAUA,MAAMH,IAAI,GAAG,QAAQ,CAAC,GAC/D;IACN;IAEA,MAAMI,UAAU;QACdtE,aAAa;QACbE,cAAc;QACdE,gBAAgB;QAChBE,aAAa;QACbR,cAAc;YAAEyE,MAAM;QAAM;IAC9B;IACA,MAAMC,aAAa,CAAC7C;QAClB3B,aAAa2B,MAAML,KAAK;QACxBhB,aAAa;QACbR,cAAc;YAAEyE,MAAM;YAAU5C;QAAM;IACxC;IAEA,MAAM8C,aAAa;QACjB,IAAI,CAAC5E,YAAY;QACjB,IAAIA,WAAW0E,IAAI,KAAK,OAAO;YAC7B,MAAMG,OAAO3H,iBAAiBwB,YAAYgB,UAAUQ,WAAW;gBAC7D4E,OAAO1E,cAAc;gBACrB2E,SAASzE,gBAAgB;YAC3B;YACA,IAAIuE,KAAKlC,EAAE,KAAK,OAAO,OAAO,KAAKlC,aAAaoE,KAAK9B,KAAK;YAC1D,IAAI9B,aAAa;gBACf,qEAAqE;gBACrE,MAAM8C,SAAS,MAAMb,KAAK;oBACxB8B,QAAQ;oBACRvD,OAAOvB;oBACP4E,OAAO1E,cAAc;oBACrB2E,SAASzE,gBAAgB;gBAC3B;gBACA,IAAI,CAACyD,QAAQ;gBACb9D,cAAc;gBACdR,gBAAgB,CAAC,CAAC,EAAES,UAAU+E,IAAI,GAAG,OAAO,EAAEnB,MAAMC,SAAS,EAAE;oBAC7DlB,SAAS;oBACTC,SAAS;gBACX;gBACA;YACF;YACA,kEAAkE;YAClE,6DAA6D;YAC7D,IAAI,MAAMd,KAAK6C,KAAKnF,QAAQ,EAAE,CAAC,CAAC,EAAEQ,UAAU+E,IAAI,GAAG,OAAO,CAAC,GAAGhF,cAAc;YAC5E;QACF;QACA,MAAM8D,SAAS,MAAMb,KAAK;YAAE8B,QAAQ;YAAUE,SAASlF,WAAW8B,KAAK,CAACC,EAAE;YAAEN,OAAOvB;QAAU;QAC7F,IAAI,CAAC6D,QAAQ;QACb9D,cAAc;QACdR,gBAAgB,CAAC,OAAO,EAAEqE,MAAMC,SAAS,EAAE;YAAElB,SAAS;YAAWC,SAAS;QAAM;IAClF;IAEA,MAAMqC,aAAa,CAACrD;QAClBjB,eAAe;QACfF,YAAYmB;IACd;IACA,MAAMsD,eAAe;QACnB,IAAI,CAAC1E,UAAU;QACf,MAAMqD,SAAS,MAAMb,KAAK;YACxB8B,QAAQ;YACRE,SAASxE,SAASqB,EAAE;YACpBnB,aAAaA,eAAe;QAC9B;QACA,IAAI,CAACmD,QAAQ;QACbpD,YAAY;QACZlB,gBAAgB,CAAC,CAAC,EAAEiB,SAASe,KAAK,CAAC,SAAS,EAAEqC,MAAMC,SAAS,EAAE;YAC7DlB,SAAS;YACTC,SAAS;QACX;IACF;IAEA,MAAM+B,OAAO,CAACQ,MAAcC,KAC1B,KAAKtD,KAAK5E,kBAAkBsC,UAAU2F,MAAMC,KAAK;IAEnD,MAAMC,aAAa,CAACzD;QAClB,MAAM0D,YAAY9F,SAAS6C,cAAc,KAAKT,MAAMC,EAAE;QACtD,MAAM0D,QAA8B;YAClC;gBACEC,KAAK;gBACLjE,OAAO;gBACPkE,oBAAM,KAACxK;oBAAQyK,MAAM3K,iBAAiB2K,IAAI;oBAAEC,MAAM;;gBAClDC,UAAUhG;gBACViG,SAAS,IAAMpB,WAAW7C;YAC5B;YACA;gBACE4D,KAAK;gBACLjE,OAAO+D,YAAY,kBAAkB;gBACrCG,oBAAM,KAACxK;oBAAQyK,MAAM1K,eAAe0K,IAAI;oBAAEC,MAAM;;gBAChDC,UAAUhG,QAAS,CAAC0F,aAAa,CAAC1D,MAAMkE,MAAM;gBAC9CC,gBAAgB,CAACnE,MAAMkE,MAAM,GAAG,4CAA4CE;gBAC5EH,SAAS,IACP,KAAK/D,KACHvE,mBAAmBiC,UAAU8F,YAAY,OAAO1D,MAAMC,EAAE,GACxDyD,YAAY,oBAAoB,CAAC,IAAI,EAAEnE,MAAM,WAAW,EAAES,MAAML,KAAK,CAAC,CAAC,CAAC;YAE9E;YACA;gBACEiE,KAAK;gBACLjE,OAAOK,MAAMkE,MAAM,GAAG,eAAe;gBACrCL,oBACE,KAACxK;oBAAQyK,MAAM9D,MAAMkE,MAAM,GAAGjL,iBAAiB6K,IAAI,GAAG5K,cAAc4K,IAAI;oBAAEC,MAAM;;gBAElFC,UAAUhG;gBACViG,SAAS,IACP,KAAK/D,KACHtE,uBAAuBgC,UAAUoC,MAAMC,EAAE,EAAE,CAACD,MAAMkE,MAAM,GACxDlE,MAAMkE,MAAM,GAAG,CAAC,CAAC,EAAElE,MAAML,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,EAAEK,MAAML,KAAK,CAAC,WAAW,CAAC;YAElF;SACD;QACD,8EAA8E;QAC9E,IAAI,CAACI,WAAWC,QAAQ;YACtB2D,MAAMU,IAAI,CAAC;gBACTT,KAAK;gBACLjE,OAAO;gBACPkE,oBAAM,KAACxK;oBAAQyK,MAAM/K,iBAAiB+K,IAAI;oBAAEC,MAAM;;gBAClDO,aAAa;gBACbN,UAAUhG;gBACViG,SAAS,IAAMZ,WAAWrD;YAC5B;QACF;QACA,OAAO2D;IACT;IAEA,MAAMY,eAAe3F,WAAWlD,qBAAqBkB,YAAYgB,UAAUgB,SAASqB,EAAE,IAAI,EAAE;IAC5F,MAAMuE,UAAU,IAAKtF,CAAAA,OAAO1C,MAAM,GAAG,IAAI,CAAA,IAAM4C,CAAAA,SAAS5C,MAAM,GAAG,IAAI,CAAA;IACrE,MAAMiI,SAASnI,aAAakD,QAAQzC,GAAG,CAAC,CAAC,GAAGuF,KAAK,GAAKA,OAAOoC,OAAO,CAAC,MAAM,CAACC,SAC1EA,OAAOC,WAAW;IAGpB,qBACE,MAACrK;QAAMsK,SAAS;;0BACd,MAACtK;gBACCuK,WAAW;oBAAEC,IAAI;oBAAUC,IAAI;gBAAM;gBACrCH,SAAS;gBACTI,IAAI;oBAAEC,gBAAgB;oBAAiBC,YAAY;wBAAEH,IAAI;oBAAS;gBAAE;;kCAEpE,MAACzK;wBAAMsK,SAAS;;0CACd,KAAChK;gCAAWkG,SAAQ;0CAAa,GAAGnE,WAAW+C,KAAK,CAAC,OAAO,CAAC;;0CAC7D,KAAC9E;gCAAWkG,SAAQ;gCAAQqE,OAAM;0CAC/B,AAAC3F,CAAAA,cACE,CAAC,sBAAsB,EAAEH,KAAK,GAAG,EAAE1C,WAAW+C,KAAK,CAAC,wBAAwB,CAAC,GAC7E,wEACA,CAAC,WAAW,EAAEL,KAAK,EAAE,CAAC,GACtB,CAAC,qBAAqB,EAAEA,KAAK,GAAG,EAAE1C,WAAW+C,KAAK,CAAC,wBAAwB,CAAC,GAC3E/C,CAAAA,WAAWyI,UAAU,GAClB,gEACA,EAAC,IACL,yDAAwD,IAC1D,yEACA;;;;oBAGL/H,sBACC,MAAC/C;wBAAMuK,WAAU;wBAAMD,SAAS;wBAAGI,IAAI;4BAAEK,YAAY;wBAAE;;0CACrD,KAACxL;gCACCiK,MAAK;gCACLC,UAAUhG,QAAQJ,SAAS4C,MAAM,CAAChE,MAAM,GAAG;gCAC3CyH,SAAS,IAAM,KAAK/D,KAAKpE,mBAAmB8B,WAAW;0CAEtD;;0CAEH,KAAC9D;gCAAOiK,MAAK;gCAAQhD,SAAQ;gCAAWiD,UAAUhG;gCAAMiG,SAAStB;0CAC9D;;;yBAGH;;;YAEL,CAAC7E,QAAQ,qBACR,MAACtE;gBAAYuK,MAAK;gBAAQwB,cAAY,GAAG3I,WAAW+C,KAAK,CAAC,OAAO,CAAC;;kCAChE,KAACjF;kCACC,cAAA,MAACC;;8CACC,KAACF;oCAAUwK,IAAI;wCAAEO,OAAO;oCAAI;8CAAI;;8CAChC,KAAC/K;8CAAW;;gCACXyE,OAAO1C,MAAM,iBAAG,KAAC/B;8CAAW;qCAAuB;gCACnD2E,SAAS5C,MAAM,iBAAG,KAAC/B;8CAAW;qCAAuB;8CACtD,KAACA;8CAAW;;8CACZ,KAACA;oCAAUgL,OAAM;;;;;kCAGrB,MAACjL;;4BACEoD,SAAS4C,MAAM,CAAChE,MAAM,KAAK,kBAC1B,KAAC7B;0CACC,cAAA,KAACF;oCAAUiL,SAASlB;8CAClB,cAAA,KAAC3J;wCAAWkG,SAAQ;wCAAQqE,OAAM;kDAC/B,CAAC,kCAAkC,EAAE9F,KAAK,iBAAiB,CAAC;;;iCAIjE;4BACH1B,SAAS4C,MAAM,CAACzD,GAAG,CAAC,CAACiD,OAAO2F;oCAmEZ3F;qDAlEf,MAACrF;oCAECiL,KAAK;oCACL,4DAA4D;oCAC5D,6DAA6D;oCAC7DC,WAAW,CAAC7H;oCACZ8H,aAAa,CAACC;wCACZ9G,YAAY0G;wCACZI,MAAMC,YAAY,CAACC,aAAa,GAAG;oCACrC;oCACAC,YAAY,CAACH;wCACX,IAAI/G,aAAa,MAAM;wCACvB+G,MAAMI,cAAc;wCACpBJ,MAAMC,YAAY,CAACI,UAAU,GAAG;oCAClC;oCACAC,QAAQ,CAACN;wCACPA,MAAMI,cAAc;wCACpB,IAAInH,aAAa,QAAQA,aAAa2G,OAAO5C,KAAK/D,UAAU2G;wCAC5D1G,YAAY;oCACd;oCACAqH,WAAW,IAAMrH,YAAY;oCAC7BgG,IAAI,aACEjF,MAAMkE,MAAM,GAAG,CAAC,IAAI;wCAAEqC,SAAS;oCAAI,GACnCvH,aAAa2G,QAAQ;wCAAEa,SAAS;wCAAGC,cAAc;oCAAe,IAAI,CAAC;;sDAG3E,KAAChM;sDACC,cAAA,MAACF;gDAAMuK,WAAU;gDAAMD,SAAS;gDAAGI,IAAI;oDAAEE,YAAY;gDAAS;;kEAC5D,KAAC9L;wDACCyK,MAAM9K,gBAAgB8K,IAAI;wDAC1BC,MAAM;wDACN2C,OAAO;4DAAEC,QAAQ3I,OAAO,YAAY;wDAAO;wDAC3C4I,aAAW;;kEAEb,MAACvM;wDACC0J,MAAK;wDACLC,UAAU2B,UAAU,KAAK3H;wDACzBiG,SAAS,IAAMlB,KAAK4C,OAAOA,QAAQ;;0EAEnC,KAACtM;gEAAQyK,MAAMhL,WAAWgL,IAAI;gEAAEC,MAAM;;0EACtC,KAACzK;0EAAQ,CAAC,KAAK,EAAE0G,MAAML,KAAK,CAAC,GAAG,CAAC;;;;kEAEnC,MAACtF;wDACC0J,MAAK;wDACLC,UAAU2B,UAAU/H,SAAS4C,MAAM,CAAChE,MAAM,GAAG,KAAKwB;wDAClDiG,SAAS,IAAMlB,KAAK4C,OAAOA,QAAQ;;0EAEnC,KAACtM;gEAAQyK,MAAMjL,aAAaiL,IAAI;gEAAEC,MAAM;;0EACxC,KAACzK;0EAAQ,CAAC,KAAK,EAAE0G,MAAML,KAAK,CAAC,KAAK,CAAC;;;;;;;sDAIzC,KAAClF;sDACC,cAAA,MAACF;gDAAMuK,WAAU;gDAAMD,SAAS;gDAAGI,IAAI;oDAAEE,YAAY;gDAAS;;kEAC5D,KAACtK;wDAAWkG,SAAQ;kEAASf,MAAML,KAAK;;oDACvCI,WAAWC,uBACV,KAACjG;wDAAKgK,MAAK;wDAAQhD,SAAQ;wDAAWpB,OAAM;yDAC1C;;;;wCAGPT,OAAO1C,MAAM,iBACZ,KAAC/B;4CAAUwK,IAAI;gDAAE4B,UAAU;4CAAI;sDAC7B,cAAA,MAACjM;gDACCkM,MAAM;gDACN/C,MAAK;gDACLhD,SAAQ;gDACRf,KAAK,GAAEA,eAAAA,MAAMgD,KAAK,YAAXhD,eAAe;gDACtBgE,UAAUhG,QAAQ,CAACV;gDACnByJ,UAAU,CAAChB;wDAEI7G;oDADb,MAAM8D,QAAQgE,OAAOjB,MAAM/I,MAAM,CAACgD,KAAK,KAAK;oDAC5C,MAAMiH,QAAO/H,eAAAA,OAAOgI,IAAI,CAAC,CAACxE,QAAUA,MAAMzC,EAAE,KAAK+C,2BAApC9D,aAA4CS,KAAK;oDAC9D,MAAMS,OAAO6G,OAAO,CAAC,CAAC,EAAEjH,MAAML,KAAK,CAAC,KAAK,EAAEsH,MAAM,GAAG,CAAC,CAAC,EAAEjH,MAAML,KAAK,CAAC,cAAc,CAAC;oDACnF,IAAI,CAACR,aAAa;wDAChB,KAAKe,KAAKrE,sBAAsBe,YAAYgB,UAAUoC,MAAMC,EAAE,EAAE+C,QAAQ5C;wDACxE;oDACF;oDACA,mEAAmE;oDACnE,KAAKgB,KAAK;wDAAE8B,QAAQ;wDAASE,SAASpD,MAAMC,EAAE;wDAAE+C;oDAAM,GAAGmE,IAAI,CAAC,CAAClF;wDAC7D,IAAIA,QAAQ;4DACVtE,gBAAgB,GAAGyC,OAAO4B,MAAMC,SAAS,EAAE;gEAAElB,SAAS;gEAAWC,SAAS;4DAAM;wDAClF;oDACF;gDACF;gDACAoG,SAAS;gDACTC,WAAW;oDACTP,QAAQ;wDAAEQ,cAAc;wDAAM,cAAc,CAAC,SAAS,EAAEtH,MAAML,KAAK,EAAE;oDAAC;gDACxE;;kEAEA,KAACrF;wDAAS0F,OAAM;kEAAI;;oDACnBd,OAAOnC,GAAG,CAAC,CAACiG,sBACX,KAAC1I;4DAAwB0F,OAAOgD,MAAM/C,EAAE;sEACrC+C,MAAMrD,KAAK;2DADCqD,MAAM/C,EAAE;;;6CAM3B;wCACHb,SAAS5C,MAAM,GACd,mEAAmE;wCACnE,oCAAoC;sDACpC,KAAC/B;sDACC,cAAA,KAACI;gDAAWkG,SAAQ;gDAAQqE,OAAOpF,MAAMiD,OAAO,GAAG,iBAAiB;0DACjEjD,MAAMiD,OAAO,GAAGxH,qBAAqBmB,YAAYoD,MAAMiD,OAAO,IAAI;;6CAGrE;sDACJ,KAACxI;sDACC,cAAA,MAACF;gDAAMgN,UAAU;gDAACzC,WAAU;gDAAMD,SAAS;gDAAGI,IAAI;oDAAEuC,UAAU;oDAAQrC,YAAY;gDAAS;;oDACxFnF,MAAMkE,MAAM,GAAG,qBACd,KAACnK;wDAAKgK,MAAK;wDAAQhD,SAAQ;wDAAWqE,OAAM;wDAAUzF,OAAM;;oDAE7D/B,SAAS6C,cAAc,KAAKT,MAAMC,EAAE,iBACnC,KAAClG;wDAAKgK,MAAK;wDAAQhD,SAAQ;wDAAWqE,OAAM;wDAAUzF,OAAM;yDAC1D;oDACHK,MAAMkE,MAAM,IAAItG,SAAS6C,cAAc,KAAKT,MAAMC,EAAE,iBACnD,KAACpF;wDAAWkG,SAAQ;wDAAQqE,OAAM;kEAC/B;yDAED;;;;sDAGR,KAAC3K;4CAAUgL,OAAM;4CAAQR,IAAI;gDAAEO,OAAO;4CAAG;sDACvC,cAAA,KAACjM;gDAAeoG,OAAOK,MAAML,KAAK;gDAAEgE,OAAOF,WAAWzD;;;;mCAzHnDA,MAAMC,EAAE;;;;;;YAgItBnC,SAAS,CAACD,uBACT,KAAChD;gBAAWkG,SAAQ;gBAAUqE,OAAM;0BACjC,+EACC;iBAEF;0BAEJ,MAACpL;gBAAOyN,MAAMvJ,eAAe;gBAAMwJ,SAAS,IAAMvJ,cAAc;gBAAOiJ,SAAS;gBAACO,UAAS;;kCACxF,KAACvN;kCACE8D,CAAAA,8BAAAA,WAAY0E,IAAI,MAAK,WAAW,CAAC,QAAQ,EAAE1E,WAAW8B,KAAK,CAACL,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAED,OAAO;;kCAE1F,KAACxF;kCACC,cAAA,MAACK;4BAAMsK,SAAS;4BAAKI,IAAI;gCAAE2C,IAAI;4BAAE;;gCAC9B1J,CAAAA,8BAAAA,WAAY0E,IAAI,MAAK,YAAY,CAACnD,4BACjC,KAACtF;oCAAkB4G,SAAQ;8CACxB,CAAC,MAAM,EAAEzE,aAAakD,QAAQzC,GAAG,CAAC,CAAC,CAACsF,IAAI,GAAKA,MAAM,oBAAoB,CAAC,GACvE;qCAEF;8CACJ,KAACzH;oCACCmJ,MAAK;oCACLpE,OAAM;oCACNK,OAAO5B;oCACP2I,UAAU,CAAChB;wCACT1H,aAAa0H,MAAM/I,MAAM,CAACgD,KAAK;wCAC/BrB,aAAa;oCACf;oCACAkJ,WAAW,CAAC9B;wCACV,IAAIA,MAAMnC,GAAG,KAAK,SAAS,KAAKd;oCAClC;oCACA7B,OAAO6G,QAAQpJ;oCACfqJ,YAAYrJ,aAAa;oCACzB2I,WAAW;wCAAEW,WAAW;4CAAEC,WAAWxP;wCAAkB;oCAAE;oCACzDyP,SAAS;oCACTd,SAAS;;gCAEVlJ,CAAAA,8BAAAA,WAAY0E,IAAI,MAAK,SAAS1D,OAAO1C,MAAM,iBAC1C,MAAC5B;oCACCkM,MAAM;oCACN/C,MAAK;oCACLpE,OAAM;oCACNK,OAAO1B;oCACPyI,UAAU,CAAChB,QAAUxH,cAAcyI,OAAOjB,MAAM/I,MAAM,CAACgD,KAAK;oCAC5DoH,SAAS;oCACTC,WAAW;wCAAEP,QAAQ;4CAAEQ,cAAc;wCAAK;wCAAGa,YAAY;4CAAEC,QAAQ;wCAAK;oCAAE;;sDAE1E,KAAC9N;4CAAS0F,OAAM;sDAAI;;wCACnBd,OAAOnC,GAAG,CAAC,CAACiG,sBACX,KAAC1I;gDAAwB0F,OAAOgD,MAAM/C,EAAE;0DACrC+C,MAAMrD,KAAK;+CADCqD,MAAM/C,EAAE;;qCAKzB;gCACH/B,CAAAA,8BAAAA,WAAY0E,IAAI,MAAK,SAASxD,SAAS5C,MAAM,iBAC5C,KAAC5B;oCACCkM,MAAM;oCACN/C,MAAK;oCACLpE,OAAM;oCACNK,OAAOxB;oCACPuI,UAAU,CAAChB;wCACTtH,gBAAgBuI,OAAOjB,MAAM/I,MAAM,CAACgD,KAAK;wCACzCrB,aAAa;oCACf;oCACAyI,SAAS;8CAER/H,gBAAgBtC,GAAG,CAAC,CAACkG,wBACpB,KAAC3I;4CAAuB0F,OAAOiD;sDAC5BxH,qBAAqBmB,YAAYqG;2CADrBA;qCAKjB;;;;kCAGR,MAAChJ;;0CACC,KAACH;gCAAOmK,SAAS,IAAM9F,cAAc;0CAAQ;;0CAC7C,KAACrE;gCACCiH,SAAQ;gCACRiD,UAAUhG,QAAQ,CAACI,UAAU+E,IAAI;gCACjCc,SAAS,IAAM,KAAKnB;0CAEnB5E,CAAAA,8BAAAA,WAAY0E,IAAI,MAAK,WAAW,WAAW;;;;;;0BAKlD,MAAC5I;gBAAOyN,MAAM7I,aAAa;gBAAM8I,SAAS,IAAM7I,YAAY;gBAAOuI,SAAS;gBAACO,UAAS;;kCACpF,KAACvN;kCAAa,CAAC,QAAQ,UAAEwE,4BAAAA,SAAUe,KAAK,mBAAI,GAAG,EAAE,CAAC;;kCAClD,KAACzF;kCACC,cAAA,MAACK;4BAAMsK,SAAS;4BAAKI,IAAI;gCAAE2C,IAAI;4BAAE;;8CAC/B,KAACzN;oCAAkB4G,SAAQ;8CACxBtB,cACG,CAAC,4CAA4C,EAAEH,KAAK,SAAS,CAAC,GAC9D,CAAC,oCAAoC,EAAEmF,OAAO,cAAc,CAAC,GAC5DrF,CAAAA,SAAS5C,MAAM,GACZ,iEACA,oDAAmD,IACvD;;gCAELiD,cAAc,qBACb,MAAC7E;oCACCkM,MAAM;oCACN/C,MAAK;oCACLpE,OAAM;oCACNK,OAAOlB;oCACPiI,UAAU,CAAChB,QAAUhH,eAAeiI,OAAOjB,MAAM/I,MAAM,CAACgD,KAAK;oCAC7DoH,SAAS;oCACTC,WAAW;wCAAEP,QAAQ;4CAAEQ,cAAc;wCAAK;wCAAGa,YAAY;4CAAEC,QAAQ;wCAAK;oCAAE;;wCAGzEhJ,SAAS5C,MAAM,GAAG,qBAAO,KAAClC;4CAAS0F,OAAM;sDAAI;;wCAC7CuE,aAAaxH,GAAG,CAAC,CAACiD,sBACjB,KAAC1F;gDAAwB0F,OAAOA,MAAML,KAAK;0DACxCK,MAAML,KAAK;+CADCK,MAAMC,EAAE;;;;;;kCAQjC,MAAChG;;0CACC,KAACH;gCAAOmK,SAAS,IAAMpF,YAAY;0CAAQ;;0CAC3C,KAAC/E;gCACCiH,SAAQ;gCACRqE,OAAM;gCACNpB,UAAUhG,QAASoB,SAAS5C,MAAM,GAAG,KAAK,CAACsC;gCAC3CmF,SAAS,IAAM,KAAKX;0CAEnB;;;;;;;;AAMb;AACApG,mBAAmBmL,WAAW,GAAG;AAEjC,eAAenL,mBAAkB"}
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The digest beside this route says once a morning what is late and what
|
|
21
21
|
* is due today; nothing said "the 3:00 call is at 3:00". Every hour this
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* finds the orgs with an open task whose `remindAtMs` has come
|
|
23
|
+
* (`orgIdsWithDueReminders`), reads those orgs' tasks and tells each one's
|
|
24
|
+
* assignee ONCE — a console notification per task
|
|
24
25
|
* (`content.taskReminder`, opening the record the task is for) and one
|
|
25
26
|
* email per member per run listing everything of theirs that fell due.
|
|
26
27
|
* The words and the rule live in `crm-task-reminders.ts`; this file is the
|
|
@@ -48,8 +49,8 @@
|
|
|
48
49
|
* not come back every hour. A reminder is left unstamped only when the run
|
|
49
50
|
* never reached a verdict: the send-rate governor refused, or the org
|
|
50
51
|
* threw. Orgs are swept in id order behind a cursor, as the digest sweeps
|
|
51
|
-
* them, so the workflow's `done:false` loop carries
|
|
52
|
-
*
|
|
52
|
+
* them, so the workflow's `done:false` loop carries any number of them
|
|
53
|
+
* across invocations.
|
|
53
54
|
*
|
|
54
55
|
* The task query reads the NEWEST reminders that have come due, not the
|
|
55
56
|
* oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a
|
|
@@ -70,7 +71,8 @@ export declare const CRM_TASK_REMINDERS_JOB_ID = "crm-task-reminders";
|
|
|
70
71
|
* More than the digest's twenty-five: an org here costs one task query and,
|
|
71
72
|
* only when something fell due, the roster and the sends. The common hour
|
|
72
73
|
* finds nothing due in most orgs, and a chunk that reads a hundred empty
|
|
73
|
-
* queries finishes well inside `maxDuration = 60
|
|
74
|
+
* queries finishes well inside `maxDuration = 60`, which is what the whole
|
|
75
|
+
* platform costs on the night `orgIdsWithDueReminders` cannot answer.
|
|
74
76
|
*/
|
|
75
77
|
export declare const CRM_TASK_REMINDERS_ORG_CHUNK = 100;
|
|
76
78
|
/**
|
|
@@ -49,8 +49,9 @@ import { composeCrmTaskReminderBody, composeCrmTaskReminderEmailParts, composeCr
|
|
|
49
49
|
*
|
|
50
50
|
* The digest beside this route says once a morning what is late and what
|
|
51
51
|
* is due today; nothing said "the 3:00 call is at 3:00". Every hour this
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* finds the orgs with an open task whose `remindAtMs` has come
|
|
53
|
+
* (`orgIdsWithDueReminders`), reads those orgs' tasks and tells each one's
|
|
54
|
+
* assignee ONCE — a console notification per task
|
|
54
55
|
* (`content.taskReminder`, opening the record the task is for) and one
|
|
55
56
|
* email per member per run listing everything of theirs that fell due.
|
|
56
57
|
* The words and the rule live in `crm-task-reminders.ts`; this file is the
|
|
@@ -78,8 +79,8 @@ import { composeCrmTaskReminderBody, composeCrmTaskReminderEmailParts, composeCr
|
|
|
78
79
|
* not come back every hour. A reminder is left unstamped only when the run
|
|
79
80
|
* never reached a verdict: the send-rate governor refused, or the org
|
|
80
81
|
* threw. Orgs are swept in id order behind a cursor, as the digest sweeps
|
|
81
|
-
* them, so the workflow's `done:false` loop carries
|
|
82
|
-
*
|
|
82
|
+
* them, so the workflow's `done:false` loop carries any number of them
|
|
83
|
+
* across invocations.
|
|
83
84
|
*
|
|
84
85
|
* The task query reads the NEWEST reminders that have come due, not the
|
|
85
86
|
* oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a
|
|
@@ -98,13 +99,55 @@ import { composeCrmTaskReminderBody, composeCrmTaskReminderEmailParts, composeCr
|
|
|
98
99
|
* More than the digest's twenty-five: an org here costs one task query and,
|
|
99
100
|
* only when something fell due, the roster and the sends. The common hour
|
|
100
101
|
* finds nothing due in most orgs, and a chunk that reads a hundred empty
|
|
101
|
-
* queries finishes well inside `maxDuration = 60
|
|
102
|
+
* queries finishes well inside `maxDuration = 60`, which is what the whole
|
|
103
|
+
* platform costs on the night `orgIdsWithDueReminders` cannot answer.
|
|
102
104
|
*/ export const CRM_TASK_REMINDERS_ORG_CHUNK = 100;
|
|
105
|
+
/** Tasks per page of the platform-wide due query. */ const DUE_TASK_PAGE = 500;
|
|
103
106
|
/**
|
|
104
107
|
* The most members per org whose address is chased through the auth pools
|
|
105
108
|
* when their member document carries none — the digest's bound, for the
|
|
106
109
|
* digest's reason: work, not audience.
|
|
107
110
|
*/ export const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5;
|
|
111
|
+
/**
|
|
112
|
+
* THE ORGS WITH A REMINDER OWED, found from the tasks (AGL-4121).
|
|
113
|
+
*
|
|
114
|
+
* The sweep used to walk the orgs to find them: every org document read, and
|
|
115
|
+
* every entitled org's tasks asked, every hour, to learn that nothing had come
|
|
116
|
+
* due in almost all of them. Asked of the collection group, an hour with
|
|
117
|
+
* nothing due is one query and no org at all.
|
|
118
|
+
*
|
|
119
|
+
* Every page is read, not the newest, and that is what keeps this honest.
|
|
120
|
+
* `CRM_TASK_REMINDER_CEILING` is safe per org because the newest five hundred
|
|
121
|
+
* of ONE org begin with what fell due since the last run; across the platform
|
|
122
|
+
* one busy org's handled reminders could bury a quiet org's first one. This
|
|
123
|
+
* pays once for the same open tasks the org queries read, projected to what
|
|
124
|
+
* the rule needs, and `remindOrg` then decides from its own window exactly as
|
|
125
|
+
* it always has. An org found here with nothing inside that window reports
|
|
126
|
+
* `nothing-due`, as it did.
|
|
127
|
+
*
|
|
128
|
+
* Needs the COLLECTION_GROUP `(status, remindAtMs desc)` index, and the
|
|
129
|
+
* `crmTasks` entry for it is deployed after the code that first asks: see the
|
|
130
|
+
* caller for what happens until then.
|
|
131
|
+
*/ async function orgIdsWithDueReminders(firestore, nowMs) {
|
|
132
|
+
const orgIds = new Set();
|
|
133
|
+
let after;
|
|
134
|
+
for(;;){
|
|
135
|
+
let query = firestore.collectionGroup(CRM_COLLECTIONS.tasks).where('status', '==', 'open').where('remindAtMs', '<=', nowMs).orderBy('remindAtMs', 'desc').select('status', 'remindAtMs', 'reminderSentAtMs').limit(DUE_TASK_PAGE);
|
|
136
|
+
if (after) query = query.startAfter(after);
|
|
137
|
+
const page = await query.get();
|
|
138
|
+
for (const doc of page.docs){
|
|
139
|
+
const org = doc.ref.parent.parent;
|
|
140
|
+
if ((org == null ? void 0 : org.parent.id) !== 'orgs') continue;
|
|
141
|
+
if (crmTaskReminderDue(doc.data(), nowMs)) {
|
|
142
|
+
orgIds.add(org.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (page.docs.length < DUE_TASK_PAGE) return [
|
|
146
|
+
...orgIds
|
|
147
|
+
].sort();
|
|
148
|
+
after = page.docs[page.docs.length - 1];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
108
151
|
async function remindOrg(ctx, orgDoc) {
|
|
109
152
|
var _orgDoc_data, _org_slug, _org_name;
|
|
110
153
|
const { firestore, nowMs, timeZone, dryRun } = ctx;
|
|
@@ -415,8 +458,6 @@ export async function crmTaskRemindersRoute(request) {
|
|
|
415
458
|
// nothing due still ran.
|
|
416
459
|
if (method === 'POST') await recordCronBeat(CRM_TASK_REMINDERS_JOB_ID);
|
|
417
460
|
try {
|
|
418
|
-
var _ref;
|
|
419
|
-
var _orgDocs_;
|
|
420
461
|
const firestore = firebaseAdmin.app().firestore();
|
|
421
462
|
const nowMs = Date.now();
|
|
422
463
|
const ctx = {
|
|
@@ -428,17 +469,43 @@ export async function crmTaskRemindersRoute(request) {
|
|
|
428
469
|
origin: consoleOrigin()
|
|
429
470
|
};
|
|
430
471
|
// The resumable sweep, in the shape the digest reads orgs (AGL-2220):
|
|
431
|
-
// ordered by id,
|
|
432
|
-
// validated as an org id before it becomes a reference.
|
|
472
|
+
// ordered by id, one more than a chunk to learn whether there is more, the
|
|
473
|
+
// cursor validated as an org id before it becomes a reference.
|
|
433
474
|
const requestedLimit = Number(body == null ? void 0 : body.limit);
|
|
434
475
|
const pageSize = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.min(Math.floor(requestedLimit), CRM_TASK_REMINDERS_ORG_CHUNK) : CRM_TASK_REMINDERS_ORG_CHUNK;
|
|
435
476
|
const rawCursor = body == null ? void 0 : body.cursor;
|
|
436
477
|
const cursor = typeof rawCursor === 'string' && rawCursor.length > 0 && !rawCursor.includes('/') ? rawCursor : null;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
478
|
+
/*
|
|
479
|
+
* Only the orgs with something owed. A due query that fails is not an hour
|
|
480
|
+
* with nothing due, so it never answers for one: it falls back to walking
|
|
481
|
+
* every org, which needs no collection-group index and is what makes this
|
|
482
|
+
* safe to ship ahead of the index it uses. Composite indexes deploy in the
|
|
483
|
+
* promotion's tail, and a reminder must not wait on the order of that tail.
|
|
484
|
+
*/ const withDue = await orgIdsWithDueReminders(firestore, nowMs).catch((error)=>{
|
|
485
|
+
console.warn('[crm] task reminders: the due query failed, sweeping every org', error);
|
|
486
|
+
return null;
|
|
487
|
+
});
|
|
488
|
+
let orgDocs;
|
|
489
|
+
let hasMore;
|
|
490
|
+
/** Where the next chunk starts, whichever way this one was chosen. */ let lastOrgId;
|
|
491
|
+
if (withDue) {
|
|
492
|
+
var _chunk_;
|
|
493
|
+
const ahead = cursor ? withDue.filter((orgId)=>orgId > cursor) : withDue;
|
|
494
|
+
const chunk = ahead.slice(0, pageSize);
|
|
495
|
+
hasMore = ahead.length > pageSize;
|
|
496
|
+
lastOrgId = (_chunk_ = chunk[chunk.length - 1]) != null ? _chunk_ : null;
|
|
497
|
+
// A task can outlive its org by a moment; there is nobody to tell.
|
|
498
|
+
orgDocs = chunk.length ? (await firestore.getAll(...chunk.map((orgId)=>firestore.collection('orgs').doc(orgId)))).filter((doc)=>doc.exists) : [];
|
|
499
|
+
} else {
|
|
500
|
+
var _ref;
|
|
501
|
+
var _orgDocs_;
|
|
502
|
+
let orgQuery = firestore.collection('orgs').orderBy(firebaseAdmin.firestore.FieldPath.documentId()).limit(pageSize + 1);
|
|
503
|
+
if (cursor) orgQuery = orgQuery.startAfter(firestore.collection('orgs').doc(cursor));
|
|
504
|
+
const page = await orgQuery.get();
|
|
505
|
+
hasMore = page.docs.length > pageSize;
|
|
506
|
+
orgDocs = hasMore ? page.docs.slice(0, pageSize) : page.docs;
|
|
507
|
+
lastOrgId = (_ref = (_orgDocs_ = orgDocs[orgDocs.length - 1]) == null ? void 0 : _orgDocs_.id) != null ? _ref : null;
|
|
508
|
+
}
|
|
442
509
|
const orgs = {};
|
|
443
510
|
const failures = {};
|
|
444
511
|
let due = 0;
|
|
@@ -481,7 +548,7 @@ export async function crmTaskRemindersRoute(request) {
|
|
|
481
548
|
} : {}, {
|
|
482
549
|
// A deferred run reports `done: true` — see the module comment —
|
|
483
550
|
// so the workflow's loop does not re-POST into a full window.
|
|
484
|
-
nextCursor: deferred || !hasMore ? null :
|
|
551
|
+
nextCursor: deferred || !hasMore ? null : lastOrgId,
|
|
485
552
|
done: deferred || !hasMore
|
|
486
553
|
}), // 207: this chunk finished and some org in it needs a person, which is
|
|
487
554
|
// what fails the workflow (its one alerting channel).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/task-reminders-route.ts"],"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\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildRoute,\n CRM_COLLECTIONS,\n isReleaseFlagOnForOrg,\n notificationMuted,\n parseOrgReleaseFlagOverrides,\n pluginRequestFromWeb,\n resolveBrandingProfile,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n Route,\n} from '@aglyn/aglyn/server'\nimport {\n crmDigestEntitled,\n} from '../model/crm-digest'\nimport { rateLimitedRetryAtMs, sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport {\n findUserByUidAcrossPools,\n firebaseAdmin,\n getServerReleaseFlagValues,\n listOrgMembers,\n memberHasOrgPermission,\n meterPlatformEmail,\n notifyUsers,\n} from '@aglyn/tenant-data-admin'\n// From the LEAF, not the barrel (AGL-2407): the cron specs mock\n// `@aglyn/tenant-data-admin` wholesale, and a suppression gate imported\n// through the barrel would be silently replaced by whatever the factory\n// lists. Same reasoning as the digest beside this route.\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type LoadedSystemEmail,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { isCronAuthorized, isCronDryRun } from '@aglyn/tenant-data-admin/server/cron-auth'\nimport { writeCronBeat } from '@aglyn/aglyn/app-utils/health-report'\nimport { brandSupportLine } from '@aglyn/aglyn/app-utils/brand-support-line'\nimport { platformConsoleOrigin as consoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { digestTimeZone } from './daily-digest-route'\nimport {\n composeCrmTaskReminderBody,\n composeCrmTaskReminderEmailParts,\n composeCrmTaskReminderEmailText,\n crmTaskReminderMergeValues,\n composeCrmTaskReminderSubject,\n CRM_TASK_REMINDER_CEILING,\n type CrmReminderTask,\n crmTaskReminderDue,\n crmTaskReminderLink,\n} from '../model/crm-task-reminders'\n\n/**\n * The mark a scheduled run leaves for `/api/health/crons` (AGL-1955), on the\n * INVOCATION and before the work — the console's rule for every cron. Never\n * throws: reaching the admin app is inside the try, because a monitor that\n * can take down the job it describes is worse than the failure it watches.\n */\nasync function recordCronBeat(jobId: string): Promise<void> {\n try {\n await writeCronBeat(firebaseAdmin.app().firestore(), jobId)\n } catch {\n // Deliberately swallowed: a job that stops stamping reds its own row.\n }\n}\n\n// lockdown-423: exempt — server-internal cron (x-cron-secret), no user caller; it reads an org's CRM to remind its members and writes nothing a locked org could lose.\n\n/**\n * A task's reminder at its own time (AGL-2659): `POST /api/crm/task-reminders`.\n *\n * The digest beside this route says once a morning what is late and what\n * is due today; nothing said \"the 3:00 call is at 3:00\". Every hour this\n * reads, per org, the open tasks whose `remindAtMs` has come and tells\n * each one's assignee ONCE — a console notification per task\n * (`content.taskReminder`, opening the record the task is for) and one\n * email per member per run listing everything of theirs that fell due.\n * The words and the rule live in `crm-task-reminders.ts`; this file is the\n * reads, the sends and the sweep, in the digest's shape wherever the two\n * do the same thing.\n *\n * ## Who is told, and who is not\n *\n * The task's ASSIGNEE, and nobody else — an unassigned task is a deliberate\n * choice in the drawer, as the digest reads it. Only a member who may open\n * the CRM (`data.manage`, resolved through their custom role), only in an\n * org whose plan carries the suite and whose `release_crm` flag is\n * on: the plan gate is the digest's own (`crmDigestEntitled`), so a plan\n * that gets no digest gets no reminders either. The recipient's\n * operational-category mute (`notificationPrefs.content`) governs the WHOLE\n * reminder, mail included — unlike the digest, which has a switch of its\n * own for the schedule it keeps; a reminder is one message about one task.\n *\n * ## Once, and resumable\n *\n * `reminderSentAtMs` on the task is the idempotence: stamped in one batch\n * with the member's sends, so a rerun over the same hour reads the mark and\n * sends nothing, and stamped on every reminder the run HANDLED — sent, or\n * found nobody to send it to — so a reminder nobody can be told about does\n * not come back every hour. A reminder is left unstamped only when the run\n * never reached a verdict: the send-rate governor refused, or the org\n * threw. Orgs are swept in id order behind a cursor, as the digest sweeps\n * them, so the workflow's `done:false` loop carries a platform of any\n * size across invocations.\n *\n * The task query reads the NEWEST reminders that have come due, not the\n * oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a\n * bounded page safe against a window that only ever grows.\n *\n * The email is a `bulk` send under the platform send-rate governor\n * (AGL-2409), metered as PLATFORM mail, for the digest's reasons: a cron\n * fan-out on the shared sending domain, and the platform reminding a\n * member is not the org's sending. A refusal STOPS the run and reports\n * `done: true`; the refused member was never stamped, and the next hour's\n * run finds their reminders at the top of the window again.\n */\n\n/** The beat `/api/health/crons` reads — the `SCHEDULED_JOBS` row's id. */\nexport const CRM_TASK_REMINDERS_JOB_ID = 'crm-task-reminders'\n\n/**\n * Orgs per invocation.\n *\n * More than the digest's twenty-five: an org here costs one task query and,\n * only when something fell due, the roster and the sends. The common hour\n * finds nothing due in most orgs, and a chunk that reads a hundred empty\n * queries finishes well inside `maxDuration = 60`.\n */\nexport const CRM_TASK_REMINDERS_ORG_CHUNK = 100\n\n/**\n * The most members per org whose address is chased through the auth pools\n * when their member document carries none — the digest's bound, for the\n * digest's reason: work, not audience.\n */\nexport const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Snapshot = FirebaseFirestore.QueryDocumentSnapshot\n\n/** What one member's row in the response says. */\ninterface MemberReport {\n tasks: number\n skipped?: string\n notified?: number\n emailed?: boolean\n emailReason?: string\n deferred?: boolean\n}\n\ninterface OrgReport {\n skipped?: string\n /** Reminders that had come due and were not yet handled. */\n due: number\n /** Reminders this run stamped as handled. */\n handled: number\n /** Console notifications sent. */\n notified: number\n /** Emails sent — one per member per run. */\n emailed: number\n members?: Record<string, MemberReport>\n tasksTruncated?: boolean\n /** The send-rate governor refused; the sweep stops here. */\n deferred?: boolean\n}\n\ninterface SweepContext {\n firestore: Firestore\n nowMs: number\n timeZone: string\n dryRun: boolean\n flagValues: Awaited<ReturnType<typeof getServerReleaseFlagValues>>\n origin: string\n}\n\n/** A due reminder with the document it came from, so the stamp has a target. */\ninterface DueTask extends CrmReminderTask {\n ref: FirebaseFirestore.DocumentReference\n}\n\nasync function remindOrg(ctx: SweepContext, orgDoc: Snapshot): Promise<OrgReport> {\n const { firestore, nowMs, timeZone, dryRun } = ctx\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Partial<AglynOrgBilling> & Record<string, unknown>\n const quiet: OrgReport = { due: 0, handled: 0, notified: 0, emailed: 0 }\n\n if (!crmDigestEntitled(resolveOrgEntitlements(org).features)) {\n return { ...quiet, skipped: 'not-entitled' }\n }\n const flagOn = isReleaseFlagOnForOrg(\n 'release_crm',\n ctx.flagValues['release_crm'],\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org),\n )\n if (!flagOn) return { ...quiet, skipped: 'release-flag' }\n\n /*\n * Every open task whose reminder has come, newest first, on\n * `(status, remindAtMs desc)`. The \"not yet handled\" half is a fact about\n * a field's absence, which a query cannot select on, so it is asked of\n * the page in memory — and the page being the NEWEST five hundred is what\n * keeps the handled ones from crowding out the next one due.\n */\n const page = await orgDoc.ref\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .limit(CRM_TASK_REMINDER_CEILING)\n .get()\n const due: DueTask[] = []\n for (const doc of page.docs) {\n const data = doc.data() as Record<string, unknown>\n if (!crmTaskReminderDue(data as Parameters<typeof crmTaskReminderDue>[0], nowMs)) continue\n due.push({\n ref: doc.ref,\n id: doc.id,\n title: String(data['title'] ?? ''),\n kind: (data['kind'] as CrmReminderTask['kind']) ?? 'todo',\n dueAtMs: typeof data['dueAtMs'] === 'number' ? (data['dueAtMs'] as number) : null,\n remindAtMs: Number(data['remindAtMs']),\n assigneeUid: String(data['assigneeUid'] ?? ''),\n hostId: String(data['hostId'] ?? ''),\n contactId: (data['contactId'] as string) || undefined,\n companyId: (data['companyId'] as string) || undefined,\n dealId: (data['dealId'] as string) || undefined,\n })\n }\n const tasksTruncated = page.docs.length >= CRM_TASK_REMINDER_CEILING\n if (!due.length) {\n return { ...quiet, skipped: 'nothing-due', ...(tasksTruncated ? { tasksTruncated } : {}) }\n }\n\n const report: OrgReport = { due: due.length, handled: 0, notified: 0, emailed: 0, members: {} }\n if (tasksTruncated) report.tasksTruncated = true\n const rows = report.members as Record<string, MemberReport>\n\n /**\n * Stamp a set of reminders as handled, in one batch, unless this is a\n * plan. Everything a member's verdict covers lands together, which is\n * what makes a rerun read the same answer for all of them.\n */\n const settle = async (tasks: DueTask[]): Promise<void> => {\n report.handled += tasks.length\n if (dryRun) return\n const batch = firestore.batch()\n for (const task of tasks) batch.update(task.ref, { reminderSentAtMs: nowMs })\n await batch.commit()\n }\n\n const byAssignee = new Map<string, DueTask[]>()\n const unassigned: DueTask[] = []\n for (const task of due) {\n if (!task.assigneeUid) {\n unassigned.push(task)\n continue\n }\n const mine = byAssignee.get(task.assigneeUid) ?? []\n mine.push(task)\n byAssignee.set(task.assigneeUid, mine)\n }\n // A reminder for nobody is handled by saying so: it would otherwise sit\n // at the top of the window every hour for as long as the task is open.\n if (unassigned.length) {\n rows[''] = { tasks: unassigned.length, skipped: 'unassigned' }\n await settle(unassigned)\n }\n if (!byAssignee.size) return report\n\n const listed = (await listOrgMembers(orgId)).filter(\n (member) => member.orgSuspended !== true,\n )\n // A locked or disabled account is not told (AGL-3418): it cannot sign in\n // to act on it.\n const reachable = new Set(\n await withoutMailWithheldAccounts(listed.map((member) => member.$id)),\n )\n const members = listed.filter((member) => reachable.has(member.$id))\n const memberById = new Map(members.map((member) => [member.$id, member]))\n // Sorted so the order members are told in — and the member a refusal\n // lands on — is the same on every run.\n const uids = [...byAssignee.keys()].sort()\n const userDocs = await firestore.getAll(\n ...uids.map((uid) => firestore.collection('users').doc(uid)),\n )\n const userById = new Map(userDocs.map((doc) => [doc.id, doc]))\n\n // The sites the due tasks name, for the absolute links the mail carries.\n const hostIds = [...new Set(due.map((task) => task.hostId).filter(Boolean))]\n const hostDocs = hostIds.length\n ? await firestore.getAll(...hostIds.map((hostId) => firestore.collection('hosts').doc(hostId)))\n : []\n const hostById = new Map(hostDocs.map((doc) => [doc.id, doc]))\n\n const branding = resolveBrandingProfile(org)\n const orgSlug = String(org['slug'] ?? '')\n const workspaceName = String(org['name'] ?? '').trim() || orgSlug\n const orgHub = orgSlug ? `${ctx.origin}${buildRoute(Route.ORG_HOME, { orgSlug })}/crm` : null\n const hubUrl = (hostId: string): string | null => {\n if (!hostId) return orgHub\n const subdomain = String(hostById.get(hostId)?.get('subdomain') ?? '')\n if (!orgSlug || !subdomain) return null\n return `${ctx.origin}${buildRoute(Route.HOST_DASHBOARD, { orgSlug, host: subdomain })}/crm`\n }\n // The record the task is for, on its own hub — the notification's link\n // past the `/{hostId}/crm` or `/org/crm` prefix the console rewrites.\n const taskUrl = (task: CrmReminderTask): string => {\n const hub = hubUrl(task.hostId)\n if (!hub) return ctx.origin\n const link = crmTaskReminderLink(task.hostId || null, task)\n return `${hub}${link.slice(link.indexOf('/crm') + '/crm'.length)}`\n }\n const settingsUrl = `${ctx.origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const supportLine = brandSupportLine(branding)\n const brand = systemEmailBrand(org)\n let reminderEmail: Promise<LoadedSystemEmail | null> | undefined\n let poolLookups = 0\n\n for (const uid of uids) {\n const tasks = byAssignee.get(uid) as DueTask[]\n const row: MemberReport = { tasks: tasks.length }\n rows[uid] = row\n const member = memberById.get(uid)\n if (!member) {\n row.skipped = 'not-a-member'\n await settle(tasks)\n continue\n }\n if (!(await memberHasOrgPermission(orgId, member as AglynOrgMember, 'data.manage'))) {\n row.skipped = 'no-data-manage'\n await settle(tasks)\n continue\n }\n const userDoc = userById.get(uid)\n if (notificationMuted(userDoc?.get('notificationPrefs'), 'content.taskReminder')) {\n row.skipped = 'muted'\n await settle(tasks)\n continue\n }\n if (dryRun) {\n row.notified = tasks.length\n row.emailed = true\n await settle(tasks)\n continue\n }\n\n /*\n * THE EMAIL FIRST, for the digest's reason: a refusal from the\n * send-rate governor stops the run with nothing else done for this\n * member — no notification to double next hour, no mark to skip them\n * on — so the order here is the idempotence, not a preference.\n */\n let emailed = false\n let emailReason: string | undefined\n let address = String(member.email ?? '').trim().toLowerCase()\n if (!address.includes('@') && poolLookups < CRM_TASK_REMINDERS_MAX_UID_LOOKUPS) {\n poolLookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n const recipients = address.includes('@')\n ? await filterSuppressedEmails([address], firestore)\n : []\n if (!recipients.length) {\n emailReason = address.includes('@') ? 'suppressed' : 'no-recipient'\n } else {\n const reminderInput = {\n tasks,\n timeZone,\n productName: branding.productName,\n workspaceName,\n taskUrl,\n settingsUrl,\n supportLine,\n }\n const subject = composeCrmTaskReminderSubject(tasks)\n // The `crm-task-reminder` system email (AGL-3367), in the org's brand,\n // read once per org and rendered per member.\n reminderEmail ??= loadSystemEmail('crm-task-reminder').catch(() => null)\n const loaded = await reminderEmail\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...brand.merge,\n 'reminder.subject': subject,\n 'reminder.body': composeCrmTaskReminderEmailText({ ...reminderInput, omitClosing: true }),\n ...crmTaskReminderMergeValues(composeCrmTaskReminderEmailParts(reminderInput)),\n settingsUrl,\n },\n brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject,\n text: composeCrmTaskReminderEmailText(reminderInput),\n }),\n fromName: branding.fromName,\n context: 'crm-task-reminder',\n priority: 'bulk',\n })\n if (result.sent) {\n emailed = true\n await meterPlatformEmail().catch(() => undefined)\n } else if (rateLimitedRetryAtMs(result) !== null) {\n row.deferred = true\n report.deferred = true\n return report\n } else {\n emailReason = (result as { reason?: string }).reason ?? 'failed'\n }\n }\n\n // One notification per task: each opens the record it is for.\n for (const task of tasks) {\n await notifyUsers([uid], {\n type: 'content.taskReminder',\n title: 'Task reminder',\n body: composeCrmTaskReminderBody(task, timeZone, workspaceName),\n link: crmTaskReminderLink(task.hostId || null, task),\n orgId,\n ...(task.hostId ? { hostId: task.hostId } : {}),\n })\n }\n await settle(tasks)\n row.notified = tasks.length\n row.emailed = emailed\n if (emailReason) row.emailReason = emailReason\n report.notified += tasks.length\n if (emailed) report.emailed += 1\n }\n return report\n}\n\nexport async function crmTaskRemindersRoute(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST' && method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n if (!process.env.CRON_SECRET) {\n return Response.json(\n { error: 'Task reminders are not configured (CRON_SECRET).' },\n { status: 501 },\n )\n }\n if (!isCronAuthorized(headers)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n // A GET reports the plan and sends nothing; a bodyless POST — what the\n // scheduled workflow sends — is a real run. `isCronDryRun` keys the\n // default on the METHOD for exactly that reason.\n const dryRun = isCronDryRun({ method, body, query })\n // Stamped on the invocation, not on the work (AGL-1955): an hour with\n // nothing due still ran.\n if (method === 'POST') await recordCronBeat(CRM_TASK_REMINDERS_JOB_ID)\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const nowMs = Date.now()\n const ctx: SweepContext = {\n firestore,\n nowMs,\n timeZone: digestTimeZone(),\n dryRun,\n flagValues: await getServerReleaseFlagValues(),\n origin: consoleOrigin(),\n }\n\n // The resumable sweep, in the shape the digest reads orgs (AGL-2220):\n // ordered by id, `limit + 1` to learn whether there is more, the cursor\n // validated as an org id before it becomes a reference.\n const requestedLimit = Number((body as { limit?: unknown } | undefined)?.limit)\n const pageSize =\n Number.isFinite(requestedLimit) && requestedLimit > 0\n ? Math.min(Math.floor(requestedLimit), CRM_TASK_REMINDERS_ORG_CHUNK)\n : CRM_TASK_REMINDERS_ORG_CHUNK\n const rawCursor = (body as { cursor?: unknown } | undefined)?.cursor\n const cursor =\n typeof rawCursor === 'string' && rawCursor.length > 0 && !rawCursor.includes('/')\n ? rawCursor\n : null\n let orgQuery = firestore\n .collection('orgs')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(pageSize + 1)\n if (cursor) orgQuery = orgQuery.startAfter(firestore.collection('orgs').doc(cursor))\n const page = await orgQuery.get()\n const hasMore = page.docs.length > pageSize\n const orgDocs = hasMore ? page.docs.slice(0, pageSize) : page.docs\n\n const orgs: Record<string, OrgReport> = {}\n const failures: Record<string, string> = {}\n let due = 0\n let handled = 0\n let notified = 0\n let emailed = 0\n let deferred = false\n for (const orgDoc of orgDocs) {\n try {\n const report = await remindOrg(ctx, orgDoc)\n orgs[orgDoc.id] = report\n due += report.due\n handled += report.handled\n notified += report.notified\n emailed += report.emailed\n if (report.deferred) {\n deferred = true\n break\n }\n } catch (error) {\n console.error('[crm] task reminders failed for org', orgDoc.id, error)\n failures[orgDoc.id] = (error as Error)?.message ?? 'unknown'\n }\n }\n const failed = Object.keys(failures)\n return Response.json(\n {\n timeZone: ctx.timeZone,\n dryRun,\n swept: orgDocs.length,\n due,\n handled,\n notified,\n emailed,\n orgs,\n ...(deferred ? { deferred: true } : {}),\n ...(failed.length ? { failures } : {}),\n // A deferred run reports `done: true` — see the module comment —\n // so the workflow's loop does not re-POST into a full window.\n nextCursor: deferred || !hasMore ? null : (orgDocs[orgDocs.length - 1]?.id ?? null),\n done: deferred || !hasMore,\n },\n // 207: this chunk finished and some org in it needs a person, which is\n // what fails the workflow (its one alerting channel).\n { status: failed.length ? 207 : 200 },\n )\n } catch (error) {\n console.error('[crm] task reminders run failed', error)\n return Response.json({ error: 'Task reminders run failed' }, { status: 500 })\n }\n}\n"],"names":["buildRoute","CRM_COLLECTIONS","isReleaseFlagOnForOrg","notificationMuted","parseOrgReleaseFlagOverrides","pluginRequestFromWeb","resolveBrandingProfile","resolveEffectivePlan","resolveOrgEntitlements","Route","crmDigestEntitled","rateLimitedRetryAtMs","sendEmail","withoutMailWithheldAccounts","findUserByUidAcrossPools","firebaseAdmin","getServerReleaseFlagValues","listOrgMembers","memberHasOrgPermission","meterPlatformEmail","notifyUsers","filterSuppressedEmails","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","isCronAuthorized","isCronDryRun","writeCronBeat","brandSupportLine","platformConsoleOrigin","consoleOrigin","digestTimeZone","composeCrmTaskReminderBody","composeCrmTaskReminderEmailParts","composeCrmTaskReminderEmailText","crmTaskReminderMergeValues","composeCrmTaskReminderSubject","CRM_TASK_REMINDER_CEILING","crmTaskReminderDue","crmTaskReminderLink","recordCronBeat","jobId","app","firestore","CRM_TASK_REMINDERS_JOB_ID","CRM_TASK_REMINDERS_ORG_CHUNK","CRM_TASK_REMINDERS_MAX_UID_LOOKUPS","remindOrg","ctx","orgDoc","org","nowMs","timeZone","dryRun","orgId","id","data","quiet","due","handled","notified","emailed","features","skipped","flagOn","flagValues","page","ref","collection","tasks","where","orderBy","limit","get","doc","docs","push","title","String","kind","dueAtMs","remindAtMs","Number","assigneeUid","hostId","contactId","undefined","companyId","dealId","tasksTruncated","length","report","members","rows","settle","batch","task","update","reminderSentAtMs","commit","byAssignee","Map","unassigned","mine","set","size","listed","filter","member","orgSuspended","reachable","Set","map","$id","has","memberById","uids","keys","sort","userDocs","getAll","uid","userById","hostIds","Boolean","hostDocs","hostById","branding","orgSlug","workspaceName","trim","orgHub","origin","ORG_HOME","hubUrl","subdomain","HOST_DASHBOARD","host","taskUrl","hub","link","slice","indexOf","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","supportLine","brand","reminderEmail","poolLookups","row","userDoc","emailReason","address","email","toLowerCase","includes","pooled","catch","record","recipients","reminderInput","productName","subject","loaded","rendered","merge","omitClosing","options","result","to","text","fromName","context","priority","sent","deferred","reason","type","body","crmTaskRemindersRoute","request","method","headers","rawHeaders","query","Response","json","error","status","process","env","CRON_SECRET","orgDocs","Date","now","requestedLimit","pageSize","isFinite","Math","min","floor","rawCursor","cursor","orgQuery","FieldPath","documentId","startAfter","hasMore","orgs","failures","console","message","failed","Object","swept","nextCursor","done"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/task-reminders-route.ts"],"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\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildRoute,\n CRM_COLLECTIONS,\n isReleaseFlagOnForOrg,\n notificationMuted,\n parseOrgReleaseFlagOverrides,\n pluginRequestFromWeb,\n resolveBrandingProfile,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n Route,\n} from '@aglyn/aglyn/server'\nimport {\n crmDigestEntitled,\n} from '../model/crm-digest'\nimport { rateLimitedRetryAtMs, sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport {\n findUserByUidAcrossPools,\n firebaseAdmin,\n getServerReleaseFlagValues,\n listOrgMembers,\n memberHasOrgPermission,\n meterPlatformEmail,\n notifyUsers,\n} from '@aglyn/tenant-data-admin'\n// From the LEAF, not the barrel (AGL-2407): the cron specs mock\n// `@aglyn/tenant-data-admin` wholesale, and a suppression gate imported\n// through the barrel would be silently replaced by whatever the factory\n// lists. Same reasoning as the digest beside this route.\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type LoadedSystemEmail,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { isCronAuthorized, isCronDryRun } from '@aglyn/tenant-data-admin/server/cron-auth'\nimport { writeCronBeat } from '@aglyn/aglyn/app-utils/health-report'\nimport { brandSupportLine } from '@aglyn/aglyn/app-utils/brand-support-line'\nimport { platformConsoleOrigin as consoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { digestTimeZone } from './daily-digest-route'\nimport {\n composeCrmTaskReminderBody,\n composeCrmTaskReminderEmailParts,\n composeCrmTaskReminderEmailText,\n crmTaskReminderMergeValues,\n composeCrmTaskReminderSubject,\n CRM_TASK_REMINDER_CEILING,\n type CrmReminderTask,\n crmTaskReminderDue,\n crmTaskReminderLink,\n} from '../model/crm-task-reminders'\n\n/**\n * The mark a scheduled run leaves for `/api/health/crons` (AGL-1955), on the\n * INVOCATION and before the work — the console's rule for every cron. Never\n * throws: reaching the admin app is inside the try, because a monitor that\n * can take down the job it describes is worse than the failure it watches.\n */\nasync function recordCronBeat(jobId: string): Promise<void> {\n try {\n await writeCronBeat(firebaseAdmin.app().firestore(), jobId)\n } catch {\n // Deliberately swallowed: a job that stops stamping reds its own row.\n }\n}\n\n// lockdown-423: exempt — server-internal cron (x-cron-secret), no user caller; it reads an org's CRM to remind its members and writes nothing a locked org could lose.\n\n/**\n * A task's reminder at its own time (AGL-2659): `POST /api/crm/task-reminders`.\n *\n * The digest beside this route says once a morning what is late and what\n * is due today; nothing said \"the 3:00 call is at 3:00\". Every hour this\n * finds the orgs with an open task whose `remindAtMs` has come\n * (`orgIdsWithDueReminders`), reads those orgs' tasks and tells each one's\n * assignee ONCE — a console notification per task\n * (`content.taskReminder`, opening the record the task is for) and one\n * email per member per run listing everything of theirs that fell due.\n * The words and the rule live in `crm-task-reminders.ts`; this file is the\n * reads, the sends and the sweep, in the digest's shape wherever the two\n * do the same thing.\n *\n * ## Who is told, and who is not\n *\n * The task's ASSIGNEE, and nobody else — an unassigned task is a deliberate\n * choice in the drawer, as the digest reads it. Only a member who may open\n * the CRM (`data.manage`, resolved through their custom role), only in an\n * org whose plan carries the suite and whose `release_crm` flag is\n * on: the plan gate is the digest's own (`crmDigestEntitled`), so a plan\n * that gets no digest gets no reminders either. The recipient's\n * operational-category mute (`notificationPrefs.content`) governs the WHOLE\n * reminder, mail included — unlike the digest, which has a switch of its\n * own for the schedule it keeps; a reminder is one message about one task.\n *\n * ## Once, and resumable\n *\n * `reminderSentAtMs` on the task is the idempotence: stamped in one batch\n * with the member's sends, so a rerun over the same hour reads the mark and\n * sends nothing, and stamped on every reminder the run HANDLED — sent, or\n * found nobody to send it to — so a reminder nobody can be told about does\n * not come back every hour. A reminder is left unstamped only when the run\n * never reached a verdict: the send-rate governor refused, or the org\n * threw. Orgs are swept in id order behind a cursor, as the digest sweeps\n * them, so the workflow's `done:false` loop carries any number of them\n * across invocations.\n *\n * The task query reads the NEWEST reminders that have come due, not the\n * oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a\n * bounded page safe against a window that only ever grows.\n *\n * The email is a `bulk` send under the platform send-rate governor\n * (AGL-2409), metered as PLATFORM mail, for the digest's reasons: a cron\n * fan-out on the shared sending domain, and the platform reminding a\n * member is not the org's sending. A refusal STOPS the run and reports\n * `done: true`; the refused member was never stamped, and the next hour's\n * run finds their reminders at the top of the window again.\n */\n\n/** The beat `/api/health/crons` reads — the `SCHEDULED_JOBS` row's id. */\nexport const CRM_TASK_REMINDERS_JOB_ID = 'crm-task-reminders'\n\n/**\n * Orgs per invocation.\n *\n * More than the digest's twenty-five: an org here costs one task query and,\n * only when something fell due, the roster and the sends. The common hour\n * finds nothing due in most orgs, and a chunk that reads a hundred empty\n * queries finishes well inside `maxDuration = 60`, which is what the whole\n * platform costs on the night `orgIdsWithDueReminders` cannot answer.\n */\nexport const CRM_TASK_REMINDERS_ORG_CHUNK = 100\n\n/** Tasks per page of the platform-wide due query. */\nconst DUE_TASK_PAGE = 500\n\n/**\n * The most members per org whose address is chased through the auth pools\n * when their member document carries none — the digest's bound, for the\n * digest's reason: work, not audience.\n */\nexport const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Snapshot = FirebaseFirestore.DocumentSnapshot\n\n/** What one member's row in the response says. */\ninterface MemberReport {\n tasks: number\n skipped?: string\n notified?: number\n emailed?: boolean\n emailReason?: string\n deferred?: boolean\n}\n\ninterface OrgReport {\n skipped?: string\n /** Reminders that had come due and were not yet handled. */\n due: number\n /** Reminders this run stamped as handled. */\n handled: number\n /** Console notifications sent. */\n notified: number\n /** Emails sent — one per member per run. */\n emailed: number\n members?: Record<string, MemberReport>\n tasksTruncated?: boolean\n /** The send-rate governor refused; the sweep stops here. */\n deferred?: boolean\n}\n\ninterface SweepContext {\n firestore: Firestore\n nowMs: number\n timeZone: string\n dryRun: boolean\n flagValues: Awaited<ReturnType<typeof getServerReleaseFlagValues>>\n origin: string\n}\n\n/** A due reminder with the document it came from, so the stamp has a target. */\ninterface DueTask extends CrmReminderTask {\n ref: FirebaseFirestore.DocumentReference\n}\n\n/**\n * THE ORGS WITH A REMINDER OWED, found from the tasks (AGL-4121).\n *\n * The sweep used to walk the orgs to find them: every org document read, and\n * every entitled org's tasks asked, every hour, to learn that nothing had come\n * due in almost all of them. Asked of the collection group, an hour with\n * nothing due is one query and no org at all.\n *\n * Every page is read, not the newest, and that is what keeps this honest.\n * `CRM_TASK_REMINDER_CEILING` is safe per org because the newest five hundred\n * of ONE org begin with what fell due since the last run; across the platform\n * one busy org's handled reminders could bury a quiet org's first one. This\n * pays once for the same open tasks the org queries read, projected to what\n * the rule needs, and `remindOrg` then decides from its own window exactly as\n * it always has. An org found here with nothing inside that window reports\n * `nothing-due`, as it did.\n *\n * Needs the COLLECTION_GROUP `(status, remindAtMs desc)` index, and the\n * `crmTasks` entry for it is deployed after the code that first asks: see the\n * caller for what happens until then.\n */\nasync function orgIdsWithDueReminders(firestore: Firestore, nowMs: number): Promise<string[]> {\n const orgIds = new Set<string>()\n let after: FirebaseFirestore.QueryDocumentSnapshot | undefined\n for (;;) {\n let query = firestore\n .collectionGroup(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .select('status', 'remindAtMs', 'reminderSentAtMs')\n .limit(DUE_TASK_PAGE)\n if (after) query = query.startAfter(after)\n const page = await query.get()\n for (const doc of page.docs) {\n const org = doc.ref.parent.parent\n if (org?.parent.id !== 'orgs') continue\n if (crmTaskReminderDue(doc.data() as Parameters<typeof crmTaskReminderDue>[0], nowMs)) {\n orgIds.add(org.id)\n }\n }\n if (page.docs.length < DUE_TASK_PAGE) return [...orgIds].sort()\n after = page.docs[page.docs.length - 1]\n }\n}\n\nasync function remindOrg(ctx: SweepContext, orgDoc: Snapshot): Promise<OrgReport> {\n const { firestore, nowMs, timeZone, dryRun } = ctx\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Partial<AglynOrgBilling> & Record<string, unknown>\n const quiet: OrgReport = { due: 0, handled: 0, notified: 0, emailed: 0 }\n\n if (!crmDigestEntitled(resolveOrgEntitlements(org).features)) {\n return { ...quiet, skipped: 'not-entitled' }\n }\n const flagOn = isReleaseFlagOnForOrg(\n 'release_crm',\n ctx.flagValues['release_crm'],\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org),\n )\n if (!flagOn) return { ...quiet, skipped: 'release-flag' }\n\n /*\n * Every open task whose reminder has come, newest first, on\n * `(status, remindAtMs desc)`. The \"not yet handled\" half is a fact about\n * a field's absence, which a query cannot select on, so it is asked of\n * the page in memory — and the page being the NEWEST five hundred is what\n * keeps the handled ones from crowding out the next one due.\n */\n const page = await orgDoc.ref\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .limit(CRM_TASK_REMINDER_CEILING)\n .get()\n const due: DueTask[] = []\n for (const doc of page.docs) {\n const data = doc.data() as Record<string, unknown>\n if (!crmTaskReminderDue(data as Parameters<typeof crmTaskReminderDue>[0], nowMs)) continue\n due.push({\n ref: doc.ref,\n id: doc.id,\n title: String(data['title'] ?? ''),\n kind: (data['kind'] as CrmReminderTask['kind']) ?? 'todo',\n dueAtMs: typeof data['dueAtMs'] === 'number' ? (data['dueAtMs'] as number) : null,\n remindAtMs: Number(data['remindAtMs']),\n assigneeUid: String(data['assigneeUid'] ?? ''),\n hostId: String(data['hostId'] ?? ''),\n contactId: (data['contactId'] as string) || undefined,\n companyId: (data['companyId'] as string) || undefined,\n dealId: (data['dealId'] as string) || undefined,\n })\n }\n const tasksTruncated = page.docs.length >= CRM_TASK_REMINDER_CEILING\n if (!due.length) {\n return { ...quiet, skipped: 'nothing-due', ...(tasksTruncated ? { tasksTruncated } : {}) }\n }\n\n const report: OrgReport = { due: due.length, handled: 0, notified: 0, emailed: 0, members: {} }\n if (tasksTruncated) report.tasksTruncated = true\n const rows = report.members as Record<string, MemberReport>\n\n /**\n * Stamp a set of reminders as handled, in one batch, unless this is a\n * plan. Everything a member's verdict covers lands together, which is\n * what makes a rerun read the same answer for all of them.\n */\n const settle = async (tasks: DueTask[]): Promise<void> => {\n report.handled += tasks.length\n if (dryRun) return\n const batch = firestore.batch()\n for (const task of tasks) batch.update(task.ref, { reminderSentAtMs: nowMs })\n await batch.commit()\n }\n\n const byAssignee = new Map<string, DueTask[]>()\n const unassigned: DueTask[] = []\n for (const task of due) {\n if (!task.assigneeUid) {\n unassigned.push(task)\n continue\n }\n const mine = byAssignee.get(task.assigneeUid) ?? []\n mine.push(task)\n byAssignee.set(task.assigneeUid, mine)\n }\n // A reminder for nobody is handled by saying so: it would otherwise sit\n // at the top of the window every hour for as long as the task is open.\n if (unassigned.length) {\n rows[''] = { tasks: unassigned.length, skipped: 'unassigned' }\n await settle(unassigned)\n }\n if (!byAssignee.size) return report\n\n const listed = (await listOrgMembers(orgId)).filter(\n (member) => member.orgSuspended !== true,\n )\n // A locked or disabled account is not told (AGL-3418): it cannot sign in\n // to act on it.\n const reachable = new Set(\n await withoutMailWithheldAccounts(listed.map((member) => member.$id)),\n )\n const members = listed.filter((member) => reachable.has(member.$id))\n const memberById = new Map(members.map((member) => [member.$id, member]))\n // Sorted so the order members are told in — and the member a refusal\n // lands on — is the same on every run.\n const uids = [...byAssignee.keys()].sort()\n const userDocs = await firestore.getAll(\n ...uids.map((uid) => firestore.collection('users').doc(uid)),\n )\n const userById = new Map(userDocs.map((doc) => [doc.id, doc]))\n\n // The sites the due tasks name, for the absolute links the mail carries.\n const hostIds = [...new Set(due.map((task) => task.hostId).filter(Boolean))]\n const hostDocs = hostIds.length\n ? await firestore.getAll(...hostIds.map((hostId) => firestore.collection('hosts').doc(hostId)))\n : []\n const hostById = new Map(hostDocs.map((doc) => [doc.id, doc]))\n\n const branding = resolveBrandingProfile(org)\n const orgSlug = String(org['slug'] ?? '')\n const workspaceName = String(org['name'] ?? '').trim() || orgSlug\n const orgHub = orgSlug ? `${ctx.origin}${buildRoute(Route.ORG_HOME, { orgSlug })}/crm` : null\n const hubUrl = (hostId: string): string | null => {\n if (!hostId) return orgHub\n const subdomain = String(hostById.get(hostId)?.get('subdomain') ?? '')\n if (!orgSlug || !subdomain) return null\n return `${ctx.origin}${buildRoute(Route.HOST_DASHBOARD, { orgSlug, host: subdomain })}/crm`\n }\n // The record the task is for, on its own hub — the notification's link\n // past the `/{hostId}/crm` or `/org/crm` prefix the console rewrites.\n const taskUrl = (task: CrmReminderTask): string => {\n const hub = hubUrl(task.hostId)\n if (!hub) return ctx.origin\n const link = crmTaskReminderLink(task.hostId || null, task)\n return `${hub}${link.slice(link.indexOf('/crm') + '/crm'.length)}`\n }\n const settingsUrl = `${ctx.origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const supportLine = brandSupportLine(branding)\n const brand = systemEmailBrand(org)\n let reminderEmail: Promise<LoadedSystemEmail | null> | undefined\n let poolLookups = 0\n\n for (const uid of uids) {\n const tasks = byAssignee.get(uid) as DueTask[]\n const row: MemberReport = { tasks: tasks.length }\n rows[uid] = row\n const member = memberById.get(uid)\n if (!member) {\n row.skipped = 'not-a-member'\n await settle(tasks)\n continue\n }\n if (!(await memberHasOrgPermission(orgId, member as AglynOrgMember, 'data.manage'))) {\n row.skipped = 'no-data-manage'\n await settle(tasks)\n continue\n }\n const userDoc = userById.get(uid)\n if (notificationMuted(userDoc?.get('notificationPrefs'), 'content.taskReminder')) {\n row.skipped = 'muted'\n await settle(tasks)\n continue\n }\n if (dryRun) {\n row.notified = tasks.length\n row.emailed = true\n await settle(tasks)\n continue\n }\n\n /*\n * THE EMAIL FIRST, for the digest's reason: a refusal from the\n * send-rate governor stops the run with nothing else done for this\n * member — no notification to double next hour, no mark to skip them\n * on — so the order here is the idempotence, not a preference.\n */\n let emailed = false\n let emailReason: string | undefined\n let address = String(member.email ?? '').trim().toLowerCase()\n if (!address.includes('@') && poolLookups < CRM_TASK_REMINDERS_MAX_UID_LOOKUPS) {\n poolLookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n const recipients = address.includes('@')\n ? await filterSuppressedEmails([address], firestore)\n : []\n if (!recipients.length) {\n emailReason = address.includes('@') ? 'suppressed' : 'no-recipient'\n } else {\n const reminderInput = {\n tasks,\n timeZone,\n productName: branding.productName,\n workspaceName,\n taskUrl,\n settingsUrl,\n supportLine,\n }\n const subject = composeCrmTaskReminderSubject(tasks)\n // The `crm-task-reminder` system email (AGL-3367), in the org's brand,\n // read once per org and rendered per member.\n reminderEmail ??= loadSystemEmail('crm-task-reminder').catch(() => null)\n const loaded = await reminderEmail\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...brand.merge,\n 'reminder.subject': subject,\n 'reminder.body': composeCrmTaskReminderEmailText({ ...reminderInput, omitClosing: true }),\n ...crmTaskReminderMergeValues(composeCrmTaskReminderEmailParts(reminderInput)),\n settingsUrl,\n },\n brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject,\n text: composeCrmTaskReminderEmailText(reminderInput),\n }),\n fromName: branding.fromName,\n context: 'crm-task-reminder',\n priority: 'bulk',\n })\n if (result.sent) {\n emailed = true\n await meterPlatformEmail().catch(() => undefined)\n } else if (rateLimitedRetryAtMs(result) !== null) {\n row.deferred = true\n report.deferred = true\n return report\n } else {\n emailReason = (result as { reason?: string }).reason ?? 'failed'\n }\n }\n\n // One notification per task: each opens the record it is for.\n for (const task of tasks) {\n await notifyUsers([uid], {\n type: 'content.taskReminder',\n title: 'Task reminder',\n body: composeCrmTaskReminderBody(task, timeZone, workspaceName),\n link: crmTaskReminderLink(task.hostId || null, task),\n orgId,\n ...(task.hostId ? { hostId: task.hostId } : {}),\n })\n }\n await settle(tasks)\n row.notified = tasks.length\n row.emailed = emailed\n if (emailReason) row.emailReason = emailReason\n report.notified += tasks.length\n if (emailed) report.emailed += 1\n }\n return report\n}\n\nexport async function crmTaskRemindersRoute(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST' && method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n if (!process.env.CRON_SECRET) {\n return Response.json(\n { error: 'Task reminders are not configured (CRON_SECRET).' },\n { status: 501 },\n )\n }\n if (!isCronAuthorized(headers)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n // A GET reports the plan and sends nothing; a bodyless POST — what the\n // scheduled workflow sends — is a real run. `isCronDryRun` keys the\n // default on the METHOD for exactly that reason.\n const dryRun = isCronDryRun({ method, body, query })\n // Stamped on the invocation, not on the work (AGL-1955): an hour with\n // nothing due still ran.\n if (method === 'POST') await recordCronBeat(CRM_TASK_REMINDERS_JOB_ID)\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const nowMs = Date.now()\n const ctx: SweepContext = {\n firestore,\n nowMs,\n timeZone: digestTimeZone(),\n dryRun,\n flagValues: await getServerReleaseFlagValues(),\n origin: consoleOrigin(),\n }\n\n // The resumable sweep, in the shape the digest reads orgs (AGL-2220):\n // ordered by id, one more than a chunk to learn whether there is more, the\n // cursor validated as an org id before it becomes a reference.\n const requestedLimit = Number((body as { limit?: unknown } | undefined)?.limit)\n const pageSize =\n Number.isFinite(requestedLimit) && requestedLimit > 0\n ? Math.min(Math.floor(requestedLimit), CRM_TASK_REMINDERS_ORG_CHUNK)\n : CRM_TASK_REMINDERS_ORG_CHUNK\n const rawCursor = (body as { cursor?: unknown } | undefined)?.cursor\n const cursor =\n typeof rawCursor === 'string' && rawCursor.length > 0 && !rawCursor.includes('/')\n ? rawCursor\n : null\n /*\n * Only the orgs with something owed. A due query that fails is not an hour\n * with nothing due, so it never answers for one: it falls back to walking\n * every org, which needs no collection-group index and is what makes this\n * safe to ship ahead of the index it uses. Composite indexes deploy in the\n * promotion's tail, and a reminder must not wait on the order of that tail.\n */\n const withDue = await orgIdsWithDueReminders(firestore, nowMs).catch((error: unknown) => {\n console.warn('[crm] task reminders: the due query failed, sweeping every org', error)\n return null\n })\n let orgDocs: Snapshot[]\n let hasMore: boolean\n /** Where the next chunk starts, whichever way this one was chosen. */\n let lastOrgId: string | null\n if (withDue) {\n const ahead = cursor ? withDue.filter((orgId) => orgId > cursor) : withDue\n const chunk = ahead.slice(0, pageSize)\n hasMore = ahead.length > pageSize\n lastOrgId = chunk[chunk.length - 1] ?? null\n // A task can outlive its org by a moment; there is nobody to tell.\n orgDocs = chunk.length\n ? (await firestore.getAll(...chunk.map((orgId) => firestore.collection('orgs').doc(orgId)))).filter(\n (doc) => doc.exists,\n )\n : []\n } else {\n let orgQuery = firestore\n .collection('orgs')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(pageSize + 1)\n if (cursor) orgQuery = orgQuery.startAfter(firestore.collection('orgs').doc(cursor))\n const page = await orgQuery.get()\n hasMore = page.docs.length > pageSize\n orgDocs = hasMore ? page.docs.slice(0, pageSize) : page.docs\n lastOrgId = orgDocs[orgDocs.length - 1]?.id ?? null\n }\n\n const orgs: Record<string, OrgReport> = {}\n const failures: Record<string, string> = {}\n let due = 0\n let handled = 0\n let notified = 0\n let emailed = 0\n let deferred = false\n for (const orgDoc of orgDocs) {\n try {\n const report = await remindOrg(ctx, orgDoc)\n orgs[orgDoc.id] = report\n due += report.due\n handled += report.handled\n notified += report.notified\n emailed += report.emailed\n if (report.deferred) {\n deferred = true\n break\n }\n } catch (error) {\n console.error('[crm] task reminders failed for org', orgDoc.id, error)\n failures[orgDoc.id] = (error as Error)?.message ?? 'unknown'\n }\n }\n const failed = Object.keys(failures)\n return Response.json(\n {\n timeZone: ctx.timeZone,\n dryRun,\n swept: orgDocs.length,\n due,\n handled,\n notified,\n emailed,\n orgs,\n ...(deferred ? { deferred: true } : {}),\n ...(failed.length ? { failures } : {}),\n // A deferred run reports `done: true` — see the module comment —\n // so the workflow's loop does not re-POST into a full window.\n nextCursor: deferred || !hasMore ? null : lastOrgId,\n done: deferred || !hasMore,\n },\n // 207: this chunk finished and some org in it needs a person, which is\n // what fails the workflow (its one alerting channel).\n { status: failed.length ? 207 : 200 },\n )\n } catch (error) {\n console.error('[crm] task reminders run failed', error)\n return Response.json({ error: 'Task reminders run failed' }, { status: 500 })\n }\n}\n"],"names":["buildRoute","CRM_COLLECTIONS","isReleaseFlagOnForOrg","notificationMuted","parseOrgReleaseFlagOverrides","pluginRequestFromWeb","resolveBrandingProfile","resolveEffectivePlan","resolveOrgEntitlements","Route","crmDigestEntitled","rateLimitedRetryAtMs","sendEmail","withoutMailWithheldAccounts","findUserByUidAcrossPools","firebaseAdmin","getServerReleaseFlagValues","listOrgMembers","memberHasOrgPermission","meterPlatformEmail","notifyUsers","filterSuppressedEmails","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","isCronAuthorized","isCronDryRun","writeCronBeat","brandSupportLine","platformConsoleOrigin","consoleOrigin","digestTimeZone","composeCrmTaskReminderBody","composeCrmTaskReminderEmailParts","composeCrmTaskReminderEmailText","crmTaskReminderMergeValues","composeCrmTaskReminderSubject","CRM_TASK_REMINDER_CEILING","crmTaskReminderDue","crmTaskReminderLink","recordCronBeat","jobId","app","firestore","CRM_TASK_REMINDERS_JOB_ID","CRM_TASK_REMINDERS_ORG_CHUNK","DUE_TASK_PAGE","CRM_TASK_REMINDERS_MAX_UID_LOOKUPS","orgIdsWithDueReminders","nowMs","orgIds","Set","after","query","collectionGroup","tasks","where","orderBy","select","limit","startAfter","page","get","doc","docs","org","ref","parent","id","data","add","length","sort","remindOrg","ctx","orgDoc","timeZone","dryRun","orgId","quiet","due","handled","notified","emailed","features","skipped","flagOn","flagValues","collection","push","title","String","kind","dueAtMs","remindAtMs","Number","assigneeUid","hostId","contactId","undefined","companyId","dealId","tasksTruncated","report","members","rows","settle","batch","task","update","reminderSentAtMs","commit","byAssignee","Map","unassigned","mine","set","size","listed","filter","member","orgSuspended","reachable","map","$id","has","memberById","uids","keys","userDocs","getAll","uid","userById","hostIds","Boolean","hostDocs","hostById","branding","orgSlug","workspaceName","trim","orgHub","origin","ORG_HOME","hubUrl","subdomain","HOST_DASHBOARD","host","taskUrl","hub","link","slice","indexOf","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","supportLine","brand","reminderEmail","poolLookups","row","userDoc","emailReason","address","email","toLowerCase","includes","pooled","catch","record","recipients","reminderInput","productName","subject","loaded","rendered","merge","omitClosing","options","result","to","text","fromName","context","priority","sent","deferred","reason","type","body","crmTaskRemindersRoute","request","method","headers","rawHeaders","Response","json","error","status","process","env","CRON_SECRET","Date","now","requestedLimit","pageSize","isFinite","Math","min","floor","rawCursor","cursor","withDue","console","warn","orgDocs","hasMore","lastOrgId","chunk","ahead","exists","orgQuery","FieldPath","documentId","orgs","failures","message","failed","Object","swept","nextCursor","done"],"mappings":"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"}
|