@aglyn/plugins-outreach 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +93 -0
- package/package.json +52 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/compliance-section.d.ts +20 -0
- package/src/lib/components/compliance-section.js +291 -0
- package/src/lib/components/compliance-section.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +36 -0
- package/src/lib/components/enroll-dialog.js +656 -0
- package/src/lib/components/enroll-dialog.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +28 -0
- package/src/lib/components/enrollments-table.js +375 -0
- package/src/lib/components/enrollments-table.js.map +1 -0
- package/src/lib/components/mailbox-card.d.ts +46 -0
- package/src/lib/components/mailbox-card.js +517 -0
- package/src/lib/components/mailbox-card.js.map +1 -0
- package/src/lib/components/mailboxes-section.d.ts +24 -0
- package/src/lib/components/mailboxes-section.js +258 -0
- package/src/lib/components/mailboxes-section.js.map +1 -0
- package/src/lib/components/outreach-console-page.d.ts +17 -0
- package/src/lib/components/outreach-console-page.js +81 -0
- package/src/lib/components/outreach-console-page.js.map +1 -0
- package/src/lib/components/outreach-console-sections.d.ts +43 -0
- package/src/lib/components/outreach-console-sections.js +55 -0
- package/src/lib/components/outreach-console-sections.js.map +1 -0
- package/src/lib/components/outreach-ui.d.ts +51 -0
- package/src/lib/components/outreach-ui.js +171 -0
- package/src/lib/components/outreach-ui.js.map +1 -0
- package/src/lib/components/send-window-fields.d.ts +25 -0
- package/src/lib/components/send-window-fields.js +178 -0
- package/src/lib/components/send-window-fields.js.map +1 -0
- package/src/lib/components/sequence-detail.d.ts +54 -0
- package/src/lib/components/sequence-detail.js +435 -0
- package/src/lib/components/sequence-detail.js.map +1 -0
- package/src/lib/components/sequence-editor.d.ts +37 -0
- package/src/lib/components/sequence-editor.js +485 -0
- package/src/lib/components/sequence-editor.js.map +1 -0
- package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
- package/src/lib/components/sequence-mailbox-picker.js +137 -0
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
- package/src/lib/components/sequence-preview.d.ts +27 -0
- package/src/lib/components/sequence-preview.js +146 -0
- package/src/lib/components/sequence-preview.js.map +1 -0
- package/src/lib/components/sequence-step-card.d.ts +31 -0
- package/src/lib/components/sequence-step-card.js +326 -0
- package/src/lib/components/sequence-step-card.js.map +1 -0
- package/src/lib/components/sequences-section.d.ts +40 -0
- package/src/lib/components/sequences-section.js +310 -0
- package/src/lib/components/sequences-section.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +38 -0
- package/src/lib/components/use-outreach-api.js +139 -0
- package/src/lib/components/use-outreach-api.js.map +1 -0
- package/src/lib/components/use-outreach-crm.d.ts +50 -0
- package/src/lib/components/use-outreach-crm.js +200 -0
- package/src/lib/components/use-outreach-crm.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +55 -0
- package/src/lib/components/use-outreach-data.js +199 -0
- package/src/lib/components/use-outreach-data.js.map +1 -0
- package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
- package/src/lib/components/use-outreach-mailbox-api.js +101 -0
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
- package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
- package/src/lib/components/use-outreach-mailboxes.js +68 -0
- package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
- package/src/lib/components/use-outreach-settings.d.ts +17 -0
- package/src/lib/components/use-outreach-settings.js +64 -0
- package/src/lib/components/use-outreach-settings.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +68 -0
- package/src/lib/constants/api-routes.js +57 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +46 -0
- package/src/lib/constants/bundle-common.js +55 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/runtime-jobs.d.ts +24 -0
- package/src/lib/constants/runtime-jobs.js +24 -0
- package/src/lib/constants/runtime-jobs.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +99 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/engine/compose.d.ts +86 -0
- package/src/lib/engine/compose.js +212 -0
- package/src/lib/engine/compose.js.map +1 -0
- package/src/lib/engine/delivery-status.d.ts +53 -0
- package/src/lib/engine/delivery-status.js +184 -0
- package/src/lib/engine/delivery-status.js.map +1 -0
- package/src/lib/engine/do-not-contact.d.ts +18 -0
- package/src/lib/engine/do-not-contact.js +34 -0
- package/src/lib/engine/do-not-contact.js.map +1 -0
- package/src/lib/engine/enrollment-state.d.ts +156 -0
- package/src/lib/engine/enrollment-state.js +303 -0
- package/src/lib/engine/enrollment-state.js.map +1 -0
- package/src/lib/engine/gates.d.ts +125 -0
- package/src/lib/engine/gates.js +232 -0
- package/src/lib/engine/gates.js.map +1 -0
- package/src/lib/engine/index.d.ts +28 -0
- package/src/lib/engine/index.js +33 -0
- package/src/lib/engine/index.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +49 -0
- package/src/lib/engine/mailbox-health.js +83 -0
- package/src/lib/engine/mailbox-health.js.map +1 -0
- package/src/lib/engine/opt-out-intent.d.ts +41 -0
- package/src/lib/engine/opt-out-intent.js +220 -0
- package/src/lib/engine/opt-out-intent.js.map +1 -0
- package/src/lib/engine/recipient-country.d.ts +41 -0
- package/src/lib/engine/recipient-country.js +171 -0
- package/src/lib/engine/recipient-country.js.map +1 -0
- package/src/lib/engine/schedule.d.ts +70 -0
- package/src/lib/engine/schedule.js +115 -0
- package/src/lib/engine/schedule.js.map +1 -0
- package/src/lib/engine/sending-capacity.d.ts +96 -0
- package/src/lib/engine/sending-capacity.js +172 -0
- package/src/lib/engine/sending-capacity.js.map +1 -0
- package/src/lib/engine/sequence-validation.d.ts +90 -0
- package/src/lib/engine/sequence-validation.js +291 -0
- package/src/lib/engine/sequence-validation.js.map +1 -0
- package/src/lib/engine/thread-classification.d.ts +78 -0
- package/src/lib/engine/thread-classification.js +224 -0
- package/src/lib/engine/thread-classification.js.map +1 -0
- package/src/lib/engine/thread-message.d.ts +80 -0
- package/src/lib/engine/thread-message.js +143 -0
- package/src/lib/engine/thread-message.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +81 -0
- package/src/lib/enrollment/enroll-people.js +250 -0
- package/src/lib/enrollment/enroll-people.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.d.ts +32 -0
- package/src/lib/enrollment/gate-lookups.js +154 -0
- package/src/lib/enrollment/gate-lookups.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
- package/src/lib/mailboxes/mailbox-api.js +66 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -0
- package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
- package/src/lib/mailboxes/mailbox-credentials.js +102 -0
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
- package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
- package/src/lib/mailboxes/mailbox-erasure.js +148 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
- package/src/lib/mailboxes/mailbox-gate.js +98 -0
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
- package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
- package/src/lib/mailboxes/mailbox-revoke.js +41 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
- package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
- package/src/lib/mailboxes/mailbox-routes.js +605 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
- package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
- package/src/lib/mailboxes/mailbox-settings.js +212 -0
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
- package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
- package/src/lib/mailboxes/mailbox-transport.js +94 -0
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
- package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
- package/src/lib/mailboxes/oauth-redirect.js +63 -0
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
- package/src/lib/mailboxes/oauth-state.d.ts +152 -0
- package/src/lib/mailboxes/oauth-state.js +212 -0
- package/src/lib/mailboxes/oauth-state.js.map +1 -0
- package/src/lib/mailboxes/outreach-config.d.ts +64 -0
- package/src/lib/mailboxes/outreach-config.js +77 -0
- package/src/lib/mailboxes/outreach-config.js.map +1 -0
- package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
- package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
- package/src/lib/model/compliance-settings.d.ts +78 -0
- package/src/lib/model/compliance-settings.js +155 -0
- package/src/lib/model/compliance-settings.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +210 -0
- package/src/lib/model/outreach-api.js +28 -0
- package/src/lib/model/outreach-api.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +563 -0
- package/src/lib/model/outreach.types.js +225 -0
- package/src/lib/model/outreach.types.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +84 -0
- package/src/lib/model/sequence-draft.js +239 -0
- package/src/lib/model/sequence-draft.js.map +1 -0
- package/src/lib/model/step-preview.d.ts +45 -0
- package/src/lib/model/step-preview.js +98 -0
- package/src/lib/model/step-preview.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +23 -0
- package/src/lib/model/stored-records.js +84 -0
- package/src/lib/model/stored-records.js.map +1 -0
- package/src/lib/plugin.d.ts +46 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +64 -0
- package/src/lib/routes/enroll-routes.js +350 -0
- package/src/lib/routes/enroll-routes.js.map +1 -0
- package/src/lib/routes/enrollment-routes.d.ts +19 -0
- package/src/lib/routes/enrollment-routes.js +171 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -0
- package/src/lib/routes/platform-deps.d.ts +51 -0
- package/src/lib/routes/platform-deps.js +66 -0
- package/src/lib/routes/platform-deps.js.map +1 -0
- package/src/lib/routes/preview-routes.d.ts +33 -0
- package/src/lib/routes/preview-routes.js +117 -0
- package/src/lib/routes/preview-routes.js.map +1 -0
- package/src/lib/routes/register-routes.d.ts +22 -0
- package/src/lib/routes/register-routes.js +92 -0
- package/src/lib/routes/register-routes.js.map +1 -0
- package/src/lib/routes/route-deps.d.ts +49 -0
- package/src/lib/routes/route-deps.js +27 -0
- package/src/lib/routes/route-deps.js.map +1 -0
- package/src/lib/routes/route-gate.d.ts +94 -0
- package/src/lib/routes/route-gate.js +87 -0
- package/src/lib/routes/route-gate.js.map +1 -0
- package/src/lib/routes/route-http.d.ts +32 -0
- package/src/lib/routes/route-http.js +62 -0
- package/src/lib/routes/route-http.js.map +1 -0
- package/src/lib/routes/sequence-routes.d.ts +32 -0
- package/src/lib/routes/sequence-routes.js +359 -0
- package/src/lib/routes/sequence-routes.js.map +1 -0
- package/src/lib/routes/settings-routes.d.ts +30 -0
- package/src/lib/routes/settings-routes.js +73 -0
- package/src/lib/routes/settings-routes.js.map +1 -0
- package/src/lib/runtime/enrollment-events.d.ts +86 -0
- package/src/lib/runtime/enrollment-events.js +141 -0
- package/src/lib/runtime/enrollment-events.js.map +1 -0
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
- package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
- package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
- package/src/lib/runtime/mailbox-health-store.js +168 -0
- package/src/lib/runtime/mailbox-health-store.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +39 -0
- package/src/lib/runtime/person-erasure.js +57 -0
- package/src/lib/runtime/person-erasure.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
- package/src/lib/runtime/platform-runtime-deps.js +98 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
- package/src/lib/runtime/runtime-deps.d.ts +77 -0
- package/src/lib/runtime/runtime-deps.js +18 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -0
- package/src/lib/runtime/send-job.d.ts +42 -0
- package/src/lib/runtime/send-job.js +780 -0
- package/src/lib/runtime/send-job.js.map +1 -0
- package/src/lib/runtime/sync-job.d.ts +41 -0
- package/src/lib/runtime/sync-job.js +480 -0
- package/src/lib/runtime/sync-job.js.map +1 -0
- package/src/lib/runtime/timeline.d.ts +67 -0
- package/src/lib/runtime/timeline.js +93 -0
- package/src/lib/runtime/timeline.js.map +1 -0
- package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
- package/src/lib/runtime/unsubscribe-link.js +121 -0
- package/src/lib/runtime/unsubscribe-link.js.map +1 -0
- package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
- package/src/lib/runtime/unsubscribe-route.js +103 -0
- package/src/lib/runtime/unsubscribe-route.js.map +1 -0
- package/src/lib/server.d.ts +53 -0
- package/src/lib/server.js +94 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/storage/compliance-settings-store.d.ts +44 -0
- package/src/lib/storage/compliance-settings-store.js +78 -0
- package/src/lib/storage/compliance-settings-store.js.map +1 -0
- package/src/lib/storage/do-not-contact-store.d.ts +60 -0
- package/src/lib/storage/do-not-contact-store.js +168 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -0
- package/src/lib/storage/outreach-records.d.ts +33 -0
- package/src/lib/storage/outreach-records.js +35 -0
- package/src/lib/storage/outreach-records.js.map +1 -0
- package/src/lib/subprocessors.d.ts +48 -0
- package/src/lib/subprocessors.js +70 -0
- package/src/lib/subprocessors.js.map +1 -0
- package/src/lib/transport/gmail-client.d.ts +145 -0
- package/src/lib/transport/gmail-client.js +229 -0
- package/src/lib/transport/gmail-client.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +84 -0
- package/src/lib/transport/gmail-errors.js +138 -0
- package/src/lib/transport/gmail-errors.js.map +1 -0
- package/src/lib/transport/google-oauth.d.ts +138 -0
- package/src/lib/transport/google-oauth.js +247 -0
- package/src/lib/transport/google-oauth.js.map +1 -0
- package/src/lib/transport/http.d.ts +53 -0
- package/src/lib/transport/http.js +79 -0
- package/src/lib/transport/http.js.map +1 -0
- package/src/lib/transport/rfc5322.d.ts +116 -0
- package/src/lib/transport/rfc5322.js +330 -0
- package/src/lib/transport/rfc5322.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +69 -0
- package/src/lib/transport/send-message.js +77 -0
- package/src/lib/transport/send-message.js.map +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { contactDisplayName, normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
19
|
+
import { CRM_COLLECTIONS, crmViewIsListed, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
20
|
+
import { CRM_EMAIL_TEMPLATES_LIMIT, crmEmailTemplateIsListed, normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
21
|
+
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
22
|
+
import { nameSearchToken } from "@aglyn/aglyn/app-utils/name-search";
|
|
23
|
+
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
24
|
+
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
25
|
+
import { collection, getDocs, limit, onSnapshot, orderBy, query, where } from "firebase/firestore";
|
|
26
|
+
import { useEffect, useState } from "react";
|
|
27
|
+
/**
|
|
28
|
+
* What Outreach reads from the organization's CRM (AGL-2980): the saved
|
|
29
|
+
* Contacts views people can be enrolled from, the email templates a step
|
|
30
|
+
* can send, and the contacts a search finds.
|
|
31
|
+
*
|
|
32
|
+
* Through the core's own CRM model — the collection names, the listing
|
|
33
|
+
* rules, the view translation the dynamic-list sweep uses — and never
|
|
34
|
+
* through the CRM plugin, which a plugin may not import. The Firestore
|
|
35
|
+
* rules gate every one of these reads on `data.manage`, the same permission
|
|
36
|
+
* the enroll routes ask for, so a member who cannot read the people cannot
|
|
37
|
+
* enroll them either.
|
|
38
|
+
*/ const denied = (error)=>(error == null ? void 0 : error.code) === 'permission-denied';
|
|
39
|
+
/** The most saved views the picker lists. */ export const OUTREACH_VIEWS_LIMIT = 50;
|
|
40
|
+
/**
|
|
41
|
+
* The saved Contacts views this reader may enroll from: their own and the
|
|
42
|
+
* shared ones — a colleague's private view is theirs — that translate whole
|
|
43
|
+
* into an audience. A view filtering on something enrolling cannot apply is
|
|
44
|
+
* not offered, because dropping that filter would enroll more people than
|
|
45
|
+
* the view shows.
|
|
46
|
+
*/ export function useOutreachSavedViews(orgId, uid) {
|
|
47
|
+
const firestore = useFirestore();
|
|
48
|
+
const [result, setResult] = useState({
|
|
49
|
+
status: 'loading',
|
|
50
|
+
data: []
|
|
51
|
+
});
|
|
52
|
+
useEffect(()=>{
|
|
53
|
+
setResult({
|
|
54
|
+
status: 'loading',
|
|
55
|
+
data: []
|
|
56
|
+
});
|
|
57
|
+
if (!orgId) return undefined;
|
|
58
|
+
return onSnapshot(query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.views), where('section', '==', 'contacts'), orderBy('name'), limit(OUTREACH_VIEWS_LIMIT)), (snapshot)=>setResult({
|
|
59
|
+
status: 'ready',
|
|
60
|
+
data: snapshot.docs.map((entry)=>({
|
|
61
|
+
id: entry.id,
|
|
62
|
+
data: entry.data()
|
|
63
|
+
})).filter(({ data })=>{
|
|
64
|
+
var _data_ownerUid;
|
|
65
|
+
return typeof data['name'] === 'string' && data['name'] !== '' && crmViewIsListed({
|
|
66
|
+
shared: data['shared'] === true,
|
|
67
|
+
ownerUid: String((_data_ownerUid = data['ownerUid']) != null ? _data_ownerUid : '')
|
|
68
|
+
}, uid) && dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters'])).unsupported.length === 0;
|
|
69
|
+
}).map(({ id, data })=>({
|
|
70
|
+
id,
|
|
71
|
+
name: String(data['name'])
|
|
72
|
+
}))
|
|
73
|
+
}), (error)=>{
|
|
74
|
+
if (!denied(error)) console.error('[outreach] saved views could not be read', error);
|
|
75
|
+
setResult({
|
|
76
|
+
status: denied(error) ? 'refused' : 'error',
|
|
77
|
+
data: []
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}, [
|
|
81
|
+
firestore,
|
|
82
|
+
orgId,
|
|
83
|
+
uid
|
|
84
|
+
]);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The CRM email templates a step at this site can use: whole letters, not
|
|
89
|
+
* snippets; the team's and this reader's own; written for the whole
|
|
90
|
+
* organization or for this site.
|
|
91
|
+
*/ export function useOutreachEmailTemplates(orgId, hostId, uid) {
|
|
92
|
+
const firestore = useFirestore();
|
|
93
|
+
const [result, setResult] = useState({
|
|
94
|
+
status: 'loading',
|
|
95
|
+
data: []
|
|
96
|
+
});
|
|
97
|
+
useEffect(()=>{
|
|
98
|
+
setResult({
|
|
99
|
+
status: 'loading',
|
|
100
|
+
data: []
|
|
101
|
+
});
|
|
102
|
+
if (!orgId) return undefined;
|
|
103
|
+
return onSnapshot(query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.emailTemplates), orderBy('name'), limit(CRM_EMAIL_TEMPLATES_LIMIT)), (snapshot)=>setResult({
|
|
104
|
+
status: 'ready',
|
|
105
|
+
data: snapshot.docs.map((entry)=>({
|
|
106
|
+
id: entry.id,
|
|
107
|
+
template: normalizeCrmEmailTemplate(entry.data())
|
|
108
|
+
})).filter(({ template })=>template.kind === 'template' && crmEmailTemplateIsListed(template, uid) && (!hostId || visibleToHost(template.visibleTo, hostId))).map(({ id, template })=>({
|
|
109
|
+
id,
|
|
110
|
+
name: template.name,
|
|
111
|
+
subject: template.subject,
|
|
112
|
+
body: template.body
|
|
113
|
+
}))
|
|
114
|
+
}), (error)=>{
|
|
115
|
+
if (!denied(error)) console.error('[outreach] email templates could not be read', error);
|
|
116
|
+
setResult({
|
|
117
|
+
status: denied(error) ? 'refused' : 'error',
|
|
118
|
+
data: []
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}, [
|
|
122
|
+
firestore,
|
|
123
|
+
orgId,
|
|
124
|
+
hostId,
|
|
125
|
+
uid
|
|
126
|
+
]);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
/** The most contacts one search answers with. */ export const OUTREACH_SEARCH_LIMIT = 25;
|
|
130
|
+
/**
|
|
131
|
+
* The contacts at `hostId` a search finds: by address when the text is one,
|
|
132
|
+
* otherwise by a word of the name — the `nameTokens` the contacts carry for
|
|
133
|
+
* the CRM's own search. `idle` until something is typed.
|
|
134
|
+
*/ export function useOutreachContactSearch(input) {
|
|
135
|
+
const { orgId, hostId, contactGroupId, text } = input;
|
|
136
|
+
const firestore = useFirestore();
|
|
137
|
+
const [result, setResult] = useState({
|
|
138
|
+
status: 'ready',
|
|
139
|
+
data: []
|
|
140
|
+
});
|
|
141
|
+
const email = normalizeContactEmail(text);
|
|
142
|
+
const token = email ? '' : nameSearchToken(text);
|
|
143
|
+
const idle = !orgId || !hostId || !email && !token;
|
|
144
|
+
useEffect(()=>{
|
|
145
|
+
if (idle || !orgId || !hostId) {
|
|
146
|
+
setResult({
|
|
147
|
+
status: 'ready',
|
|
148
|
+
data: []
|
|
149
|
+
});
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
let current = true;
|
|
153
|
+
setResult((previous)=>({
|
|
154
|
+
status: 'loading',
|
|
155
|
+
data: previous.data
|
|
156
|
+
}));
|
|
157
|
+
const contacts = collection(firestore, 'orgs', orgId, 'contacts');
|
|
158
|
+
// Settles the typing before it reads: one query per pause, not per key.
|
|
159
|
+
const timer = setTimeout(()=>{
|
|
160
|
+
getDocs(email ? query(contacts, where('email', '==', email), limit(OUTREACH_SEARCH_LIMIT)) : query(contacts, where('nameTokens', 'array-contains', token), orderBy('nameLower'), limit(OUTREACH_SEARCH_LIMIT))).then((snapshot)=>{
|
|
161
|
+
if (!current) return;
|
|
162
|
+
setResult({
|
|
163
|
+
status: 'ready',
|
|
164
|
+
data: snapshot.docs.map((entry)=>({
|
|
165
|
+
id: entry.id,
|
|
166
|
+
data: entry.data()
|
|
167
|
+
})).filter(({ data })=>visibleToHost(data['visibleTo'], hostId)).map(({ id, data })=>({
|
|
168
|
+
id,
|
|
169
|
+
name: contactDisplayName(data, contactGroupId != null ? contactGroupId : hostId).trim(),
|
|
170
|
+
email: normalizeContactEmail(data['email'])
|
|
171
|
+
}))
|
|
172
|
+
});
|
|
173
|
+
}).catch((error)=>{
|
|
174
|
+
if (!current) return;
|
|
175
|
+
if (!denied(error)) console.error('[outreach] the contact search failed', error);
|
|
176
|
+
setResult({
|
|
177
|
+
status: denied(error) ? 'refused' : 'error',
|
|
178
|
+
data: []
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}, 300);
|
|
182
|
+
return ()=>{
|
|
183
|
+
current = false;
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
};
|
|
186
|
+
}, [
|
|
187
|
+
firestore,
|
|
188
|
+
orgId,
|
|
189
|
+
hostId,
|
|
190
|
+
contactGroupId,
|
|
191
|
+
email,
|
|
192
|
+
token,
|
|
193
|
+
idle
|
|
194
|
+
]);
|
|
195
|
+
return _extends({}, result, {
|
|
196
|
+
idle
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=use-outreach-crm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-crm.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'use client'\n\nimport {\n contactDisplayName,\n normalizeContactEmail,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CRM_COLLECTIONS,\n crmViewIsListed,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CRM_EMAIL_TEMPLATES_LIMIT,\n crmEmailTemplateIsListed,\n normalizeCrmEmailTemplate,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport { nameSearchToken } from '@aglyn/aglyn/app-utils/name-search'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport type { OutreachLoad } from './use-outreach-data'\n\n/**\n * What Outreach reads from the organization's CRM (AGL-2980): the saved\n * Contacts views people can be enrolled from, the email templates a step\n * can send, and the contacts a search finds.\n *\n * Through the core's own CRM model — the collection names, the listing\n * rules, the view translation the dynamic-list sweep uses — and never\n * through the CRM plugin, which a plugin may not import. The Firestore\n * rules gate every one of these reads on `data.manage`, the same permission\n * the enroll routes ask for, so a member who cannot read the people cannot\n * enroll them either.\n */\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\n/** A saved Contacts view people can be enrolled from. */\nexport interface OutreachViewOption {\n id: string\n name: string\n}\n\n/** The most saved views the picker lists. */\nexport const OUTREACH_VIEWS_LIMIT = 50\n\n/**\n * The saved Contacts views this reader may enroll from: their own and the\n * shared ones — a colleague's private view is theirs — that translate whole\n * into an audience. A view filtering on something enrolling cannot apply is\n * not offered, because dropping that filter would enroll more people than\n * the view shows.\n */\nexport function useOutreachSavedViews(\n orgId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachViewOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachViewOption[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.views),\n where('section', '==', 'contacts'),\n orderBy('name'),\n limit(OUTREACH_VIEWS_LIMIT),\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n data: entry.data() as Record<string, unknown>,\n }))\n .filter(\n ({ data }) =>\n typeof data['name'] === 'string' &&\n data['name'] !== '' &&\n crmViewIsListed(\n {\n shared: data['shared'] === true,\n ownerUid: String(data['ownerUid'] ?? ''),\n },\n uid,\n ) &&\n dynamicListDimensionsForCrmView(\n normalizeCrmViewFilters(data['filters']),\n ).unsupported.length === 0,\n )\n .map(({ id, data }) => ({ id, name: String(data['name']) })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] saved views could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, uid])\n return result\n}\n\n/** A CRM email template a step can send the body of. */\nexport interface OutreachTemplateOption {\n id: string\n name: string\n subject: string\n body: string\n}\n\n/**\n * The CRM email templates a step at this site can use: whole letters, not\n * snippets; the team's and this reader's own; written for the whole\n * organization or for this site.\n */\nexport function useOutreachEmailTemplates(\n orgId: string | null,\n hostId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachTemplateOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachTemplateOption[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.emailTemplates),\n orderBy('name'),\n limit(CRM_EMAIL_TEMPLATES_LIMIT),\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n template: normalizeCrmEmailTemplate(entry.data()),\n }))\n .filter(\n ({ template }) =>\n template.kind === 'template' &&\n crmEmailTemplateIsListed(template, uid) &&\n (!hostId || visibleToHost(template.visibleTo, hostId)),\n )\n .map(({ id, template }) => ({\n id,\n name: template.name,\n subject: template.subject,\n body: template.body,\n })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] email templates could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, hostId, uid])\n return result\n}\n\n/** A contact a search found. */\nexport interface OutreachContactOption {\n id: string\n name: string\n email: string | null\n}\n\n/** The most contacts one search answers with. */\nexport const OUTREACH_SEARCH_LIMIT = 25\n\n/**\n * The contacts at `hostId` a search finds: by address when the text is one,\n * otherwise by a word of the name — the `nameTokens` the contacts carry for\n * the CRM's own search. `idle` until something is typed.\n */\nexport function useOutreachContactSearch(input: {\n orgId: string | null\n hostId: string | null\n contactGroupId: string | null\n text: string\n}): OutreachLoad<OutreachContactOption[]> & { idle: boolean } {\n const { orgId, hostId, contactGroupId, text } = input\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachContactOption[]>>({\n status: 'ready',\n data: [],\n })\n const email = normalizeContactEmail(text)\n const token = email ? '' : nameSearchToken(text)\n const idle = !orgId || !hostId || (!email && !token)\n useEffect(() => {\n if (idle || !orgId || !hostId) {\n setResult({ status: 'ready', data: [] })\n return undefined\n }\n let current = true\n setResult((previous) => ({ status: 'loading', data: previous.data }))\n const contacts = collection(firestore, 'orgs', orgId, 'contacts')\n // Settles the typing before it reads: one query per pause, not per key.\n const timer = setTimeout(() => {\n getDocs(\n email\n ? query(\n contacts,\n where('email', '==', email),\n limit(OUTREACH_SEARCH_LIMIT),\n )\n : query(\n contacts,\n where('nameTokens', 'array-contains', token),\n orderBy('nameLower'),\n limit(OUTREACH_SEARCH_LIMIT),\n ),\n )\n .then((snapshot) => {\n if (!current) return\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n data: entry.data() as Record<string, unknown>,\n }))\n .filter(({ data }) =>\n visibleToHost(\n data['visibleTo'] as string[] | undefined,\n hostId,\n ),\n )\n .map(({ id, data }) => ({\n id,\n name: contactDisplayName(data, contactGroupId ?? hostId).trim(),\n email: normalizeContactEmail(data['email']),\n })),\n })\n })\n .catch((error: unknown) => {\n if (!current) return\n if (!denied(error))\n console.error('[outreach] the contact search failed', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n })\n }, 300)\n return () => {\n current = false\n clearTimeout(timer)\n }\n }, [firestore, orgId, hostId, contactGroupId, email, token, idle])\n return { ...result, idle }\n}\n"],"names":["contactDisplayName","normalizeContactEmail","CRM_COLLECTIONS","crmViewIsListed","normalizeCrmViewFilters","CRM_EMAIL_TEMPLATES_LIMIT","crmEmailTemplateIsListed","normalizeCrmEmailTemplate","dynamicListDimensionsForCrmView","nameSearchToken","visibleToHost","useFirestore","collection","getDocs","limit","onSnapshot","orderBy","query","where","useEffect","useState","denied","error","code","OUTREACH_VIEWS_LIMIT","useOutreachSavedViews","orgId","uid","firestore","result","setResult","status","data","undefined","views","snapshot","docs","map","entry","id","filter","shared","ownerUid","String","unsupported","length","name","console","useOutreachEmailTemplates","hostId","emailTemplates","template","kind","visibleTo","subject","body","OUTREACH_SEARCH_LIMIT","useOutreachContactSearch","input","contactGroupId","text","email","token","idle","current","previous","contacts","timer","setTimeout","then","trim","catch","clearTimeout"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
|
|
2
|
+
/**
|
|
3
|
+
* The Outreach documents the console reads LIVE (AGL-2980): sequences,
|
|
4
|
+
* enrollments, and the mailboxes they send from.
|
|
5
|
+
*
|
|
6
|
+
* Read straight from Firestore — the rules let an org-wide member holding
|
|
7
|
+
* `outreach.use` on an entitled organization read them, and every write goes
|
|
8
|
+
* through a route — so what a page shows is what the routes and the sending
|
|
9
|
+
* runtime wrote, the moment it lands.
|
|
10
|
+
*
|
|
11
|
+
* Every hook answers one of four states, and a page renders each: still
|
|
12
|
+
* loading, ready, failed, or REFUSED — the rules said no, which is a reader
|
|
13
|
+
* who may not see Outreach here rather than something that went wrong.
|
|
14
|
+
*/
|
|
15
|
+
export type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused';
|
|
16
|
+
export interface OutreachLoad<T> {
|
|
17
|
+
status: OutreachLoadStatus;
|
|
18
|
+
data: T;
|
|
19
|
+
}
|
|
20
|
+
/** The most sequences the list reads. */
|
|
21
|
+
export declare const OUTREACH_SEQUENCES_LIMIT = 200;
|
|
22
|
+
/** The organization's sequences, newest first. */
|
|
23
|
+
export declare function useOutreachSequences(orgId: string | null): OutreachLoad<OutreachSequence[]>;
|
|
24
|
+
/** One sequence, or `null` once it is known not to exist. */
|
|
25
|
+
export declare function useOutreachSequence(orgId: string | null, sequenceId: string | null): OutreachLoad<OutreachSequence | null>;
|
|
26
|
+
/** How many enrollments one page of the table reads; "Show more" reads another. */
|
|
27
|
+
export declare const OUTREACH_ENROLLMENTS_PAGE = 200;
|
|
28
|
+
export interface OutreachEnrollmentsLoad extends OutreachLoad<OutreachEnrollment[]> {
|
|
29
|
+
/** Whether a further page may exist. */
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
showMore(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A sequence's enrollments, the most recently enrolled first.
|
|
35
|
+
*
|
|
36
|
+
* Paged by document id — an equality on `sequenceId` in the id's own order
|
|
37
|
+
* needs no composite index — and sorted by enrollment time once read, so a
|
|
38
|
+
* table of up to a page reads in the order people were enrolled.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useOutreachEnrollments(orgId: string | null, sequenceId: string | null): OutreachEnrollmentsLoad;
|
|
41
|
+
/** The counts the sequence list shows beside each sequence. */
|
|
42
|
+
export interface OutreachSequenceCounts {
|
|
43
|
+
enrolled: number;
|
|
44
|
+
active: number;
|
|
45
|
+
replied: number;
|
|
46
|
+
bounced: number;
|
|
47
|
+
optedOut: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Each sequence's enrollment counts, by server-side aggregation — one count
|
|
51
|
+
* per figure, never a read of the enrollments themselves. Recounted when the
|
|
52
|
+
* sequences asked about change — the list asks about the page on screen —
|
|
53
|
+
* and absent while counting, or if a count failed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useOutreachSequenceCounts(orgId: string | null, sequenceIds: readonly string[]): Record<string, OutreachSequenceCounts>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
19
|
+
import { collection, doc, documentId, getCountFromServer, limit, onSnapshot, orderBy, query, where } from "firebase/firestore";
|
|
20
|
+
import { useEffect, useMemo, useState } from "react";
|
|
21
|
+
import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
|
|
22
|
+
import { readStoredOutreachEnrollment, readStoredOutreachSequence } from "../model/stored-records.js";
|
|
23
|
+
const denied = (error)=>(error == null ? void 0 : error.code) === 'permission-denied';
|
|
24
|
+
function failed(data, error, what) {
|
|
25
|
+
if (denied(error)) return {
|
|
26
|
+
status: 'refused',
|
|
27
|
+
data
|
|
28
|
+
};
|
|
29
|
+
console.error(`[outreach] ${what} could not be read`, error);
|
|
30
|
+
return {
|
|
31
|
+
status: 'error',
|
|
32
|
+
data
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The most sequences the list reads. */ export const OUTREACH_SEQUENCES_LIMIT = 200;
|
|
36
|
+
/** The organization's sequences, newest first. */ export function useOutreachSequences(orgId) {
|
|
37
|
+
const firestore = useFirestore();
|
|
38
|
+
const [result, setResult] = useState({
|
|
39
|
+
status: 'loading',
|
|
40
|
+
data: []
|
|
41
|
+
});
|
|
42
|
+
useEffect(()=>{
|
|
43
|
+
setResult({
|
|
44
|
+
status: 'loading',
|
|
45
|
+
data: []
|
|
46
|
+
});
|
|
47
|
+
if (!orgId) return undefined;
|
|
48
|
+
return onSnapshot(query(collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences), // Ordered, so the cap keeps the newest rather than an arbitrary set;
|
|
49
|
+
// every sequence carries `createdAtMs`, stamped by the save route.
|
|
50
|
+
orderBy('createdAtMs', 'desc'), limit(OUTREACH_SEQUENCES_LIMIT)), (snapshot)=>{
|
|
51
|
+
const sequences = snapshot.docs.map((entry)=>readStoredOutreachSequence(entry.id, entry.data())).filter((sequence)=>sequence !== null).sort((a, b)=>b.createdAtMs - a.createdAtMs);
|
|
52
|
+
setResult({
|
|
53
|
+
status: 'ready',
|
|
54
|
+
data: sequences
|
|
55
|
+
});
|
|
56
|
+
}, (error)=>setResult(failed([], error, 'sequences')));
|
|
57
|
+
}, [
|
|
58
|
+
firestore,
|
|
59
|
+
orgId
|
|
60
|
+
]);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/** One sequence, or `null` once it is known not to exist. */ export function useOutreachSequence(orgId, sequenceId) {
|
|
64
|
+
const firestore = useFirestore();
|
|
65
|
+
const [result, setResult] = useState({
|
|
66
|
+
status: 'loading',
|
|
67
|
+
data: null
|
|
68
|
+
});
|
|
69
|
+
useEffect(()=>{
|
|
70
|
+
setResult({
|
|
71
|
+
status: 'loading',
|
|
72
|
+
data: null
|
|
73
|
+
});
|
|
74
|
+
if (!orgId || !sequenceId) return undefined;
|
|
75
|
+
return onSnapshot(doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId), (snapshot)=>setResult({
|
|
76
|
+
status: 'ready',
|
|
77
|
+
data: readStoredOutreachSequence(snapshot.id, snapshot.exists() ? snapshot.data() : undefined)
|
|
78
|
+
}), (error)=>setResult(failed(null, error, 'the sequence')));
|
|
79
|
+
}, [
|
|
80
|
+
firestore,
|
|
81
|
+
orgId,
|
|
82
|
+
sequenceId
|
|
83
|
+
]);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/** How many enrollments one page of the table reads; "Show more" reads another. */ export const OUTREACH_ENROLLMENTS_PAGE = 200;
|
|
87
|
+
/**
|
|
88
|
+
* A sequence's enrollments, the most recently enrolled first.
|
|
89
|
+
*
|
|
90
|
+
* Paged by document id — an equality on `sequenceId` in the id's own order
|
|
91
|
+
* needs no composite index — and sorted by enrollment time once read, so a
|
|
92
|
+
* table of up to a page reads in the order people were enrolled.
|
|
93
|
+
*/ export function useOutreachEnrollments(orgId, sequenceId) {
|
|
94
|
+
const firestore = useFirestore();
|
|
95
|
+
const [pages, setPages] = useState(1);
|
|
96
|
+
const [result, setResult] = useState({
|
|
97
|
+
status: 'loading',
|
|
98
|
+
data: [],
|
|
99
|
+
size: 0
|
|
100
|
+
});
|
|
101
|
+
useEffect(()=>setPages(1), [
|
|
102
|
+
orgId,
|
|
103
|
+
sequenceId
|
|
104
|
+
]);
|
|
105
|
+
useEffect(()=>{
|
|
106
|
+
if (!orgId || !sequenceId) {
|
|
107
|
+
setResult({
|
|
108
|
+
status: 'loading',
|
|
109
|
+
data: [],
|
|
110
|
+
size: 0
|
|
111
|
+
});
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return onSnapshot(query(collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments), where('sequenceId', '==', sequenceId), orderBy(documentId()), limit(pages * OUTREACH_ENROLLMENTS_PAGE)), (snapshot)=>{
|
|
115
|
+
const enrollments = snapshot.docs.map((entry)=>readStoredOutreachEnrollment(entry.id, entry.data())).filter((enrollment)=>enrollment !== null).sort((a, b)=>b.createdAtMs - a.createdAtMs);
|
|
116
|
+
setResult({
|
|
117
|
+
status: 'ready',
|
|
118
|
+
data: enrollments,
|
|
119
|
+
size: snapshot.size
|
|
120
|
+
});
|
|
121
|
+
}, (error)=>setResult(_extends({}, failed([], error, 'the enrollments'), {
|
|
122
|
+
size: 0
|
|
123
|
+
})));
|
|
124
|
+
}, [
|
|
125
|
+
firestore,
|
|
126
|
+
orgId,
|
|
127
|
+
sequenceId,
|
|
128
|
+
pages
|
|
129
|
+
]);
|
|
130
|
+
return useMemo(()=>({
|
|
131
|
+
status: result.status,
|
|
132
|
+
data: result.data,
|
|
133
|
+
hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,
|
|
134
|
+
showMore: ()=>setPages((count)=>count + 1)
|
|
135
|
+
}), [
|
|
136
|
+
result,
|
|
137
|
+
pages
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
const COUNTED = [
|
|
141
|
+
[
|
|
142
|
+
'enrolled',
|
|
143
|
+
null
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
'active',
|
|
147
|
+
'active'
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
'replied',
|
|
151
|
+
'replied'
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
'bounced',
|
|
155
|
+
'bounced'
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
'optedOut',
|
|
159
|
+
'opted_out'
|
|
160
|
+
]
|
|
161
|
+
];
|
|
162
|
+
/**
|
|
163
|
+
* Each sequence's enrollment counts, by server-side aggregation — one count
|
|
164
|
+
* per figure, never a read of the enrollments themselves. Recounted when the
|
|
165
|
+
* sequences asked about change — the list asks about the page on screen —
|
|
166
|
+
* and absent while counting, or if a count failed.
|
|
167
|
+
*/ export function useOutreachSequenceCounts(orgId, sequenceIds) {
|
|
168
|
+
const firestore = useFirestore();
|
|
169
|
+
const [counts, setCounts] = useState({});
|
|
170
|
+
const key = sequenceIds.join(',');
|
|
171
|
+
useEffect(()=>{
|
|
172
|
+
if (!orgId || !key) return undefined;
|
|
173
|
+
let current = true;
|
|
174
|
+
const enrollments = collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments);
|
|
175
|
+
Promise.all(key.split(',').map(async (sequenceId)=>{
|
|
176
|
+
const figures = await Promise.all(COUNTED.map(async ([, status])=>{
|
|
177
|
+
const counted = await getCountFromServer(status ? query(enrollments, where('sequenceId', '==', sequenceId), where('status', '==', status)) : query(enrollments, where('sequenceId', '==', sequenceId)));
|
|
178
|
+
return counted.data().count;
|
|
179
|
+
}));
|
|
180
|
+
return [
|
|
181
|
+
sequenceId,
|
|
182
|
+
Object.fromEntries(COUNTED.map(([name], index)=>[
|
|
183
|
+
name,
|
|
184
|
+
figures[index]
|
|
185
|
+
]))
|
|
186
|
+
];
|
|
187
|
+
})).then((entries)=>current && setCounts(Object.fromEntries(entries))).catch((error)=>console.error('[outreach] enrollment counts could not be read', error));
|
|
188
|
+
return ()=>{
|
|
189
|
+
current = false;
|
|
190
|
+
};
|
|
191
|
+
}, [
|
|
192
|
+
firestore,
|
|
193
|
+
orgId,
|
|
194
|
+
key
|
|
195
|
+
]);
|
|
196
|
+
return counts;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=use-outreach-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-data.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'use client'\n\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n readStoredOutreachEnrollment,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/**\n * The Outreach documents the console reads LIVE (AGL-2980): sequences,\n * enrollments, and the mailboxes they send from.\n *\n * Read straight from Firestore — the rules let an org-wide member holding\n * `outreach.use` on an entitled organization read them, and every write goes\n * through a route — so what a page shows is what the routes and the sending\n * runtime wrote, the moment it lands.\n *\n * Every hook answers one of four states, and a page renders each: still\n * loading, ready, failed, or REFUSED — the rules said no, which is a reader\n * who may not see Outreach here rather than something that went wrong.\n */\n\nexport type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused'\n\nexport interface OutreachLoad<T> {\n status: OutreachLoadStatus\n data: T\n}\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\nfunction failed<T>(data: T, error: unknown, what: string): OutreachLoad<T> {\n if (denied(error)) return { status: 'refused', data }\n console.error(`[outreach] ${what} could not be read`, error)\n return { status: 'error', data }\n}\n\n/** The most sequences the list reads. */\nexport const OUTREACH_SEQUENCES_LIMIT = 200\n\n/** The organization's sequences, newest first. */\nexport function useOutreachSequences(\n orgId: string | null,\n): OutreachLoad<OutreachSequence[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences),\n // Ordered, so the cap keeps the newest rather than an arbitrary set;\n // every sequence carries `createdAtMs`, stamped by the save route.\n orderBy('createdAtMs', 'desc'),\n limit(OUTREACH_SEQUENCES_LIMIT),\n ),\n (snapshot) => {\n const sequences = snapshot.docs\n .map((entry) => readStoredOutreachSequence(entry.id, entry.data()))\n .filter((sequence): sequence is OutreachSequence => sequence !== null)\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: sequences })\n },\n (error) => setResult(failed([], error, 'sequences')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** One sequence, or `null` once it is known not to exist. */\nexport function useOutreachSequence(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachLoad<OutreachSequence | null> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence | null>>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: readStoredOutreachSequence(\n snapshot.id,\n snapshot.exists() ? snapshot.data() : undefined,\n ),\n }),\n (error) => setResult(failed(null, error, 'the sequence')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/** How many enrollments one page of the table reads; \"Show more\" reads another. */\nexport const OUTREACH_ENROLLMENTS_PAGE = 200\n\nexport interface OutreachEnrollmentsLoad extends OutreachLoad<\n OutreachEnrollment[]\n> {\n /** Whether a further page may exist. */\n hasMore: boolean\n showMore(): void\n}\n\n/**\n * A sequence's enrollments, the most recently enrolled first.\n *\n * Paged by document id — an equality on `sequenceId` in the id's own order\n * needs no composite index — and sorted by enrollment time once read, so a\n * table of up to a page reads in the order people were enrolled.\n */\nexport function useOutreachEnrollments(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachEnrollmentsLoad {\n const firestore = useFirestore()\n const [pages, setPages] = useState(1)\n const [result, setResult] = useState<\n OutreachLoad<OutreachEnrollment[]> & { size: number }\n >({\n status: 'loading',\n data: [],\n size: 0,\n })\n useEffect(() => setPages(1), [orgId, sequenceId])\n useEffect(() => {\n if (!orgId || !sequenceId) {\n setResult({ status: 'loading', data: [], size: 0 })\n return undefined\n }\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('sequenceId', '==', sequenceId),\n orderBy(documentId()),\n limit(pages * OUTREACH_ENROLLMENTS_PAGE),\n ),\n (snapshot) => {\n const enrollments = snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter(\n (enrollment): enrollment is OutreachEnrollment =>\n enrollment !== null,\n )\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: enrollments, size: snapshot.size })\n },\n (error) =>\n setResult({ ...failed([], error, 'the enrollments'), size: 0 }),\n )\n }, [firestore, orgId, sequenceId, pages])\n return useMemo(\n () => ({\n status: result.status,\n data: result.data,\n hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,\n showMore: () => setPages((count) => count + 1),\n }),\n [result, pages],\n )\n}\n\n/** The counts the sequence list shows beside each sequence. */\nexport interface OutreachSequenceCounts {\n enrolled: number\n active: number\n replied: number\n bounced: number\n optedOut: number\n}\n\nconst COUNTED: ReadonlyArray<\n [keyof OutreachSequenceCounts, OutreachEnrollmentStatus | null]\n> = [\n ['enrolled', null],\n ['active', 'active'],\n ['replied', 'replied'],\n ['bounced', 'bounced'],\n ['optedOut', 'opted_out'],\n]\n\n/**\n * Each sequence's enrollment counts, by server-side aggregation — one count\n * per figure, never a read of the enrollments themselves. Recounted when the\n * sequences asked about change — the list asks about the page on screen —\n * and absent while counting, or if a count failed.\n */\nexport function useOutreachSequenceCounts(\n orgId: string | null,\n sequenceIds: readonly string[],\n): Record<string, OutreachSequenceCounts> {\n const firestore = useFirestore()\n const [counts, setCounts] = useState<Record<string, OutreachSequenceCounts>>(\n {},\n )\n const key = sequenceIds.join(',')\n useEffect(() => {\n if (!orgId || !key) return undefined\n let current = true\n const enrollments = collection(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.enrollments,\n )\n Promise.all(\n key.split(',').map(async (sequenceId) => {\n const figures = await Promise.all(\n COUNTED.map(async ([, status]) => {\n const counted = await getCountFromServer(\n status\n ? query(\n enrollments,\n where('sequenceId', '==', sequenceId),\n where('status', '==', status),\n )\n : query(enrollments, where('sequenceId', '==', sequenceId)),\n )\n return counted.data().count\n }),\n )\n return [\n sequenceId,\n Object.fromEntries(\n COUNTED.map(([name], index) => [name, figures[index]]),\n ) as unknown as OutreachSequenceCounts,\n ] as const\n }),\n )\n .then((entries) => current && setCounts(Object.fromEntries(entries)))\n .catch((error: unknown) =>\n console.error('[outreach] enrollment counts could not be read', error),\n )\n return () => {\n current = false\n }\n }, [firestore, orgId, key])\n return counts\n}\n"],"names":["useFirestore","collection","doc","documentId","getCountFromServer","limit","onSnapshot","orderBy","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","readStoredOutreachEnrollment","readStoredOutreachSequence","denied","error","code","failed","data","what","status","console","OUTREACH_SEQUENCES_LIMIT","useOutreachSequences","orgId","firestore","result","setResult","undefined","sequences","snapshot","docs","map","entry","id","filter","sequence","sort","a","b","createdAtMs","useOutreachSequence","sequenceId","exists","OUTREACH_ENROLLMENTS_PAGE","useOutreachEnrollments","pages","setPages","size","enrollments","enrollment","hasMore","showMore","count","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OutreachApiRefusalReason, OutreachConnectCompleteResponse, OutreachMailboxAvailability, OutreachMailboxDisconnectResponse, OutreachMailboxResponse, OutreachMailboxSettingsRequest, OutreachMailboxTestResponse } from '../mailboxes/mailbox-api';
|
|
2
|
+
/** A mailbox route's refusal, carrying the sentence and the stable reason. */
|
|
3
|
+
export declare class OutreachApiError extends Error {
|
|
4
|
+
readonly reason: OutreachApiRefusalReason | 'unreachable';
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(message: string, reason: OutreachApiRefusalReason | 'unreachable', status: number);
|
|
7
|
+
}
|
|
8
|
+
/** The mailbox routes, as the Mailboxes panel calls them. */
|
|
9
|
+
export interface OutreachMailboxApi {
|
|
10
|
+
availability(): Promise<OutreachMailboxAvailability>;
|
|
11
|
+
/** Google's consent address for a new connect, or a reconnect. */
|
|
12
|
+
connect(): Promise<string>;
|
|
13
|
+
complete(input: {
|
|
14
|
+
code: string;
|
|
15
|
+
state: string;
|
|
16
|
+
timezone?: string;
|
|
17
|
+
}): Promise<OutreachConnectCompleteResponse>;
|
|
18
|
+
saveSettings(input: Omit<OutreachMailboxSettingsRequest, 'orgId'>): Promise<OutreachMailboxResponse>;
|
|
19
|
+
setPaused(mailboxId: string, paused: boolean): Promise<OutreachMailboxResponse>;
|
|
20
|
+
sendTest(mailboxId: string): Promise<OutreachMailboxTestResponse>;
|
|
21
|
+
disconnect(mailboxId: string): Promise<OutreachMailboxDisconnectResponse>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The Mailboxes panel's one door to the mailbox routes (AGL-2978), with the
|
|
25
|
+
* member's session attached and the organization named on every call — the
|
|
26
|
+
* dispatcher's release gate reads it. A refusal is thrown as an
|
|
27
|
+
* {@link OutreachApiError} carrying the route's own sentence, which is
|
|
28
|
+
* written for the person reading it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useOutreachMailboxApi(orgId: string | null): OutreachMailboxApi;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
19
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { useCallback, useMemo } from "react";
|
|
21
|
+
import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
22
|
+
/** A mailbox route's refusal, carrying the sentence and the stable reason. */ export class OutreachApiError extends Error {
|
|
23
|
+
constructor(message, reason, status){
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'OutreachApiError';
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The Mailboxes panel's one door to the mailbox routes (AGL-2978), with the
|
|
32
|
+
* member's session attached and the organization named on every call — the
|
|
33
|
+
* dispatcher's release gate reads it. A refusal is thrown as an
|
|
34
|
+
* {@link OutreachApiError} carrying the route's own sentence, which is
|
|
35
|
+
* written for the person reading it.
|
|
36
|
+
*/ export function useOutreachMailboxApi(orgId) {
|
|
37
|
+
const { data: user } = useUser();
|
|
38
|
+
const call = useCallback(async (route, init)=>{
|
|
39
|
+
if (!orgId) throw new OutreachApiError('Open an organization first.', 'org-required', 400);
|
|
40
|
+
const path = init.method === 'GET' ? `/api/${route}?orgId=${encodeURIComponent(orgId)}` : `/api/${route}`;
|
|
41
|
+
const response = await authorizedFetch(user, path, _extends({
|
|
42
|
+
method: init.method
|
|
43
|
+
}, init.method === 'POST' ? {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json'
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(_extends({
|
|
48
|
+
orgId
|
|
49
|
+
}, init.body))
|
|
50
|
+
} : {}));
|
|
51
|
+
const payload = await response.json().catch(()=>null);
|
|
52
|
+
if (!response.ok || !payload) {
|
|
53
|
+
var _refusal_reason;
|
|
54
|
+
const refusal = payload != null ? payload : {};
|
|
55
|
+
throw new OutreachApiError(refusal.error || 'Sequences could not be reached. Try again.', (_refusal_reason = refusal.reason) != null ? _refusal_reason : 'unreachable', response.status);
|
|
56
|
+
}
|
|
57
|
+
return payload;
|
|
58
|
+
}, [
|
|
59
|
+
user,
|
|
60
|
+
orgId
|
|
61
|
+
]);
|
|
62
|
+
return useMemo(()=>({
|
|
63
|
+
availability: ()=>call(OUTREACH_API_ROUTES.mailboxesAvailability, {
|
|
64
|
+
method: 'GET'
|
|
65
|
+
}),
|
|
66
|
+
connect: async ()=>(await call(OUTREACH_API_ROUTES.mailboxesConnect, {
|
|
67
|
+
method: 'POST'
|
|
68
|
+
})).url,
|
|
69
|
+
complete: (input)=>call(OUTREACH_API_ROUTES.mailboxesConnectComplete, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: input
|
|
72
|
+
}),
|
|
73
|
+
saveSettings: (input)=>call(OUTREACH_API_ROUTES.mailboxesSettings, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
body: input
|
|
76
|
+
}),
|
|
77
|
+
setPaused: (mailboxId, paused)=>call(OUTREACH_API_ROUTES.mailboxesStatus, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: {
|
|
80
|
+
mailboxId,
|
|
81
|
+
paused
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
sendTest: (mailboxId)=>call(OUTREACH_API_ROUTES.mailboxesTest, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: {
|
|
87
|
+
mailboxId
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
disconnect: (mailboxId)=>call(OUTREACH_API_ROUTES.mailboxesDisconnect, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: {
|
|
93
|
+
mailboxId
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}), [
|
|
97
|
+
call
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=use-outreach-mailbox-api.js.map
|