@aglyn/plugins-inbox 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 +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +19 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/contacts-card.component.d.ts +16 -0
- package/src/lib/components/contacts-card.component.js +351 -0
- package/src/lib/components/contacts-card.component.js.map +1 -0
- package/src/lib/components/inbox-attribution-zone.d.ts +52 -0
- package/src/lib/components/inbox-attribution-zone.js +53 -0
- package/src/lib/components/inbox-attribution-zone.js.map +1 -0
- package/src/lib/components/inbox-console-page.d.ts +28 -0
- package/src/lib/components/inbox-console-page.js +219 -0
- package/src/lib/components/inbox-console-page.js.map +1 -0
- package/src/lib/components/inbox-console-sections.d.ts +40 -0
- package/src/lib/components/inbox-console-sections.js +66 -0
- package/src/lib/components/inbox-console-sections.js.map +1 -0
- package/src/lib/components/inbox-glance-card.component.d.ts +26 -0
- package/src/lib/components/inbox-glance-card.component.js +215 -0
- package/src/lib/components/inbox-glance-card.component.js.map +1 -0
- package/src/lib/components/submission-list-assignment.component.d.ts +29 -0
- package/src/lib/components/submission-list-assignment.component.js +257 -0
- package/src/lib/components/submission-list-assignment.component.js.map +1 -0
- package/src/lib/components/submission-reply.component.d.ts +30 -0
- package/src/lib/components/submission-reply.component.js +249 -0
- package/src/lib/components/submission-reply.component.js.map +1 -0
- package/src/lib/components/submissions-card.component.d.ts +35 -0
- package/src/lib/components/submissions-card.component.js +577 -0
- package/src/lib/components/submissions-card.component.js.map +1 -0
- package/src/lib/components/use-record-route-context.d.ts +15 -0
- package/src/lib/components/use-record-route-context.js +41 -0
- package/src/lib/components/use-record-route-context.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/reply-policy.d.ts +134 -0
- package/src/lib/model/reply-policy.js +141 -0
- package/src/lib/model/reply-policy.js.map +1 -0
- package/src/lib/model/submission-presenter.d.ts +81 -0
- package/src/lib/model/submission-presenter.js +152 -0
- package/src/lib/model/submission-presenter.js.map +1 -0
- package/src/lib/plugin.d.ts +25 -0
- package/src/lib/plugin.js +102 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server.d.ts +179 -0
- package/src/lib/server.js +666 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { formSpamCaughtNotice, formSubmissionsPausedNotice, submissionMonthKey, visitorRecordRefusedCounterId, visitorRecordsPausedNotice } from "@aglyn/aglyn";
|
|
19
|
+
import { HubSections } from "@aglyn/shared-ui-next";
|
|
20
|
+
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
21
|
+
import { Alert, AlertTitle, Typography } from "@mui/material";
|
|
22
|
+
import { doc } from "firebase/firestore";
|
|
23
|
+
import ContactsCard from "./contacts-card.component.js";
|
|
24
|
+
import { InboxCampaignsZone } from "./inbox-attribution-zone.js";
|
|
25
|
+
import SubmissionsCard from "./submissions-card.component.js";
|
|
26
|
+
/**
|
|
27
|
+
* The body of one inbox section, built only when that section is the one being
|
|
28
|
+
* read (AGL-2501).
|
|
29
|
+
*
|
|
30
|
+
* A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`
|
|
31
|
+
* would CONSTRUCT all three every render, and each card opens its Firestore
|
|
32
|
+
* listens on mount — which is the entire cost this page exists to stop paying.
|
|
33
|
+
* Only the returned branch is ever built.
|
|
34
|
+
*/ function sectionBody(section, hostId) {
|
|
35
|
+
switch(section){
|
|
36
|
+
case 'submissions':
|
|
37
|
+
return /*#__PURE__*/ _jsx(SubmissionsCard, {
|
|
38
|
+
hostId: hostId
|
|
39
|
+
});
|
|
40
|
+
case 'contacts':
|
|
41
|
+
return /*#__PURE__*/ _jsx(ContactsCard, {
|
|
42
|
+
hostId: hostId
|
|
43
|
+
});
|
|
44
|
+
case 'campaigns':
|
|
45
|
+
return /*#__PURE__*/ _jsx(InboxCampaignsZone, {
|
|
46
|
+
hostId: hostId
|
|
47
|
+
});
|
|
48
|
+
default:
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Inbox (AGL-77/104/109 → AGL-395): form submissions reader, site members +
|
|
54
|
+
* leads, and campaigns — owned by the inbox plugin and rendered by the shell's
|
|
55
|
+
* generic plugin route. The Campaigns section is a zone this page hosts; the
|
|
56
|
+
* plugin that sends campaigns draws its card there.
|
|
57
|
+
*
|
|
58
|
+
* Orders are NOT here. A sale is not something that arrived in an inbox, and
|
|
59
|
+
* the card was nested inside the members section rather than carrying a tab of
|
|
60
|
+
* its own — so the rail listed three sections while the page drew a fourth
|
|
61
|
+
* subject. Commerce already owns it: `commerce-console-sections.ts` declares
|
|
62
|
+
* an `orders` section that renders the same card.
|
|
63
|
+
*
|
|
64
|
+
* Sections are ROUTES (AGL-2501), following the hubs that migrated before it.
|
|
65
|
+
* Here it IS a read saving as well as an addressing one: this page carried
|
|
66
|
+
* `HubTabs lazy`, which mounts one panel but keeps every panel it has visited,
|
|
67
|
+
* so a reader who looked at Members & leads and went back to Submissions held
|
|
68
|
+
* both sections' listens open for the rest of the visit. A URL per section
|
|
69
|
+
* makes the saving structural — the page builds one section's body and the
|
|
70
|
+
* others do not exist to subscribe. What routing adds besides is that the URL
|
|
71
|
+
* names the section: it is linkable, the back button walks sections, and the
|
|
72
|
+
* breadcrumb says where you are.
|
|
73
|
+
*/ export function InboxConsolePage(props) {
|
|
74
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
75
|
+
const { hostId, section, sections, basePath } = props;
|
|
76
|
+
const firestore = useFirestore();
|
|
77
|
+
// Submissions this site's abuse ceiling refused (AGL-1655 → AGL-1666).
|
|
78
|
+
//
|
|
79
|
+
// Until this, the refusal existed in two places a site owner cannot see: a
|
|
80
|
+
// counters document only Firestore's console renders, and one in-app
|
|
81
|
+
// notification that `system.` bucket-muting can suppress at write time —
|
|
82
|
+
// `notifyUsers` skips the batch entirely, so a muted owner's notification
|
|
83
|
+
// is never created and cannot be recovered by unmuting. This surface is
|
|
84
|
+
// the durable one, and it is a plain read of the same document the
|
|
85
|
+
// dropped-contacts alert uses (AGL-891).
|
|
86
|
+
const { data: refusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsRefused'), [
|
|
87
|
+
firestore,
|
|
88
|
+
hostId
|
|
89
|
+
]);
|
|
90
|
+
// Keyed by the month the SERVER wrote, via the shared helper — a key
|
|
91
|
+
// derived differently here would read zero refusals on exactly the sites
|
|
92
|
+
// being refused.
|
|
93
|
+
const pausedNotice = formSubmissionsPausedNotice({
|
|
94
|
+
refused: Number((_ref = refusedCounter == null ? void 0 : refusedCounter[submissionMonthKey()]) != null ? _ref : 0),
|
|
95
|
+
ceiling: Number(refusedCounter == null ? void 0 : refusedCounter['ceiling']) || undefined
|
|
96
|
+
});
|
|
97
|
+
// Bot submissions the honeypot caught (AGL-1831 → AGL-1836). The staff org
|
|
98
|
+
// page has shown this number per host since AGL-1831; this is the same
|
|
99
|
+
// count where the site's OWNER already looks, so "is my form being hit by
|
|
100
|
+
// bots?" is answered by their own inbox instead of a support ticket. Same
|
|
101
|
+
// client-unwritable counters document shape as the refusal counter above,
|
|
102
|
+
// same host-admin read the rules already grant (AGL-1367), same shared
|
|
103
|
+
// month key — and the shared sentence returns null below one catch, so a
|
|
104
|
+
// quiet month renders nothing rather than a reassuring zero.
|
|
105
|
+
const { data: spamCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsSpam'), [
|
|
106
|
+
firestore,
|
|
107
|
+
hostId
|
|
108
|
+
]);
|
|
109
|
+
const spamNotice = formSpamCaughtNotice({
|
|
110
|
+
spam: Number((_ref1 = spamCounter == null ? void 0 : spamCounter[submissionMonthKey()]) != null ? _ref1 : 0)
|
|
111
|
+
});
|
|
112
|
+
// Sign-ups and leads this site's PLATFORM ceiling refused (AGL-1529).
|
|
113
|
+
//
|
|
114
|
+
// Same instrument as the form ceiling directly above and read the same way:
|
|
115
|
+
// a client-unwritable counters document (AGL-1367) that host admins can
|
|
116
|
+
// already read, keyed by the SERVER's month through the shared helper — a
|
|
117
|
+
// key derived differently here would read zero refusals on exactly the
|
|
118
|
+
// sites being refused. The counter id comes from the shared function for
|
|
119
|
+
// the same reason: the writer is in `@aglyn/tenant-data-admin` and this is
|
|
120
|
+
// the reader, and two spellings of one document id is a surface that
|
|
121
|
+
// renders nothing forever.
|
|
122
|
+
//
|
|
123
|
+
// This is the surface that makes the ceiling SHIPPED rather than merely
|
|
124
|
+
// implemented. Without it a refusal exists in two places a site owner
|
|
125
|
+
// cannot see: a Firestore document only our console renders, and one
|
|
126
|
+
// notification that `system.` bucket-muting can suppress at write time.
|
|
127
|
+
const { data: membersRefusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'counters', visitorRecordRefusedCounterId('siteMembers')), [
|
|
128
|
+
firestore,
|
|
129
|
+
hostId
|
|
130
|
+
]);
|
|
131
|
+
const membersPausedNotice = visitorRecordsPausedNotice({
|
|
132
|
+
kind: 'siteMembers',
|
|
133
|
+
refused: Number((_ref2 = membersRefusedCounter == null ? void 0 : membersRefusedCounter[submissionMonthKey()]) != null ? _ref2 : 0),
|
|
134
|
+
ceiling: Number(membersRefusedCounter == null ? void 0 : membersRefusedCounter['ceiling']) || undefined
|
|
135
|
+
});
|
|
136
|
+
const { data: leadsRefusedCounter } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'counters', visitorRecordRefusedCounterId('leads')), [
|
|
137
|
+
firestore,
|
|
138
|
+
hostId
|
|
139
|
+
]);
|
|
140
|
+
const leadsPausedNotice = visitorRecordsPausedNotice({
|
|
141
|
+
kind: 'leads',
|
|
142
|
+
refused: Number((_ref3 = leadsRefusedCounter == null ? void 0 : leadsRefusedCounter[submissionMonthKey()]) != null ? _ref3 : 0),
|
|
143
|
+
ceiling: Number(leadsRefusedCounter == null ? void 0 : leadsRefusedCounter['ceiling']) || undefined
|
|
144
|
+
});
|
|
145
|
+
/*
|
|
146
|
+
* Nothing until the URL names a section. The shell redirects a bare hub URL
|
|
147
|
+
* to the landing section and holds a spinner while it does, so this state is
|
|
148
|
+
* transient — and rendering a default section here instead would pay for its
|
|
149
|
+
* listens on a URL that is already being replaced.
|
|
150
|
+
*
|
|
151
|
+
* The four counter reads above run either way, and that is deliberate: they
|
|
152
|
+
* are four single-document listens, and hoisting them behind this guard
|
|
153
|
+
* would make a ceiling notice appear a frame late on the section a reader
|
|
154
|
+
* lands on.
|
|
155
|
+
*/ if (!section || !(sections == null ? void 0 : sections.length) || !basePath) return null;
|
|
156
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
157
|
+
children: [
|
|
158
|
+
pausedNotice ? /*#__PURE__*/ _jsxs(Alert, {
|
|
159
|
+
severity: "warning",
|
|
160
|
+
sx: {
|
|
161
|
+
mb: 2
|
|
162
|
+
},
|
|
163
|
+
children: [
|
|
164
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
165
|
+
children: pausedNotice.title
|
|
166
|
+
}),
|
|
167
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
168
|
+
component: "div",
|
|
169
|
+
variant: "body2",
|
|
170
|
+
children: pausedNotice.message
|
|
171
|
+
}),
|
|
172
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
173
|
+
component: "div",
|
|
174
|
+
variant: "body2",
|
|
175
|
+
color: "text.secondary",
|
|
176
|
+
sx: {
|
|
177
|
+
mt: 0.5
|
|
178
|
+
},
|
|
179
|
+
children: pausedNotice.until
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
}) : null,
|
|
183
|
+
spamNotice ? /*#__PURE__*/ _jsx(Alert, {
|
|
184
|
+
severity: "info",
|
|
185
|
+
sx: {
|
|
186
|
+
mb: 2
|
|
187
|
+
},
|
|
188
|
+
children: spamNotice
|
|
189
|
+
}) : null,
|
|
190
|
+
[
|
|
191
|
+
membersPausedNotice,
|
|
192
|
+
leadsPausedNotice
|
|
193
|
+
].map((notice)=>notice ? /*#__PURE__*/ _jsxs(Alert, {
|
|
194
|
+
severity: "warning",
|
|
195
|
+
sx: {
|
|
196
|
+
mb: 2
|
|
197
|
+
},
|
|
198
|
+
children: [
|
|
199
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
200
|
+
children: notice.title
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
203
|
+
component: "div",
|
|
204
|
+
variant: "body2",
|
|
205
|
+
children: notice.message
|
|
206
|
+
})
|
|
207
|
+
]
|
|
208
|
+
}, notice.title) : null),
|
|
209
|
+
/*#__PURE__*/ _jsx(HubSections, {
|
|
210
|
+
sections: sections,
|
|
211
|
+
children: sectionBody(section, hostId)
|
|
212
|
+
})
|
|
213
|
+
]
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
InboxConsolePage.displayName = 'InboxConsolePage';
|
|
217
|
+
export default InboxConsolePage;
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=inbox-console-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n formSpamCaughtNotice,\n formSubmissionsPausedNotice,\n submissionMonthKey,\n visitorRecordRefusedCounterId,\n visitorRecordsPausedNotice,\n} from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Alert, AlertTitle, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport type { ReactNode } from 'react'\nimport ContactsCard from './contacts-card.component'\nimport { InboxCampaignsZone } from './inbox-attribution-zone'\nimport type { InboxConsoleSectionId } from './inbox-console-sections'\nimport SubmissionsCard from './submissions-card.component'\n\n/**\n * The body of one inbox section, built only when that section is the one being\n * read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all three every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: InboxConsoleSectionId,\n hostId: string,\n): ReactNode {\n switch (section) {\n case 'submissions':\n return <SubmissionsCard hostId={hostId} />\n case 'contacts':\n return <ContactsCard hostId={hostId} />\n case 'campaigns':\n return <InboxCampaignsZone hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * Inbox (AGL-77/104/109 → AGL-395): form submissions reader, site members +\n * leads, and campaigns — owned by the inbox plugin and rendered by the shell's\n * generic plugin route. The Campaigns section is a zone this page hosts; the\n * plugin that sends campaigns draws its card there.\n *\n * Orders are NOT here. A sale is not something that arrived in an inbox, and\n * the card was nested inside the members section rather than carrying a tab of\n * its own — so the rail listed three sections while the page drew a fourth\n * subject. Commerce already owns it: `commerce-console-sections.ts` declares\n * an `orders` section that renders the same card.\n *\n * Sections are ROUTES (AGL-2501), following the hubs that migrated before it.\n * Here it IS a read saving as well as an addressing one: this page carried\n * `HubTabs lazy`, which mounts one panel but keeps every panel it has visited,\n * so a reader who looked at Members & leads and went back to Submissions held\n * both sections' listens open for the rest of the visit. A URL per section\n * makes the saving structural — the page builds one section's body and the\n * others do not exist to subscribe. What routing adds besides is that the URL\n * names the section: it is linkable, the back button walks sections, and the\n * breadcrumb says where you are.\n */\nexport function InboxConsolePage(props: ConsolePluginPageProps) {\n const { hostId, section, sections, basePath } = props\n const firestore = useFirestore()\n\n // Submissions this site's abuse ceiling refused (AGL-1655 → AGL-1666).\n //\n // Until this, the refusal existed in two places a site owner cannot see: a\n // counters document only Firestore's console renders, and one in-app\n // notification that `system.` bucket-muting can suppress at write time —\n // `notifyUsers` skips the batch entirely, so a muted owner's notification\n // is never created and cannot be recovered by unmuting. This surface is\n // the durable one, and it is a plain read of the same document the\n // dropped-contacts alert uses (AGL-891).\n const { data: refusedCounter } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsRefused'),\n [firestore, hostId],\n )\n // Keyed by the month the SERVER wrote, via the shared helper — a key\n // derived differently here would read zero refusals on exactly the sites\n // being refused.\n const pausedNotice = formSubmissionsPausedNotice({\n refused: Number(refusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(refusedCounter?.['ceiling']) || undefined,\n })\n\n // Bot submissions the honeypot caught (AGL-1831 → AGL-1836). The staff org\n // page has shown this number per host since AGL-1831; this is the same\n // count where the site's OWNER already looks, so \"is my form being hit by\n // bots?\" is answered by their own inbox instead of a support ticket. Same\n // client-unwritable counters document shape as the refusal counter above,\n // same host-admin read the rules already grant (AGL-1367), same shared\n // month key — and the shared sentence returns null below one catch, so a\n // quiet month renders nothing rather than a reassuring zero.\n const { data: spamCounter } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'counters', 'formSubmissionsSpam'),\n [firestore, hostId],\n )\n const spamNotice = formSpamCaughtNotice({\n spam: Number(spamCounter?.[submissionMonthKey()] ?? 0),\n })\n\n // Sign-ups and leads this site's PLATFORM ceiling refused (AGL-1529).\n //\n // Same instrument as the form ceiling directly above and read the same way:\n // a client-unwritable counters document (AGL-1367) that host admins can\n // already read, keyed by the SERVER's month through the shared helper — a\n // key derived differently here would read zero refusals on exactly the\n // sites being refused. The counter id comes from the shared function for\n // the same reason: the writer is in `@aglyn/tenant-data-admin` and this is\n // the reader, and two spellings of one document id is a surface that\n // renders nothing forever.\n //\n // This is the surface that makes the ceiling SHIPPED rather than merely\n // implemented. Without it a refusal exists in two places a site owner\n // cannot see: a Firestore document only our console renders, and one\n // notification that `system.` bucket-muting can suppress at write time.\n const { data: membersRefusedCounter } = useFirestoreDoc<any>(\n () =>\n doc(\n firestore,\n 'hosts',\n hostId,\n 'counters',\n visitorRecordRefusedCounterId('siteMembers'),\n ),\n [firestore, hostId],\n )\n const membersPausedNotice = visitorRecordsPausedNotice({\n kind: 'siteMembers',\n refused: Number(membersRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(membersRefusedCounter?.['ceiling']) || undefined,\n })\n const { data: leadsRefusedCounter } = useFirestoreDoc<any>(\n () =>\n doc(\n firestore,\n 'hosts',\n hostId,\n 'counters',\n visitorRecordRefusedCounterId('leads'),\n ),\n [firestore, hostId],\n )\n const leadsPausedNotice = visitorRecordsPausedNotice({\n kind: 'leads',\n refused: Number(leadsRefusedCounter?.[submissionMonthKey()] ?? 0),\n ceiling: Number(leadsRefusedCounter?.['ceiling']) || undefined,\n })\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n *\n * The four counter reads above run either way, and that is deliberate: they\n * are four single-document listens, and hoisting them behind this guard\n * would make a ceiling notice appear a frame late on the section a reader\n * lands on.\n */\n if (!section || !sections?.length || !basePath) return null\n\n return (\n <>\n {/* Above the RAIL on purpose (AGL-1666). A paused form is not a fact\n about the Submissions section — it is why the whole inbox stopped\n filling — and the notification that brings an owner here links to\n the surface, not to a section. Inside one section's body it would be\n invisible to anyone who followed a link into another. */}\n {pausedNotice ? (\n <Alert severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{pausedNotice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {pausedNotice.message}\n </Typography>\n <Typography\n component=\"div\"\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ mt: 0.5 }}\n >\n {pausedNotice.until}\n </Typography>\n </Alert>\n ) : null}\n {/* Beside the paused notice, and info rather than warning on purpose\n (AGL-1836): the honeypot count reports protection WORKING — those\n submissions were caught, dropped, and never stored or billed. An\n owner whose inbox went quiet checks here; bots absorbed silently is\n the answer that stops the support ticket. */}\n {spamNotice ? (\n <Alert severity=\"info\" sx={{ mb: 2 }}>\n {spamNotice}\n </Alert>\n ) : null}\n {/* Above the rail for the same reason the form notice is (AGL-1529):\n the Members and Leads lists are one section of several, and an owner\n who followed a link into another would never see a notice hidden\n inside that one. Two notices rather than one because the two ceilings\n are independent — a site can be refusing leads while sign-ups still\n land — and a merged sentence could only be true of both. Neither\n carries an `until` line: unlike the monthly form ceiling, these\n count LIVE DOCUMENTS and no date lifts them. */}\n {[membersPausedNotice, leadsPausedNotice].map((notice) =>\n notice ? (\n <Alert key={notice.title} severity=\"warning\" sx={{ mb: 2 }}>\n <AlertTitle>{notice.title}</AlertTitle>\n <Typography component=\"div\" variant=\"body2\">\n {notice.message}\n </Typography>\n </Alert>\n ) : null,\n )}\n <HubSections sections={sections}>\n {sectionBody(section as InboxConsoleSectionId, hostId)}\n </HubSections>\n </>\n )\n}\nInboxConsolePage.displayName = 'InboxConsolePage'\n\nexport default InboxConsolePage\n"],"names":["formSpamCaughtNotice","formSubmissionsPausedNotice","submissionMonthKey","visitorRecordRefusedCounterId","visitorRecordsPausedNotice","HubSections","useFirestore","useFirestoreDoc","Alert","AlertTitle","Typography","doc","ContactsCard","InboxCampaignsZone","SubmissionsCard","sectionBody","section","hostId","InboxConsolePage","props","sections","basePath","firestore","data","refusedCounter","pausedNotice","refused","Number","ceiling","undefined","spamCounter","spamNotice","spam","membersRefusedCounter","membersPausedNotice","kind","leadsRefusedCounter","leadsPausedNotice","length","severity","sx","mb","title","component","variant","message","color","mt","until","map","notice","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAEEA,oBAAoB,EACpBC,2BAA2B,EAC3BC,kBAAkB,EAClBC,6BAA6B,EAC7BC,0BAA0B,QACrB,eAAc;AACrB,SAASC,WAAW,QAAQ,wBAAuB;AACnD,SAASC,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SAASC,KAAK,EAAEC,UAAU,EAAEC,UAAU,QAAQ,gBAAe;AAC7D,SAASC,GAAG,QAAQ,qBAAoB;AAExC,OAAOC,kBAAkB,+BAA2B;AACpD,SAASC,kBAAkB,QAAQ,8BAA0B;AAE7D,OAAOC,qBAAqB,kCAA8B;AAE1D;;;;;;;;CAQC,GACD,SAASC,YACPC,OAA8B,EAC9BC,MAAc;IAEd,OAAQD;QACN,KAAK;YACH,qBAAO,KAACF;gBAAgBG,QAAQA;;QAClC,KAAK;YACH,qBAAO,KAACL;gBAAaK,QAAQA;;QAC/B,KAAK;YACH,qBAAO,KAACJ;gBAAmBI,QAAQA;;QACrC;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,iBAAiBC,KAA6B;;IAC5D,MAAM,EAAEF,MAAM,EAAED,OAAO,EAAEI,QAAQ,EAAEC,QAAQ,EAAE,GAAGF;IAChD,MAAMG,YAAYhB;IAElB,uEAAuE;IACvE,EAAE;IACF,2EAA2E;IAC3E,qEAAqE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,wEAAwE;IACxE,mEAAmE;IACnE,yCAAyC;IACzC,MAAM,EAAEiB,MAAMC,cAAc,EAAE,GAAGjB,gBAC/B,IAAMI,IAAIW,WAAW,SAASL,QAAQ,YAAY,2BAClD;QAACK;QAAWL;KAAO;IAErB,qEAAqE;IACrE,yEAAyE;IACzE,iBAAiB;IACjB,MAAMQ,eAAexB,4BAA4B;QAC/CyB,SAASC,eAAOH,kCAAAA,cAAgB,CAACtB,qBAAqB,mBAAI;QAC1D0B,SAASD,OAAOH,kCAAAA,cAAgB,CAAC,UAAU,KAAKK;IAClD;IAEA,2EAA2E;IAC3E,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1E,uEAAuE;IACvE,yEAAyE;IACzE,6DAA6D;IAC7D,MAAM,EAAEN,MAAMO,WAAW,EAAE,GAAGvB,gBAC5B,IAAMI,IAAIW,WAAW,SAASL,QAAQ,YAAY,wBAClD;QAACK;QAAWL;KAAO;IAErB,MAAMc,aAAa/B,qBAAqB;QACtCgC,MAAML,gBAAOG,+BAAAA,WAAa,CAAC5B,qBAAqB,oBAAI;IACtD;IAEA,sEAAsE;IACtE,EAAE;IACF,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,yEAAyE;IACzE,2EAA2E;IAC3E,qEAAqE;IACrE,2BAA2B;IAC3B,EAAE;IACF,wEAAwE;IACxE,sEAAsE;IACtE,qEAAqE;IACrE,wEAAwE;IACxE,MAAM,EAAEqB,MAAMU,qBAAqB,EAAE,GAAG1B,gBACtC,IACEI,IACEW,WACA,SACAL,QACA,YACAd,8BAA8B,iBAElC;QAACmB;QAAWL;KAAO;IAErB,MAAMiB,sBAAsB9B,2BAA2B;QACrD+B,MAAM;QACNT,SAASC,gBAAOM,yCAAAA,qBAAuB,CAAC/B,qBAAqB,oBAAI;QACjE0B,SAASD,OAAOM,yCAAAA,qBAAuB,CAAC,UAAU,KAAKJ;IACzD;IACA,MAAM,EAAEN,MAAMa,mBAAmB,EAAE,GAAG7B,gBACpC,IACEI,IACEW,WACA,SACAL,QACA,YACAd,8BAA8B,WAElC;QAACmB;QAAWL;KAAO;IAErB,MAAMoB,oBAAoBjC,2BAA2B;QACnD+B,MAAM;QACNT,SAASC,gBAAOS,uCAAAA,mBAAqB,CAAClC,qBAAqB,oBAAI;QAC/D0B,SAASD,OAAOS,uCAAAA,mBAAqB,CAAC,UAAU,KAAKP;IACvD;IAEA;;;;;;;;;;GAUC,GACD,IAAI,CAACb,WAAW,EAACI,4BAAAA,SAAUkB,MAAM,KAAI,CAACjB,UAAU,OAAO;IAEvD,qBACE;;YAMGI,6BACC,MAACjB;gBAAM+B,UAAS;gBAAUC,IAAI;oBAAEC,IAAI;gBAAE;;kCACpC,KAAChC;kCAAYgB,aAAaiB,KAAK;;kCAC/B,KAAChC;wBAAWiC,WAAU;wBAAMC,SAAQ;kCACjCnB,aAAaoB,OAAO;;kCAEvB,KAACnC;wBACCiC,WAAU;wBACVC,SAAQ;wBACRE,OAAM;wBACNN,IAAI;4BAAEO,IAAI;wBAAI;kCAEbtB,aAAauB,KAAK;;;iBAGrB;YAMHjB,2BACC,KAACvB;gBAAM+B,UAAS;gBAAOC,IAAI;oBAAEC,IAAI;gBAAE;0BAChCV;iBAED;YASH;gBAACG;gBAAqBG;aAAkB,CAACY,GAAG,CAAC,CAACC,SAC7CA,uBACE,MAAC1C;oBAAyB+B,UAAS;oBAAUC,IAAI;wBAAEC,IAAI;oBAAE;;sCACvD,KAAChC;sCAAYyC,OAAOR,KAAK;;sCACzB,KAAChC;4BAAWiC,WAAU;4BAAMC,SAAQ;sCACjCM,OAAOL,OAAO;;;mBAHPK,OAAOR,KAAK,IAMtB;0BAEN,KAACrC;gBAAYe,UAAUA;0BACpBL,YAAYC,SAAkCC;;;;AAIvD;AACAC,iBAAiBiC,WAAW,GAAG;AAE/B,eAAejC,iBAAgB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import type { ConsoleNavSection } from '@aglyn/aglyn';
|
|
18
|
+
export type InboxConsoleSectionId = 'submissions' | 'contacts' | 'campaigns';
|
|
19
|
+
/**
|
|
20
|
+
* The inbox console's sections, in rail order (AGL-2501).
|
|
21
|
+
*
|
|
22
|
+
* One list, read twice and never copied: `plugin.ts` registers it on the nav
|
|
23
|
+
* item so the shell can route and gate each section, and the page switches its
|
|
24
|
+
* body on the id the shell resolves back. A second copy is how a section comes
|
|
25
|
+
* to be routable under one id and drawn under another.
|
|
26
|
+
*
|
|
27
|
+
* Ids are the `?tab=` ids this page already deep-linked by, kept deliberately
|
|
28
|
+
* so a bookmark that named a tab names the same section as a route. `contacts`
|
|
29
|
+
* therefore keeps its id while its LABEL reads "Members & leads".
|
|
30
|
+
*
|
|
31
|
+
* ORDERS ARE NOT HERE, and the omission is the decision. A sale is not
|
|
32
|
+
* something that arrived in an inbox, and commerce owns the surface that
|
|
33
|
+
* lists them: `commerce-console-sections.ts` declares an `orders` section
|
|
34
|
+
* rendering the same card. Adding one here would give one record two
|
|
35
|
+
* addresses.
|
|
36
|
+
*
|
|
37
|
+
* No `navTabId` on any of them: every section ships with the surface, so they
|
|
38
|
+
* inherit the Inbox nav item's gate.
|
|
39
|
+
*/
|
|
40
|
+
export declare const INBOX_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
*/ /**
|
|
17
|
+
* The inbox console's sections, in rail order (AGL-2501).
|
|
18
|
+
*
|
|
19
|
+
* One list, read twice and never copied: `plugin.ts` registers it on the nav
|
|
20
|
+
* item so the shell can route and gate each section, and the page switches its
|
|
21
|
+
* body on the id the shell resolves back. A second copy is how a section comes
|
|
22
|
+
* to be routable under one id and drawn under another.
|
|
23
|
+
*
|
|
24
|
+
* Ids are the `?tab=` ids this page already deep-linked by, kept deliberately
|
|
25
|
+
* so a bookmark that named a tab names the same section as a route. `contacts`
|
|
26
|
+
* therefore keeps its id while its LABEL reads "Members & leads".
|
|
27
|
+
*
|
|
28
|
+
* ORDERS ARE NOT HERE, and the omission is the decision. A sale is not
|
|
29
|
+
* something that arrived in an inbox, and commerce owns the surface that
|
|
30
|
+
* lists them: `commerce-console-sections.ts` declares an `orders` section
|
|
31
|
+
* rendering the same card. Adding one here would give one record two
|
|
32
|
+
* addresses.
|
|
33
|
+
*
|
|
34
|
+
* No `navTabId` on any of them: every section ships with the surface, so they
|
|
35
|
+
* inherit the Inbox nav item's gate.
|
|
36
|
+
*/ export const INBOX_CONSOLE_SECTIONS = [
|
|
37
|
+
{
|
|
38
|
+
id: 'submissions',
|
|
39
|
+
label: 'Submissions'
|
|
40
|
+
},
|
|
41
|
+
/*
|
|
42
|
+
* The people a site collected, members and leads in one list. Its label
|
|
43
|
+
* names both because the table is one list of two collections — a member
|
|
44
|
+
* who was a lead first appears once, and a rail reading "Contacts" beside
|
|
45
|
+
* the Contacts nav item would name a different surface's subject.
|
|
46
|
+
*/ {
|
|
47
|
+
id: 'contacts',
|
|
48
|
+
label: 'Members & leads'
|
|
49
|
+
},
|
|
50
|
+
/*
|
|
51
|
+
* Borrowed from the marketing plugin, which owns the campaign. It is listed
|
|
52
|
+
* here because a campaign is the outbound half of the same conversation the
|
|
53
|
+
* submissions are the inbound half of, and a merchant reading one reaches
|
|
54
|
+
* for the other.
|
|
55
|
+
*/ {
|
|
56
|
+
id: 'campaigns',
|
|
57
|
+
label: 'Campaigns'
|
|
58
|
+
}
|
|
59
|
+
] /*
|
|
60
|
+
* Rail ORDER decides where `/inbox` lands: the shell redirects a bare hub URL
|
|
61
|
+
* to the first section in this list the reader may open (AGL-2501). There is
|
|
62
|
+
* deliberately no separate default constant — a second place to say which
|
|
63
|
+
* section is first is a second place for it to disagree with the rail.
|
|
64
|
+
*/ ;
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=inbox-console-sections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type InboxConsoleSectionId = 'submissions' | 'contacts' | 'campaigns'\n\n/**\n * The inbox console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back. A second copy is how a section comes\n * to be routable under one id and drawn under another.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `contacts`\n * therefore keeps its id while its LABEL reads \"Members & leads\".\n *\n * ORDERS ARE NOT HERE, and the omission is the decision. A sale is not\n * something that arrived in an inbox, and commerce owns the surface that\n * lists them: `commerce-console-sections.ts` declares an `orders` section\n * rendering the same card. Adding one here would give one record two\n * addresses.\n *\n * No `navTabId` on any of them: every section ships with the surface, so they\n * inherit the Inbox nav item's gate.\n */\nexport const INBOX_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'submissions', label: 'Submissions' },\n /*\n * The people a site collected, members and leads in one list. Its label\n * names both because the table is one list of two collections — a member\n * who was a lead first appears once, and a rail reading \"Contacts\" beside\n * the Contacts nav item would name a different surface's subject.\n */\n { id: 'contacts', label: 'Members & leads' },\n /*\n * Borrowed from the marketing plugin, which owns the campaign. It is listed\n * here because a campaign is the outbound half of the same conversation the\n * submissions are the inbound half of, and a merchant reading one reaches\n * for the other.\n */\n { id: 'campaigns', label: 'Campaigns' },\n]\n\n/*\n * Rail ORDER decides where `/inbox` lands: the shell redirects a bare hub URL\n * to the first section in this list the reader may open (AGL-2501). There is\n * deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["INBOX_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMA,yBAAuD;IAClE;QAAEC,IAAI;QAAeC,OAAO;IAAc;IAC1C;;;;;GAKC,GACD;QAAED,IAAI;QAAYC,OAAO;IAAkB;IAC3C;;;;;GAKC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;CACvC,CAED;;;;;CAKC,IAPA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inbox at a glance: who wrote in, how long ago, and how many are
|
|
3
|
+
* unread.
|
|
4
|
+
*
|
|
5
|
+
* A dashboard is where a site owner looks first, and a form submission is
|
|
6
|
+
* the one thing on it that is waiting for a REPLY — until now it was two
|
|
7
|
+
* clicks away with nothing on the dashboard saying it had arrived.
|
|
8
|
+
*
|
|
9
|
+
* Renders nothing until the site has a submission, the same rule the other
|
|
10
|
+
* capability glances follow: an empty card about forms on a site with no
|
|
11
|
+
* form is an advertisement, not a summary.
|
|
12
|
+
*
|
|
13
|
+
* `orderBy('createdAt', 'desc')` is checked against the writer rather than
|
|
14
|
+
* assumed — an `orderBy` drops documents missing the field, which on a
|
|
15
|
+
* newest-first list is invisible. `apps/tenant/app/api/forms/submit/route.ts`
|
|
16
|
+
* is the only path that creates one and stamps `createdAt: serverTimestamp()`
|
|
17
|
+
* on every add, the v1 API only reads and deletes, and `formSubmissions` is
|
|
18
|
+
* absent from `IMPORTABLE_FIELDS`, so no restore can mint one without it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function InboxGlanceCard(props: {
|
|
21
|
+
hostId: string;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
export declare namespace InboxGlanceCard {
|
|
24
|
+
var displayName: string;
|
|
25
|
+
}
|
|
26
|
+
export default InboxGlanceCard;
|
|
@@ -0,0 +1,215 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { buildRoute, CRM_LEAD_CLOSED_STATUSES, openLeadsFromCounts, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
20
|
+
// The CRM's route builder by its leaf path, not the plugin barrel: the
|
|
21
|
+
// barrel is the CRM's site entry point, and a dashboard card named there
|
|
22
|
+
// would ship to every published page.
|
|
23
|
+
import { pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
24
|
+
import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
25
|
+
import { Avatar, Box, Button, Stack, Typography } from "@mui/material";
|
|
26
|
+
import { collection, getCountFromServer, limit, orderBy, query, where } from "firebase/firestore";
|
|
27
|
+
import { useParams } from "next/navigation";
|
|
28
|
+
import { useEffect, useState } from "react";
|
|
29
|
+
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
30
|
+
import { useRecordRouteContext } from "./use-record-route-context.js";
|
|
31
|
+
import { relativeTime, senderHue, submissionSender } from "../model/submission-presenter.js";
|
|
32
|
+
/**
|
|
33
|
+
* Rows on the card, plus one so it can say the inbox holds more.
|
|
34
|
+
*
|
|
35
|
+
* The extra document is never rendered — it is what turns "there may be
|
|
36
|
+
* more" into a fact, the same probe the console's paged lists use.
|
|
37
|
+
*/ const PREVIEW_ROWS = 3;
|
|
38
|
+
/**
|
|
39
|
+
* The inbox at a glance: who wrote in, how long ago, and how many are
|
|
40
|
+
* unread.
|
|
41
|
+
*
|
|
42
|
+
* A dashboard is where a site owner looks first, and a form submission is
|
|
43
|
+
* the one thing on it that is waiting for a REPLY — until now it was two
|
|
44
|
+
* clicks away with nothing on the dashboard saying it had arrived.
|
|
45
|
+
*
|
|
46
|
+
* Renders nothing until the site has a submission, the same rule the other
|
|
47
|
+
* capability glances follow: an empty card about forms on a site with no
|
|
48
|
+
* form is an advertisement, not a summary.
|
|
49
|
+
*
|
|
50
|
+
* `orderBy('createdAt', 'desc')` is checked against the writer rather than
|
|
51
|
+
* assumed — an `orderBy` drops documents missing the field, which on a
|
|
52
|
+
* newest-first list is invisible. `apps/tenant/app/api/forms/submit/route.ts`
|
|
53
|
+
* is the only path that creates one and stamps `createdAt: serverTimestamp()`
|
|
54
|
+
* on every add, the v1 API only reads and deletes, and `formSubmissions` is
|
|
55
|
+
* absent from `IMPORTABLE_FIELDS`, so no restore can mint one without it.
|
|
56
|
+
*/ export function InboxGlanceCard(props) {
|
|
57
|
+
const { hostId } = props;
|
|
58
|
+
const firestore = useFirestore();
|
|
59
|
+
const { orgSlug, host } = useParams();
|
|
60
|
+
const { data: submissionDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'formSubmissions'), orderBy('createdAt', 'desc'), limit(PREVIEW_ROWS + 1)), [
|
|
61
|
+
firestore,
|
|
62
|
+
hostId
|
|
63
|
+
], {
|
|
64
|
+
idField: '$id'
|
|
65
|
+
});
|
|
66
|
+
/*
|
|
67
|
+
* How many leads are waiting to be worked (AGL-2622), so the dashboard
|
|
68
|
+
* says where the Inbox's captures went and the CRM's Leads list is one
|
|
69
|
+
* click from here. Two server-side counts rather than a window: a lead
|
|
70
|
+
* nobody has touched carries no status field at all — it is `new` by
|
|
71
|
+
* absence, the rule `crmLeadStatus` reads — and Firestore can neither
|
|
72
|
+
* count an absent field nor exclude the closed statuses without dropping
|
|
73
|
+
* the unstamped rows too. So the open count is every lead less the ones
|
|
74
|
+
* closed by a stamped status — `openLeadsFromCounts`, the subtraction the
|
|
75
|
+
* CRM's own glance card makes — and both counts are aggregations that cost
|
|
76
|
+
* one read per thousand index entries, not one per lead. `null` until
|
|
77
|
+
* they land, and left null on a refused read so the line is withheld
|
|
78
|
+
* rather than drawn as zero.
|
|
79
|
+
*/ const routeContext = useRecordRouteContext();
|
|
80
|
+
const leadsHref = routeContext ? pluginRecordListHref('lead', routeContext) : null;
|
|
81
|
+
const [openLeads, setOpenLeads] = useState(null);
|
|
82
|
+
useEffect(()=>{
|
|
83
|
+
let active = true;
|
|
84
|
+
const leads = collection(firestore, 'hosts', hostId, 'leads');
|
|
85
|
+
void Promise.all([
|
|
86
|
+
getCountFromServer(query(leads)),
|
|
87
|
+
getCountFromServer(query(leads, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)))
|
|
88
|
+
]).then(([all, closed])=>{
|
|
89
|
+
if (!active) return;
|
|
90
|
+
setOpenLeads(openLeadsFromCounts(all.data().count, closed.data().count));
|
|
91
|
+
}).catch(()=>undefined);
|
|
92
|
+
return ()=>{
|
|
93
|
+
active = false;
|
|
94
|
+
};
|
|
95
|
+
}, [
|
|
96
|
+
firestore,
|
|
97
|
+
hostId
|
|
98
|
+
]);
|
|
99
|
+
const submissions = submissionDocs != null ? submissionDocs : [];
|
|
100
|
+
if (!submissions.length && !openLeads) return null;
|
|
101
|
+
const rows = submissions.slice(0, PREVIEW_ROWS);
|
|
102
|
+
const unread = rows.filter((submission)=>!submission.read).length;
|
|
103
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
104
|
+
header: 'Inbox',
|
|
105
|
+
help: pluginDocsHelp('forms', {
|
|
106
|
+
anchor: '#the-inbox',
|
|
107
|
+
excerpt: 'The newest form submissions on this site — the Inbox has the ' + 'full list, the reader, and the routing each one took.'
|
|
108
|
+
}),
|
|
109
|
+
contentGutterX: true,
|
|
110
|
+
contentGutterY: true,
|
|
111
|
+
HeaderProps: {
|
|
112
|
+
action: /*#__PURE__*/ _jsx(Button, _extends({
|
|
113
|
+
component: AppLink
|
|
114
|
+
}, {
|
|
115
|
+
componentVariant: 'naked',
|
|
116
|
+
nativeButton: false
|
|
117
|
+
}, {
|
|
118
|
+
href: buildRoute(Route.HOST_INBOX, {
|
|
119
|
+
orgSlug,
|
|
120
|
+
host
|
|
121
|
+
}),
|
|
122
|
+
size: "small",
|
|
123
|
+
color: "primary",
|
|
124
|
+
children: 'Open inbox'
|
|
125
|
+
}))
|
|
126
|
+
},
|
|
127
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
128
|
+
spacing: 1,
|
|
129
|
+
children: [
|
|
130
|
+
rows.map((submission)=>{
|
|
131
|
+
var _submission_formName;
|
|
132
|
+
var _submission_createdAt_toDate, _submission_createdAt;
|
|
133
|
+
const sender = submissionSender(submission.fields);
|
|
134
|
+
const hue = senderHue(sender.label);
|
|
135
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
136
|
+
direction: "row",
|
|
137
|
+
spacing: 1,
|
|
138
|
+
sx: {
|
|
139
|
+
alignItems: 'center'
|
|
140
|
+
},
|
|
141
|
+
children: [
|
|
142
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
143
|
+
"aria-label": submission.read ? undefined : 'Unread',
|
|
144
|
+
sx: {
|
|
145
|
+
width: 8,
|
|
146
|
+
height: 8,
|
|
147
|
+
borderRadius: '50%',
|
|
148
|
+
bgcolor: submission.read ? 'transparent' : 'primary.main',
|
|
149
|
+
flexShrink: 0
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
/*#__PURE__*/ _jsx(Avatar, {
|
|
153
|
+
sx: {
|
|
154
|
+
width: 28,
|
|
155
|
+
height: 28,
|
|
156
|
+
typography: 'caption',
|
|
157
|
+
bgcolor: `hsl(${hue} 55% 45%)`
|
|
158
|
+
},
|
|
159
|
+
children: sender.initials
|
|
160
|
+
}),
|
|
161
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
162
|
+
sx: {
|
|
163
|
+
flex: 1,
|
|
164
|
+
minWidth: 0
|
|
165
|
+
},
|
|
166
|
+
children: [
|
|
167
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
168
|
+
variant: "body2",
|
|
169
|
+
noWrap: true,
|
|
170
|
+
children: sender.label
|
|
171
|
+
}),
|
|
172
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
173
|
+
variant: "caption",
|
|
174
|
+
color: "text.secondary",
|
|
175
|
+
noWrap: true,
|
|
176
|
+
children: (_submission_formName = submission.formName) != null ? _submission_formName : 'Form'
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
}),
|
|
180
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
181
|
+
variant: "caption",
|
|
182
|
+
color: "text.secondary",
|
|
183
|
+
noWrap: true,
|
|
184
|
+
children: relativeTime((_submission_createdAt = submission.createdAt) == null ? void 0 : (_submission_createdAt_toDate = _submission_createdAt.toDate) == null ? void 0 : _submission_createdAt_toDate.call(_submission_createdAt).getTime())
|
|
185
|
+
})
|
|
186
|
+
]
|
|
187
|
+
}, submission.$id);
|
|
188
|
+
}),
|
|
189
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
190
|
+
variant: "caption",
|
|
191
|
+
color: "text.secondary",
|
|
192
|
+
children: [
|
|
193
|
+
unread ? `${unread} unread here` : null,
|
|
194
|
+
submissions.length > PREVIEW_ROWS ? 'more in the Inbox' : null
|
|
195
|
+
].filter(Boolean).join(' · ') || 'All caught up'
|
|
196
|
+
}),
|
|
197
|
+
openLeads ? /*#__PURE__*/ _jsxs(Typography, {
|
|
198
|
+
variant: "caption",
|
|
199
|
+
color: "text.secondary",
|
|
200
|
+
children: [
|
|
201
|
+
`${openLeads.toLocaleString()} open lead${openLeads === 1 ? '' : 's'} · `,
|
|
202
|
+
leadsHref ? /*#__PURE__*/ _jsx(AppLink, {
|
|
203
|
+
href: leadsHref,
|
|
204
|
+
children: 'Work them in the CRM'
|
|
205
|
+
}) : 'in the CRM'
|
|
206
|
+
]
|
|
207
|
+
}) : null
|
|
208
|
+
]
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
InboxGlanceCard.displayName = 'InboxGlanceCard';
|
|
213
|
+
export default InboxGlanceCard;
|
|
214
|
+
|
|
215
|
+
//# sourceMappingURL=inbox-glance-card.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/inbox/src/lib/components/inbox-glance-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n buildRoute,\n CRM_LEAD_CLOSED_STATUSES,\n openLeadsFromCounts,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\n// The CRM's route builder by its leaf path, not the plugin barrel: the\n// barrel is the CRM's site entry point, and a dashboard card named there\n// would ship to every published page.\nimport { pluginRecordListHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Avatar, Box, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useEffect, useState } from 'react'\nimport { useFirestore, useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { useRecordRouteContext } from './use-record-route-context'\nimport {\n relativeTime,\n senderHue,\n submissionSender,\n} from '../model/submission-presenter'\n\n/**\n * Rows on the card, plus one so it can say the inbox holds more.\n *\n * The extra document is never rendered — it is what turns \"there may be\n * more\" into a fact, the same probe the console's paged lists use.\n */\nconst PREVIEW_ROWS = 3\n\n/**\n * The inbox at a glance: who wrote in, how long ago, and how many are\n * unread.\n *\n * A dashboard is where a site owner looks first, and a form submission is\n * the one thing on it that is waiting for a REPLY — until now it was two\n * clicks away with nothing on the dashboard saying it had arrived.\n *\n * Renders nothing until the site has a submission, the same rule the other\n * capability glances follow: an empty card about forms on a site with no\n * form is an advertisement, not a summary.\n *\n * `orderBy('createdAt', 'desc')` is checked against the writer rather than\n * assumed — an `orderBy` drops documents missing the field, which on a\n * newest-first list is invisible. `apps/tenant/app/api/forms/submit/route.ts`\n * is the only path that creates one and stamps `createdAt: serverTimestamp()`\n * on every add, the v1 API only reads and deletes, and `formSubmissions` is\n * absent from `IMPORTABLE_FIELDS`, so no restore can mint one without it.\n */\nexport function InboxGlanceCard(props: { hostId: string }) {\n const { hostId } = props\n const firestore = useFirestore()\n const { orgSlug, host } = useParams<{ orgSlug: string; host: string }>()\n const { data: submissionDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'formSubmissions'),\n orderBy('createdAt', 'desc'),\n limit(PREVIEW_ROWS + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n /*\n * How many leads are waiting to be worked (AGL-2622), so the dashboard\n * says where the Inbox's captures went and the CRM's Leads list is one\n * click from here. Two server-side counts rather than a window: a lead\n * nobody has touched carries no status field at all — it is `new` by\n * absence, the rule `crmLeadStatus` reads — and Firestore can neither\n * count an absent field nor exclude the closed statuses without dropping\n * the unstamped rows too. So the open count is every lead less the ones\n * closed by a stamped status — `openLeadsFromCounts`, the subtraction the\n * CRM's own glance card makes — and both counts are aggregations that cost\n * one read per thousand index entries, not one per lead. `null` until\n * they land, and left null on a refused read so the line is withheld\n * rather than drawn as zero.\n */\n const routeContext = useRecordRouteContext()\n const leadsHref = routeContext\n ? pluginRecordListHref('lead', routeContext)\n : null\n const [openLeads, setOpenLeads] = useState<number | null>(null)\n useEffect(() => {\n let active = true\n const leads = collection(firestore, 'hosts', hostId, 'leads')\n void Promise.all([\n getCountFromServer(query(leads)),\n getCountFromServer(query(leads, where('status', 'in', CRM_LEAD_CLOSED_STATUSES))),\n ])\n .then(([all, closed]) => {\n if (!active) return\n setOpenLeads(openLeadsFromCounts(all.data().count, closed.data().count))\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId])\n\n const submissions = submissionDocs ?? []\n if (!submissions.length && !openLeads) return null\n const rows = submissions.slice(0, PREVIEW_ROWS)\n const unread = rows.filter((submission: any) => !submission.read).length\n\n return (\n <CardDisplay\n header={'Inbox'}\n help={pluginDocsHelp('forms', {\n anchor: '#the-inbox',\n excerpt:\n 'The newest form submissions on this site — the Inbox has the ' +\n 'full list, the reader, and the routing each one took.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={buildRoute(Route.HOST_INBOX, { orgSlug, host })}\n size=\"small\"\n color=\"primary\"\n >\n {'Open inbox'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1}>\n {rows.map((submission: any) => {\n const sender = submissionSender(submission.fields)\n const hue = senderHue(sender.label)\n return (\n <Stack\n key={submission.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n {/*\n The unread DOT, as the Inbox itself draws it — same fact, same\n shape, so a reader recognizes the row when they open it.\n */}\n <Box\n aria-label={submission.read ? undefined : 'Unread'}\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: submission.read ? 'transparent' : 'primary.main',\n flexShrink: 0,\n }}\n />\n <Avatar\n sx={{\n width: 28,\n height: 28,\n typography: 'caption',\n bgcolor: `hsl(${hue} 55% 45%)`,\n }}\n >\n {sender.initials}\n </Avatar>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {sender.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {submission.formName ?? 'Form'}\n </Typography>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {relativeTime(submission.createdAt?.toDate?.().getTime())}\n </Typography>\n </Stack>\n )\n })}\n {/*\n What the card is NOT showing. `submissions` holds the probe row, so\n a fourth document is a fact rather than an inference from a full\n page — and the unread count is stated over the rows on screen, not\n over the collection, because counting the collection is a read this\n card has no reason to pay for.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n unread ? `${unread} unread here` : null,\n submissions.length > PREVIEW_ROWS ? 'more in the Inbox' : null,\n ]\n .filter(Boolean)\n .join(' · ') || 'All caught up'}\n </Typography>\n {/*\n Where the captures went (AGL-2622): the leads still to be worked,\n and the Leads list where they are, which the plugin that keeps\n them publishes. Text alone until the route params settle or where\n no plugin publishes one, so the line never links to nowhere.\n */}\n {openLeads ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${openLeads.toLocaleString()} open lead${openLeads === 1 ? '' : 's'} · `}\n {leadsHref ? (\n <AppLink href={leadsHref}>\n {'Work them in the CRM'}\n </AppLink>\n ) : (\n 'in the CRM'\n )}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nInboxGlanceCard.displayName = 'InboxGlanceCard'\n\nexport default InboxGlanceCard\n"],"names":["buildRoute","CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","pluginDocsHelp","Route","pluginRecordListHref","AppLink","CardDisplay","Avatar","Box","Button","Stack","Typography","collection","getCountFromServer","limit","orderBy","query","where","useParams","useEffect","useState","useFirestore","useFirestoreCollection","useRecordRouteContext","relativeTime","senderHue","submissionSender","PREVIEW_ROWS","InboxGlanceCard","props","hostId","firestore","orgSlug","host","data","submissionDocs","idField","routeContext","leadsHref","openLeads","setOpenLeads","active","leads","Promise","all","then","closed","count","catch","undefined","submissions","length","rows","slice","unread","filter","submission","read","header","help","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","HOST_INBOX","size","color","spacing","map","sender","fields","hue","label","direction","sx","alignItems","aria-label","width","height","borderRadius","bgcolor","flexShrink","typography","initials","flex","minWidth","variant","noWrap","formName","createdAt","toDate","getTime","$id","Boolean","join","toLocaleString","displayName"],"mappings":"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"}
|