@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.220
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 +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +63 -35
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +8 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +47 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +12 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +58 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -86,29 +86,45 @@
|
|
|
86
86
|
if (task.companyId) return `${base}/companies/${encodeURIComponent(task.companyId)}`;
|
|
87
87
|
return `${base}/tasks`;
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* `Wed, Sep 9, 3:00 PM CDT` — the words a reminder says a time with. The zone
|
|
91
|
+
* is the server's choice, not the reader's, so it is printed: a reader an
|
|
92
|
+
* hour away would otherwise take it for their own clock.
|
|
93
|
+
*/ export function crmTaskReminderWhen(ms, timeZone) {
|
|
90
94
|
return new Date(ms).toLocaleString('en-US', {
|
|
91
95
|
timeZone,
|
|
92
96
|
weekday: 'short',
|
|
93
97
|
month: 'short',
|
|
94
98
|
day: 'numeric',
|
|
95
99
|
hour: 'numeric',
|
|
96
|
-
minute: '2-digit'
|
|
100
|
+
minute: '2-digit',
|
|
101
|
+
timeZoneName: 'short'
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
|
-
*
|
|
101
|
-
* due
|
|
102
|
-
* the person chose
|
|
103
|
-
*/ export function
|
|
104
|
-
return typeof task.dueAtMs === 'number' ? `${task.title} · due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}` : task.title
|
|
105
|
+
* One task as the reminder email lists it: its title and when it is due, or
|
|
106
|
+
* that it has no due date — a reminder can sit on a task with none, at a time
|
|
107
|
+
* the person chose.
|
|
108
|
+
*/ export function crmTaskReminderLine(task, timeZone) {
|
|
109
|
+
return typeof task.dueAtMs === 'number' ? `${task.title} · due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}` : `${task.title} · no due date`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The console notification's body, one per task, which is also what a
|
|
113
|
+
* preview of it shows. It says it is a reminder and names the workspace, so
|
|
114
|
+
* it reads whole without the "Task reminder" title above it.
|
|
115
|
+
*/ export function composeCrmTaskReminderBody(task, timeZone, workspaceName) {
|
|
116
|
+
const workspace = workspaceName == null ? void 0 : workspaceName.trim();
|
|
117
|
+
const when = typeof task.dueAtMs === 'number' ? `, due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}.` : ' (no due date).';
|
|
118
|
+
return `Reminder for your task${workspace ? ` in ${workspace}` : ''}: ${task.title}${when}`;
|
|
105
119
|
}
|
|
106
120
|
/**
|
|
107
121
|
* One mail per member per run, so five tasks due at nine o'clock are one
|
|
108
|
-
* message and not five. The subject names the task when there is one
|
|
122
|
+
* message and not five. The subject names the task when there is one, and
|
|
123
|
+
* never says a task is due: a reminder can be set ahead of the due time, or
|
|
124
|
+
* on a task with none.
|
|
109
125
|
*/ export function composeCrmTaskReminderSubject(tasks) {
|
|
110
126
|
if (tasks.length === 1) return `Reminder: ${tasks[0].title}`;
|
|
111
|
-
return `Reminder: ${tasks.length} tasks
|
|
127
|
+
return `Reminder: ${tasks.length} tasks`;
|
|
112
128
|
}
|
|
113
129
|
/**
|
|
114
130
|
* The plain-text email. Text rather than a designed template for the
|
|
@@ -117,6 +133,7 @@
|
|
|
117
133
|
* its own line with its link under it, and the way out last.
|
|
118
134
|
*/ export function composeCrmTaskReminderEmailText(input) {
|
|
119
135
|
var _input_supportLine;
|
|
136
|
+
var _input_workspaceName;
|
|
120
137
|
const { tasks, timeZone, productName } = input;
|
|
121
138
|
const ordered = [
|
|
122
139
|
...tasks
|
|
@@ -124,12 +141,16 @@
|
|
|
124
141
|
var _a_dueAtMs, _b_dueAtMs;
|
|
125
142
|
return ((_a_dueAtMs = a.dueAtMs) != null ? _a_dueAtMs : a.remindAtMs) - ((_b_dueAtMs = b.dueAtMs) != null ? _b_dueAtMs : b.remindAtMs);
|
|
126
143
|
});
|
|
144
|
+
// "About", never "is due": a reminder fires at the time somebody chose,
|
|
145
|
+
// which may be a day ahead of the due date or on a task with none, and
|
|
146
|
+
// each line below says when its task is actually due.
|
|
147
|
+
const where = ((_input_workspaceName = input.workspaceName) == null ? void 0 : _input_workspaceName.trim()) ? ` in ${input.workspaceName.trim()}` : '';
|
|
127
148
|
const lines = [
|
|
128
|
-
ordered.length === 1 ? `A reminder from ${productName}
|
|
149
|
+
ordered.length === 1 ? `A reminder from ${productName} about your task${where}:` : `A reminder from ${productName} about ${ordered.length} of your tasks${where}:`,
|
|
129
150
|
''
|
|
130
151
|
];
|
|
131
152
|
for (const task of ordered){
|
|
132
|
-
lines.push(`- ${
|
|
153
|
+
lines.push(`- ${crmTaskReminderLine(task, timeZone)}`);
|
|
133
154
|
lines.push(` ${input.taskUrl(task)}`);
|
|
134
155
|
}
|
|
135
156
|
if (input.omitClosing) return lines.join('\n').trimEnd();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm-task-reminders.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\n/**\n * A task's reminder at its own time (AGL-2659).\n *\n * The daily digest says once a morning what is late and what is due today;\n * nothing said \"the 3:00 call is at 3:00\". A task now carries `remindAtMs`\n * — the due time unless a person moved it, `null` for none — and an hourly\n * route reads every open task whose reminder has come due and tells the\n * assignee once, in the console and by mail. Every judgment here is pure so\n * the three writers of the field (the task routes, the REST resource, the\n * drawer) and the route that reads it agree on what a reminder is.\n *\n * ## Which reminder a save leaves\n *\n * A caller that SAYS what the reminder is gets what it said. A caller that\n * says nothing gets the rule: a new task's reminder is its due time, and an\n * edit that moves the due date moves a reminder that still sat on the old\n * due time — the reminder was never a choice of its own, so it follows —\n * while a reminder somebody set to a time of their own stays where they put\n * it. The one thing an edit never does is invent a reminder for a task that\n * has none.\n */\n\nimport type { CrmTask } from './crm'\n\n/**\n * The most due reminders one org's sweep reads per run.\n *\n * NEWEST FIRST, which is the whole reason a ceiling is safe here. A sent\n * reminder keeps its `remindAtMs` — the drawer shows when it fired — so the\n * range \"reminders up to now\" holds every reminder an open task ever had,\n * and a window of the OLDEST five hundred would fill with ones already sent\n * and never reach a new one. The newest five hundred always begin with what\n * fell due since the last run; the sent ones sit behind them.\n */\nexport const CRM_TASK_REMINDER_CEILING = 500\n\n/** The two fields the rule reads off a stored task, or off nothing on create. */\nexport interface CrmTaskReminderState {\n dueAtMs: number | null\n remindAtMs: number | null\n}\n\n/**\n * The reminder a save leaves on the task — see the module comment.\n *\n * `remindAtMs` is what the caller SAID: a time, `null` for \"no reminder\",\n * `undefined` for nothing said. `previous` is the stored task, or `null`\n * when the task is being created. A stored task from before reminders\n * existed carries no `remindAtMs` at all; it reads as `null` here — no\n * reminder — rather than as one that follows the due date, because a task\n * nobody ever set a reminder on should not start reminding the day its\n * title is corrected.\n */\nexport function crmTaskReminderAfterEdit(input: {\n dueAtMs: number | null\n remindAtMs?: number | null\n previous: CrmTaskReminderState | null\n}): number | null {\n const { dueAtMs, remindAtMs, previous } = input\n if (remindAtMs !== undefined) return remindAtMs\n if (!previous) return dueAtMs\n const followed =\n typeof previous.remindAtMs === 'number' && previous.remindAtMs === previous.dueAtMs\n if (followed && dueAtMs !== previous.dueAtMs) return dueAtMs\n return previous.remindAtMs ?? null\n}\n\n/** Whether a task's reminder is still owed: set, and not yet handled. */\nexport function crmTaskReminderPending(\n task: Pick<CrmTask, 'remindAtMs' | 'reminderSentAtMs'>,\n): boolean {\n return typeof task.remindAtMs === 'number' && task.reminderSentAtMs === undefined\n}\n\n/**\n * Whether the runner owes this task a reminder now: open, pending, and the\n * reminder's time has come. The query reads the first and third off the\n * index; the second is a fact about a field's absence, which a query cannot\n * select on, so the runner asks it here over the page it read.\n */\nexport function crmTaskReminderDue(\n task: Pick<CrmTask, 'status' | 'remindAtMs' | 'reminderSentAtMs'>,\n nowMs: number,\n): boolean {\n return (\n task.status === 'open' &&\n crmTaskReminderPending(task) &&\n (task.remindAtMs as number) <= nowMs\n )\n}\n\n/** A due reminder as the runner carries it: the fields a line and a link need. */\nexport interface CrmReminderTask\n extends Pick<CrmTask, 'title' | 'kind' | 'contactId' | 'companyId' | 'dealId'> {\n id: string\n dueAtMs: number | null\n remindAtMs: number\n assigneeUid: string\n /** The site the task was created on, or `''` for the organization's own. */\n hostId: string\n}\n\n/**\n * The console path a reminder opens: the record the task is for, in the\n * hub, or the tasks list when it is for nobody in particular — the same\n * page the assignment notification opens, in the `/{hostDocId}/rest` shape\n * every host notification uses and the `/org/rest` shape the console\n * rewrites onto the organization's own hub for a task with no site.\n */\nexport function crmTaskReminderLink(\n hostId: string | null,\n task: Pick<CrmTask, 'contactId' | 'companyId' | 'dealId'>,\n): string {\n const base = hostId ? `/${hostId}/crm` : '/org/crm'\n if (task.contactId) return `${base}/contacts/${encodeURIComponent(task.contactId)}`\n if (task.dealId) return `${base}/deals/${encodeURIComponent(task.dealId)}`\n if (task.companyId) return `${base}/companies/${encodeURIComponent(task.companyId)}`\n return `${base}/tasks`\n}\n\n/** `Wed, Sep 9, 3:00 PM` in a named zone — the words a reminder says a time with. */\nexport function crmTaskReminderWhen(ms: number, timeZone: string): string {\n return new Date(ms).toLocaleString('en-US', {\n timeZone,\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n })\n}\n\n/**\n * The one line the console notification is: the title and when the task is\n * due. A reminder set on a task with no due date says only the title —\n * the person chose the time; the task itself has none.\n */\nexport function composeCrmTaskReminderBody(\n task: Pick<CrmReminderTask, 'title' | 'dueAtMs'>,\n timeZone: string,\n): string {\n return typeof task.dueAtMs === 'number'\n ? `${task.title} · due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}`\n : task.title\n}\n\n/**\n * One mail per member per run, so five tasks due at nine o'clock are one\n * message and not five. The subject names the task when there is one.\n */\nexport function composeCrmTaskReminderSubject(\n tasks: ReadonlyArray<Pick<CrmReminderTask, 'title'>>,\n): string {\n if (tasks.length === 1) return `Reminder: ${tasks[0].title}`\n return `Reminder: ${tasks.length} tasks are due`\n}\n\n/** What the reminder email needs from the route besides the tasks. */\nexport interface CrmTaskReminderEmailInput {\n tasks: readonly CrmReminderTask[]\n timeZone: string\n /** The product name the mail reads as — the org's brand when white-labeled. */\n productName: string\n /** The console page a task opens on, absolute. */\n taskUrl: (task: CrmReminderTask) => string\n /** Account settings → Notifications, where the category can be muted. */\n settingsUrl: string\n /** The brand's support line, already prefixed, or empty. */\n supportLine?: string\n /**\n * Leave out the closing paragraph and the support line (AGL-3367), for the\n * copy that goes inside a system email: its footer says why the mail came\n * and where to get help, and its body links the settings page.\n */\n omitClosing?: boolean\n}\n\n/**\n * The plain-text email. Text rather than a designed template for the\n * reason the digest gives: the body is a list, and `sendEmail` synthesizes\n * the HTML part that makes its links live. Soonest due first, each task on\n * its own line with its link under it, and the way out last.\n */\nexport function composeCrmTaskReminderEmailText(input: CrmTaskReminderEmailInput): string {\n const { tasks, timeZone, productName } = input\n const ordered = [...tasks].sort(\n (a, b) => (a.dueAtMs ?? a.remindAtMs) - (b.dueAtMs ?? b.remindAtMs),\n )\n const lines: string[] = [\n ordered.length === 1\n ? `A reminder from ${productName}: this task is due.`\n : `A reminder from ${productName}: ${ordered.length} tasks are due.`,\n '',\n ]\n for (const task of ordered) {\n lines.push(`- ${composeCrmTaskReminderBody(task, timeZone)}`)\n lines.push(` ${input.taskUrl(task)}`)\n }\n if (input.omitClosing) return lines.join('\\n').trimEnd()\n lines.push(\n '',\n 'You get task reminders because the Forms & bookings category is on in ' +\n `your notification settings: ${input.settingsUrl}`,\n )\n return lines.join('\\n') + (input.supportLine ?? '')\n}\n"],"names":["CRM_TASK_REMINDER_CEILING","crmTaskReminderAfterEdit","input","previous","dueAtMs","remindAtMs","undefined","followed","crmTaskReminderPending","task","reminderSentAtMs","crmTaskReminderDue","nowMs","status","crmTaskReminderLink","hostId","base","contactId","encodeURIComponent","dealId","companyId","crmTaskReminderWhen","ms","timeZone","Date","toLocaleString","weekday","month","day","hour","minute","composeCrmTaskReminderBody","title","composeCrmTaskReminderSubject","tasks","length","composeCrmTaskReminderEmailText","productName","ordered","sort","a","b","lines","push","taskUrl","omitClosing","join","trimEnd","settingsUrl","supportLine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm-task-reminders.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\n/**\n * A task's reminder at its own time (AGL-2659).\n *\n * The daily digest says once a morning what is late and what is due today;\n * nothing said \"the 3:00 call is at 3:00\". A task now carries `remindAtMs`\n * — the due time unless a person moved it, `null` for none — and an hourly\n * route reads every open task whose reminder has come due and tells the\n * assignee once, in the console and by mail. Every judgment here is pure so\n * the three writers of the field (the task routes, the REST resource, the\n * drawer) and the route that reads it agree on what a reminder is.\n *\n * ## Which reminder a save leaves\n *\n * A caller that SAYS what the reminder is gets what it said. A caller that\n * says nothing gets the rule: a new task's reminder is its due time, and an\n * edit that moves the due date moves a reminder that still sat on the old\n * due time — the reminder was never a choice of its own, so it follows —\n * while a reminder somebody set to a time of their own stays where they put\n * it. The one thing an edit never does is invent a reminder for a task that\n * has none.\n */\n\nimport type { CrmTask } from './crm'\n\n/**\n * The most due reminders one org's sweep reads per run.\n *\n * NEWEST FIRST, which is the whole reason a ceiling is safe here. A sent\n * reminder keeps its `remindAtMs` — the drawer shows when it fired — so the\n * range \"reminders up to now\" holds every reminder an open task ever had,\n * and a window of the OLDEST five hundred would fill with ones already sent\n * and never reach a new one. The newest five hundred always begin with what\n * fell due since the last run; the sent ones sit behind them.\n */\nexport const CRM_TASK_REMINDER_CEILING = 500\n\n/** The two fields the rule reads off a stored task, or off nothing on create. */\nexport interface CrmTaskReminderState {\n dueAtMs: number | null\n remindAtMs: number | null\n}\n\n/**\n * The reminder a save leaves on the task — see the module comment.\n *\n * `remindAtMs` is what the caller SAID: a time, `null` for \"no reminder\",\n * `undefined` for nothing said. `previous` is the stored task, or `null`\n * when the task is being created. A stored task from before reminders\n * existed carries no `remindAtMs` at all; it reads as `null` here — no\n * reminder — rather than as one that follows the due date, because a task\n * nobody ever set a reminder on should not start reminding the day its\n * title is corrected.\n */\nexport function crmTaskReminderAfterEdit(input: {\n dueAtMs: number | null\n remindAtMs?: number | null\n previous: CrmTaskReminderState | null\n}): number | null {\n const { dueAtMs, remindAtMs, previous } = input\n if (remindAtMs !== undefined) return remindAtMs\n if (!previous) return dueAtMs\n const followed =\n typeof previous.remindAtMs === 'number' && previous.remindAtMs === previous.dueAtMs\n if (followed && dueAtMs !== previous.dueAtMs) return dueAtMs\n return previous.remindAtMs ?? null\n}\n\n/** Whether a task's reminder is still owed: set, and not yet handled. */\nexport function crmTaskReminderPending(\n task: Pick<CrmTask, 'remindAtMs' | 'reminderSentAtMs'>,\n): boolean {\n return typeof task.remindAtMs === 'number' && task.reminderSentAtMs === undefined\n}\n\n/**\n * Whether the runner owes this task a reminder now: open, pending, and the\n * reminder's time has come. The query reads the first and third off the\n * index; the second is a fact about a field's absence, which a query cannot\n * select on, so the runner asks it here over the page it read.\n */\nexport function crmTaskReminderDue(\n task: Pick<CrmTask, 'status' | 'remindAtMs' | 'reminderSentAtMs'>,\n nowMs: number,\n): boolean {\n return (\n task.status === 'open' &&\n crmTaskReminderPending(task) &&\n (task.remindAtMs as number) <= nowMs\n )\n}\n\n/** A due reminder as the runner carries it: the fields a line and a link need. */\nexport interface CrmReminderTask\n extends Pick<CrmTask, 'title' | 'kind' | 'contactId' | 'companyId' | 'dealId'> {\n id: string\n dueAtMs: number | null\n remindAtMs: number\n assigneeUid: string\n /** The site the task was created on, or `''` for the organization's own. */\n hostId: string\n}\n\n/**\n * The console path a reminder opens: the record the task is for, in the\n * hub, or the tasks list when it is for nobody in particular — the same\n * page the assignment notification opens, in the `/{hostDocId}/rest` shape\n * every host notification uses and the `/org/rest` shape the console\n * rewrites onto the organization's own hub for a task with no site.\n */\nexport function crmTaskReminderLink(\n hostId: string | null,\n task: Pick<CrmTask, 'contactId' | 'companyId' | 'dealId'>,\n): string {\n const base = hostId ? `/${hostId}/crm` : '/org/crm'\n if (task.contactId) return `${base}/contacts/${encodeURIComponent(task.contactId)}`\n if (task.dealId) return `${base}/deals/${encodeURIComponent(task.dealId)}`\n if (task.companyId) return `${base}/companies/${encodeURIComponent(task.companyId)}`\n return `${base}/tasks`\n}\n\n/**\n * `Wed, Sep 9, 3:00 PM CDT` — the words a reminder says a time with. The zone\n * is the server's choice, not the reader's, so it is printed: a reader an\n * hour away would otherwise take it for their own clock.\n */\nexport function crmTaskReminderWhen(ms: number, timeZone: string): string {\n return new Date(ms).toLocaleString('en-US', {\n timeZone,\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n })\n}\n\n/**\n * One task as the reminder email lists it: its title and when it is due, or\n * that it has no due date — a reminder can sit on a task with none, at a time\n * the person chose.\n */\nexport function crmTaskReminderLine(\n task: Pick<CrmReminderTask, 'title' | 'dueAtMs'>,\n timeZone: string,\n): string {\n return typeof task.dueAtMs === 'number'\n ? `${task.title} · due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}`\n : `${task.title} · no due date`\n}\n\n/**\n * The console notification's body, one per task, which is also what a\n * preview of it shows. It says it is a reminder and names the workspace, so\n * it reads whole without the \"Task reminder\" title above it.\n */\nexport function composeCrmTaskReminderBody(\n task: Pick<CrmReminderTask, 'title' | 'dueAtMs'>,\n timeZone: string,\n workspaceName?: string,\n): string {\n const workspace = workspaceName?.trim()\n const when =\n typeof task.dueAtMs === 'number'\n ? `, due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}.`\n : ' (no due date).'\n return `Reminder for your task${workspace ? ` in ${workspace}` : ''}: ${task.title}${when}`\n}\n\n/**\n * One mail per member per run, so five tasks due at nine o'clock are one\n * message and not five. The subject names the task when there is one, and\n * never says a task is due: a reminder can be set ahead of the due time, or\n * on a task with none.\n */\nexport function composeCrmTaskReminderSubject(\n tasks: ReadonlyArray<Pick<CrmReminderTask, 'title'>>,\n): string {\n if (tasks.length === 1) return `Reminder: ${tasks[0].title}`\n return `Reminder: ${tasks.length} tasks`\n}\n\n/** What the reminder email needs from the route besides the tasks. */\nexport interface CrmTaskReminderEmailInput {\n tasks: readonly CrmReminderTask[]\n timeZone: string\n /** The product name the mail reads as — the org's brand when white-labeled. */\n productName: string\n /**\n * The workspace the tasks are in. The system email shows only the body, so\n * the body names it: a member of two workspaces gets reminders from each.\n */\n workspaceName?: string\n /** The console page a task opens on, absolute. */\n taskUrl: (task: CrmReminderTask) => string\n /** Account settings → Notifications, where the category can be muted. */\n settingsUrl: string\n /** The brand's support line, already prefixed, or empty. */\n supportLine?: string\n /**\n * Leave out the closing paragraph and the support line (AGL-3367), for the\n * copy that goes inside a system email: its footer says why the mail came\n * and where to get help, and its body links the settings page.\n */\n omitClosing?: boolean\n}\n\n/**\n * The plain-text email. Text rather than a designed template for the\n * reason the digest gives: the body is a list, and `sendEmail` synthesizes\n * the HTML part that makes its links live. Soonest due first, each task on\n * its own line with its link under it, and the way out last.\n */\nexport function composeCrmTaskReminderEmailText(input: CrmTaskReminderEmailInput): string {\n const { tasks, timeZone, productName } = input\n const ordered = [...tasks].sort(\n (a, b) => (a.dueAtMs ?? a.remindAtMs) - (b.dueAtMs ?? b.remindAtMs),\n )\n // \"About\", never \"is due\": a reminder fires at the time somebody chose,\n // which may be a day ahead of the due date or on a task with none, and\n // each line below says when its task is actually due.\n const where = input.workspaceName?.trim() ? ` in ${input.workspaceName.trim()}` : ''\n const lines: string[] = [\n ordered.length === 1\n ? `A reminder from ${productName} about your task${where}:`\n : `A reminder from ${productName} about ${ordered.length} of your tasks${where}:`,\n '',\n ]\n for (const task of ordered) {\n lines.push(`- ${crmTaskReminderLine(task, timeZone)}`)\n lines.push(` ${input.taskUrl(task)}`)\n }\n if (input.omitClosing) return lines.join('\\n').trimEnd()\n lines.push(\n '',\n 'You get task reminders because the Forms & bookings category is on in ' +\n `your notification settings: ${input.settingsUrl}`,\n )\n return lines.join('\\n') + (input.supportLine ?? '')\n}\n"],"names":["CRM_TASK_REMINDER_CEILING","crmTaskReminderAfterEdit","input","previous","dueAtMs","remindAtMs","undefined","followed","crmTaskReminderPending","task","reminderSentAtMs","crmTaskReminderDue","nowMs","status","crmTaskReminderLink","hostId","base","contactId","encodeURIComponent","dealId","companyId","crmTaskReminderWhen","ms","timeZone","Date","toLocaleString","weekday","month","day","hour","minute","timeZoneName","crmTaskReminderLine","title","composeCrmTaskReminderBody","workspaceName","workspace","trim","when","composeCrmTaskReminderSubject","tasks","length","composeCrmTaskReminderEmailText","productName","ordered","sort","a","b","where","lines","push","taskUrl","omitClosing","join","trimEnd","settingsUrl","supportLine"],"mappings":"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"}
|
|
@@ -624,8 +624,9 @@ export interface CrmActivity extends CrmScoped {
|
|
|
624
624
|
deliveryDetail?: string;
|
|
625
625
|
/**
|
|
626
626
|
* `hosts/{hostId}/bookings/{bookingId}` — the booking a `meeting` was
|
|
627
|
-
* filed from (AGL-2660),
|
|
628
|
-
*
|
|
627
|
+
* filed from (AGL-2660), on the rows filed before a booking reached the
|
|
628
|
+
* record through the record-timeline seam. A booking's meeting is now an
|
|
629
|
+
* entry keyed by the booking, and `sourcePluginId` names who filed it.
|
|
629
630
|
*/
|
|
630
631
|
bookingId?: string;
|
|
631
632
|
/**
|
|
@@ -1341,13 +1342,20 @@ export declare function findOrgMember<T extends {
|
|
|
1341
1342
|
/**
|
|
1342
1343
|
* Where a lead stands, in the order a person works one.
|
|
1343
1344
|
*
|
|
1345
|
+
* `nurturing` is the stage automated email holds a lead in: a sequence step
|
|
1346
|
+
* or a campaign email has reached it and no person has engaged yet. It is
|
|
1347
|
+
* open — the lead still needs somebody — but it is not untouched, so the
|
|
1348
|
+
* digest's unworked list leaves it out. Automation writes it only over
|
|
1349
|
+
* `new` (a person may also set it by hand), and a reply moves it on to
|
|
1350
|
+
* `working`.
|
|
1351
|
+
*
|
|
1344
1352
|
* `qualified` is the CONVERTED state — a lead becomes a contact by being
|
|
1345
1353
|
* qualified, and the conversion stamps `convertedContactId` beside it — and
|
|
1346
1354
|
* `unqualified` is the closed-without-conversion state with its reason. A
|
|
1347
1355
|
* fixed list rather than free text for the reason the lifecycle stages are:
|
|
1348
1356
|
* the section filters on it and a report counts by it.
|
|
1349
1357
|
*/
|
|
1350
|
-
export declare const CRM_LEAD_STATUSES: readonly ["new", "working", "qualified", "unqualified"];
|
|
1358
|
+
export declare const CRM_LEAD_STATUSES: readonly ["new", "nurturing", "working", "qualified", "unqualified"];
|
|
1351
1359
|
export type CrmLeadStatus = (typeof CRM_LEAD_STATUSES)[number];
|
|
1352
1360
|
/** How a lead status reads on screen — typed so a status cannot ship unlabeled. */
|
|
1353
1361
|
export declare const CRM_LEAD_STATUS_LABELS: Record<CrmLeadStatus, string>;
|
|
@@ -1366,6 +1374,18 @@ export declare const CRM_LEAD_OPEN_STATUSES: readonly CrmLeadStatus[];
|
|
|
1366
1374
|
* `openLeadsFromCounts`.
|
|
1367
1375
|
*/
|
|
1368
1376
|
export declare const CRM_LEAD_CLOSED_STATUSES: readonly CrmLeadStatus[];
|
|
1377
|
+
/**
|
|
1378
|
+
* The leads still needing somebody, from two server counts: every lead,
|
|
1379
|
+
* less the ones closed one way or the other.
|
|
1380
|
+
*
|
|
1381
|
+
* Subtraction rather than a count of the open statuses because a lead
|
|
1382
|
+
* nobody has touched carries NO status field — see `crmLeadStatus` — and
|
|
1383
|
+
* Firestore cannot select a document by a field's absence. The closed
|
|
1384
|
+
* statuses are always written, so they can be counted; what remains is
|
|
1385
|
+
* open. Clamped at zero for the moment between the two counts in which a
|
|
1386
|
+
* lead was closed.
|
|
1387
|
+
*/
|
|
1388
|
+
export declare function openLeadsFromCounts(total: number, closed: number): number;
|
|
1369
1389
|
export declare function isCrmLeadStatus(value: unknown): value is CrmLeadStatus;
|
|
1370
1390
|
/**
|
|
1371
1391
|
* The working state the CRM writes onto a lead document, beside what the
|
|
@@ -1401,7 +1421,7 @@ export interface CrmLeadFields extends CrmLeadProfile {
|
|
|
1401
1421
|
/**
|
|
1402
1422
|
* The campaigns the lead is filed under (AGL-3254): container ids from
|
|
1403
1423
|
* `orgs/{orgId}/emailCampaigns`, at the top of the document the way a
|
|
1404
|
-
* form carries them (`
|
|
1424
|
+
* form carries them (`container-membership.ts`), never names. Written by
|
|
1405
1425
|
* the New lead drawer, the import, the bulk bar and a sequence's enroll;
|
|
1406
1426
|
* handed to the contact's facet when the lead converts.
|
|
1407
1427
|
*/
|
package/src/lib/app-utils/crm.js
CHANGED
|
@@ -1456,6 +1456,13 @@ export function parseCrmMemberRef(value) {
|
|
|
1456
1456
|
*=========================================*/ /**
|
|
1457
1457
|
* Where a lead stands, in the order a person works one.
|
|
1458
1458
|
*
|
|
1459
|
+
* `nurturing` is the stage automated email holds a lead in: a sequence step
|
|
1460
|
+
* or a campaign email has reached it and no person has engaged yet. It is
|
|
1461
|
+
* open — the lead still needs somebody — but it is not untouched, so the
|
|
1462
|
+
* digest's unworked list leaves it out. Automation writes it only over
|
|
1463
|
+
* `new` (a person may also set it by hand), and a reply moves it on to
|
|
1464
|
+
* `working`.
|
|
1465
|
+
*
|
|
1459
1466
|
* `qualified` is the CONVERTED state — a lead becomes a contact by being
|
|
1460
1467
|
* qualified, and the conversion stamps `convertedContactId` beside it — and
|
|
1461
1468
|
* `unqualified` is the closed-without-conversion state with its reason. A
|
|
@@ -1463,12 +1470,14 @@ export function parseCrmMemberRef(value) {
|
|
|
1463
1470
|
* the section filters on it and a report counts by it.
|
|
1464
1471
|
*/ export const CRM_LEAD_STATUSES = [
|
|
1465
1472
|
'new',
|
|
1473
|
+
'nurturing',
|
|
1466
1474
|
'working',
|
|
1467
1475
|
'qualified',
|
|
1468
1476
|
'unqualified'
|
|
1469
1477
|
];
|
|
1470
1478
|
/** How a lead status reads on screen — typed so a status cannot ship unlabeled. */ export const CRM_LEAD_STATUS_LABELS = {
|
|
1471
1479
|
new: 'New',
|
|
1480
|
+
nurturing: 'Nurturing',
|
|
1472
1481
|
working: 'Working',
|
|
1473
1482
|
qualified: 'Qualified',
|
|
1474
1483
|
unqualified: 'Unqualified'
|
|
@@ -1479,6 +1488,7 @@ export function parseCrmMemberRef(value) {
|
|
|
1479
1488
|
* the history.
|
|
1480
1489
|
*/ export const CRM_LEAD_OPEN_STATUSES = [
|
|
1481
1490
|
'new',
|
|
1491
|
+
'nurturing',
|
|
1482
1492
|
'working'
|
|
1483
1493
|
];
|
|
1484
1494
|
/**
|
|
@@ -1489,6 +1499,19 @@ export function parseCrmMemberRef(value) {
|
|
|
1489
1499
|
* written, so they can be counted, and what remains is open. See
|
|
1490
1500
|
* `openLeadsFromCounts`.
|
|
1491
1501
|
*/ export const CRM_LEAD_CLOSED_STATUSES = CRM_LEAD_STATUSES.filter((status)=>!CRM_LEAD_OPEN_STATUSES.includes(status));
|
|
1502
|
+
/**
|
|
1503
|
+
* The leads still needing somebody, from two server counts: every lead,
|
|
1504
|
+
* less the ones closed one way or the other.
|
|
1505
|
+
*
|
|
1506
|
+
* Subtraction rather than a count of the open statuses because a lead
|
|
1507
|
+
* nobody has touched carries NO status field — see `crmLeadStatus` — and
|
|
1508
|
+
* Firestore cannot select a document by a field's absence. The closed
|
|
1509
|
+
* statuses are always written, so they can be counted; what remains is
|
|
1510
|
+
* open. Clamped at zero for the moment between the two counts in which a
|
|
1511
|
+
* lead was closed.
|
|
1512
|
+
*/ export function openLeadsFromCounts(total, closed) {
|
|
1513
|
+
return Math.max(0, Math.round(Number(total) || 0) - Math.round(Number(closed) || 0));
|
|
1514
|
+
}
|
|
1492
1515
|
export function isCrmLeadStatus(value) {
|
|
1493
1516
|
return typeof value === 'string' && CRM_LEAD_STATUSES.includes(value);
|
|
1494
1517
|
}
|