@odla-ai/chapter 0.25.6 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -13
- package/dist/{chunk-5SWVMJZN.js → chunk-4QZEAWQL.js} +183 -45
- package/dist/chunk-4QZEAWQL.js.map +1 -0
- package/dist/{chunk-WGQO4WDJ.js → chunk-OIWCKF2G.js} +20 -3
- package/dist/chunk-OIWCKF2G.js.map +1 -0
- package/dist/{chunk-WHA3MUDQ.js → chunk-QBTFQUDL.js} +2 -2
- package/dist/{copy-context-DI21CYQ3.d.ts → copy-context-CLvhJhrF.d.ts} +73 -42
- package/dist/index.cjs +327 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -58
- package/dist/index.d.ts +170 -58
- package/dist/index.js +321 -20
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +8 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +552 -208
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +76 -42
- package/dist/worker/index.d.ts +76 -42
- package/dist/worker/index.js +558 -209
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
- package/runbooks/adopt-existing.md +12 -1
- package/runbooks/greenfield.md +29 -7
- package/dist/chunk-5SWVMJZN.js.map +0 -1
- package/dist/chunk-WGQO4WDJ.js.map +0 -1
- /package/dist/{chunk-WHA3MUDQ.js.map → chunk-QBTFQUDL.js.map} +0 -0
package/README.md
CHANGED
|
@@ -61,8 +61,10 @@ Chapter-owned application, admin, CRM, payment, and scheduling behavior.
|
|
|
61
61
|
the browser UI does not need a second identity or follower registry.
|
|
62
62
|
- **The worker is the package.** `chapterWorker({ chapter })` is the whole
|
|
63
63
|
Cloudflare `ExportedHandler`. It serves `/api/health`, `/api/config`,
|
|
64
|
-
`/api/me`, `/api/crm/*`, `/api/network/shared`; configured leaders
|
|
65
|
-
`/api/admin/network/{targets,push}`.
|
|
64
|
+
`/api/me`, `/api/crm/*`, `/api/network/{shared,snapshot}`; configured leaders
|
|
65
|
+
also get `/api/admin/network/{targets,push,rollup}`. An enabled leader
|
|
66
|
+
formation intake adds `/api/formation/{config,applications}`. Chapter mode
|
|
67
|
+
adds the public
|
|
66
68
|
member surface (`/api/join-config`, `/api/join/resume`, `/api/applications`,
|
|
67
69
|
`/api/schedule/{slots,book}`, `/api/payments/subscription`,
|
|
68
70
|
`/api/webhooks/stripe`). The `/api/admin/*` handlers are registered in both
|
|
@@ -104,7 +106,9 @@ Chapter-owned application, admin, CRM, payment, and scheduling behavior.
|
|
|
104
106
|
config and secret *values* stay in the tenant vault. The standard record panel
|
|
105
107
|
then shows “Share with …” actions for compatible people, businesses, deals,
|
|
106
108
|
or other CRM types. The receiver validates the payload against its own CRM
|
|
107
|
-
config and stores a
|
|
109
|
+
config and stores a structured origin row, so retries update the same local
|
|
110
|
+
record. Delivery state is structured too; display tags remain a migration and
|
|
111
|
+
cohort affordance rather than the machine identity contract.
|
|
108
112
|
Shared identity/business fields move; each site's pipeline, account, and
|
|
109
113
|
billing state remain authoritative locally.
|
|
110
114
|
- **The UI kit, adoptable in pieces.** Three entries, split on the dependency
|
|
@@ -285,6 +289,75 @@ wrap or replace the provisional card just as admin workspaces can be composed.
|
|
|
285
289
|
Nested admin/page/record tabs use fragment anchors by default, not query-string
|
|
286
290
|
state.
|
|
287
291
|
|
|
292
|
+
### Leader portfolio and formation
|
|
293
|
+
|
|
294
|
+
`leaderCrmConfig()` composes generic portfolio records into an existing CRM
|
|
295
|
+
config without replacing its people, businesses, pipelines, templates, or
|
|
296
|
+
relations. Duplicate ids fail at startup:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { defineChapter, leaderCrmConfig } from "@odla-ai/chapter";
|
|
300
|
+
|
|
301
|
+
const existingCrmConfig = {
|
|
302
|
+
types: {
|
|
303
|
+
person: {
|
|
304
|
+
label: "Person",
|
|
305
|
+
labelPlural: "People",
|
|
306
|
+
nameField: "name",
|
|
307
|
+
emailField: "email",
|
|
308
|
+
fields: {
|
|
309
|
+
name: { type: "string", required: true },
|
|
310
|
+
email: { type: "email" },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const chapter = defineChapter({
|
|
317
|
+
id: "example-hub",
|
|
318
|
+
name: "Example Hub",
|
|
319
|
+
mode: "hub",
|
|
320
|
+
crm: leaderCrmConfig({ base: existingCrmConfig }),
|
|
321
|
+
formation: {},
|
|
322
|
+
network: {
|
|
323
|
+
targets: [{
|
|
324
|
+
id: "example-chapter",
|
|
325
|
+
name: "Example Chapter",
|
|
326
|
+
url: "https://chapter.example.com",
|
|
327
|
+
fields: { person: ["name", "email"] },
|
|
328
|
+
}],
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The preset adds `chapter`, `chapter_application`, and `deal` record types in
|
|
334
|
+
the `portfolio` workspace, with formation, operating, and deal-flow pipelines.
|
|
335
|
+
It also composes relations to base `person` and `company` types when present.
|
|
336
|
+
These are ordinary CRM records: the host may add fields, types, render slots,
|
|
337
|
+
and relations by composing its own config before passing it to Chapter.
|
|
338
|
+
|
|
339
|
+
`formation: {}` opts into a host-rendered public application:
|
|
340
|
+
|
|
341
|
+
- `GET /api/formation/config` returns only configured public field metadata.
|
|
342
|
+
- `POST /api/formation/applications` accepts only those fields and creates a
|
|
343
|
+
normal `chapter_application` CRM record.
|
|
344
|
+
- A stable base64url `submissionId` makes ambiguous retries resolve to the same
|
|
345
|
+
record. Hosts must generate one per form journey and retain it until the
|
|
346
|
+
response is known.
|
|
347
|
+
- Request size, string length, required fields, and public field names are
|
|
348
|
+
bounded by `formation`; internal fields such as notes are not accepted.
|
|
349
|
+
|
|
350
|
+
Formation is off unless configured. Chapter intentionally does not ship a
|
|
351
|
+
public form design: the site renders the fields in its own voice and brand.
|
|
352
|
+
|
|
353
|
+
For a hub with network targets, the standard admin catalog is Dashboard,
|
|
354
|
+
People, Portfolio, and Settings. Dashboard Overview renders aggregate network
|
|
355
|
+
health; People contains CRM types whose `workspace` is absent or `"people"`;
|
|
356
|
+
Portfolio contains types marked `workspace: "portfolio"`. Operational details
|
|
357
|
+
remain nested page tabs and record operations remain record tabs. A
|
|
358
|
+
`workspaces` transform can preserve or extend this catalog just like the
|
|
359
|
+
standard follower console.
|
|
360
|
+
|
|
288
361
|
### Leader → follower delivery
|
|
289
362
|
|
|
290
363
|
The leader declares where records may go and exactly which fields each follower
|
|
@@ -316,19 +389,22 @@ Treat the network as websites connected by explicit delivery edges:
|
|
|
316
389
|
- each `network.targets[]` entry is a directed edge from one leader to one
|
|
317
390
|
follower: `{ id, name, url, secretName?, fields }`;
|
|
318
391
|
- the payload is a versioned, allowlisted business record
|
|
319
|
-
`{ version,
|
|
320
|
-
another site's pipeline/account/billing state, or
|
|
392
|
+
`{ version: 2, source: { siteId, recordId }, type, input }`, never an
|
|
393
|
+
identity-provider session, another site's pipeline/account/billing state, or
|
|
394
|
+
an ODLA admin key;
|
|
321
395
|
- retries address the follower record by leader provenance, so the same edge
|
|
322
396
|
updates rather than duplicates.
|
|
323
397
|
|
|
324
398
|
The authorization chain has three separate hops. The leader's browser sends
|
|
325
399
|
its own Clerk session only to the leader's admin route. The leader Worker
|
|
326
400
|
authorizes that local operator, reads the edge's share secret from its own
|
|
327
|
-
vault, and
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Clerk
|
|
401
|
+
vault, and signs the exact method, path/query, sender id, timestamp, nonce, and
|
|
402
|
+
body digest. The follower validates that HMAC envelope against its vaulted
|
|
403
|
+
copy, requires the signed sender to match the v2 payload source, validates the
|
|
404
|
+
payload against its own CRM config, and writes with its own server-side
|
|
405
|
+
`ODLA_API_KEY`. The share secret itself is never sent. A follower Clerk token
|
|
406
|
+
is never needed, the leader never receives the follower's database key, and
|
|
407
|
+
sites do not share a Clerk application.
|
|
332
408
|
|
|
333
409
|
Vault the same random value as `network_share_secret` in the follower and as
|
|
334
410
|
`network_share_example_chapter` in the leader (or set a different
|
|
@@ -337,6 +413,25 @@ config. The standard collection drawer discovers the target and calls the
|
|
|
337
413
|
admin-gated push route. A follower must declare the receiving CRM type/fields;
|
|
338
414
|
otherwise it rejects the record cleanly instead of dropping fields.
|
|
339
415
|
|
|
416
|
+
The signed v2 protocol is the contract for new integrations. The follower
|
|
417
|
+
temporarily accepts the old bearer/v1 envelope so an existing edge can be
|
|
418
|
+
upgraded without downtime; new leaders must not use it.
|
|
419
|
+
|
|
420
|
+
Every received v2 record gets a `crm_record_origin` row keyed by the upstream
|
|
421
|
+
site and record id. Every push attempt gets a `crm_record_delivery` row keyed by
|
|
422
|
+
the local record and target, including attempt count, current status, payload
|
|
423
|
+
version, remote record id, and bounded latest error. Record detail responses
|
|
424
|
+
include both collections and the standard record drawer adds a Network tab
|
|
425
|
+
when either exists.
|
|
426
|
+
|
|
427
|
+
`GET /api/network/snapshot` is an HMAC-signed, aggregate-only follower endpoint.
|
|
428
|
+
It returns type totals and pipeline-stage counts, never record fields, people,
|
|
429
|
+
sessions, Clerk tokens, share secrets, or ODLA keys. The leader's admin-gated
|
|
430
|
+
`GET /api/admin/network/rollup` fans out to every configured target, verifies
|
|
431
|
+
that each returned site id equals the target id, and returns combined totals
|
|
432
|
+
plus explicit per-target failures. Consequently each target `id` must equal the
|
|
433
|
+
follower's `chapter.id`.
|
|
434
|
+
|
|
340
435
|
#### Existing follower CRMs must opt into the shared graph
|
|
341
436
|
|
|
342
437
|
When `crm` is omitted, chapter's default already contains compatible `person`
|
|
@@ -436,7 +531,8 @@ data.
|
|
|
436
531
|
|
|
437
532
|
Use distinct development tenants and follower origins for the first delivery:
|
|
438
533
|
|
|
439
|
-
1.
|
|
534
|
+
1. Confirm every target `id` exactly equals the follower's `chapter.id`. Vault
|
|
535
|
+
one random value as `network_share_secret` in the follower and under
|
|
440
536
|
the target's resolved `secretName` in the leader.
|
|
441
537
|
2. Confirm `GET /api/admin/network/targets` lists the development follower with
|
|
442
538
|
the expected compatible record types.
|
|
@@ -451,7 +547,11 @@ Use distinct development tenants and follower origins for the first delivery:
|
|
|
451
547
|
without writing CRM data.
|
|
452
548
|
7. Prove each browser JWT is accepted only by its own website. Inspect the
|
|
453
549
|
network request and client bundle to confirm neither site's Clerk token nor
|
|
454
|
-
either `ODLA_API_KEY` crosses the delivery edge
|
|
550
|
+
either `ODLA_API_KEY` crosses the delivery edge, and confirm the share
|
|
551
|
+
secret itself is absent from the request.
|
|
552
|
+
8. Load `GET /api/admin/network/rollup` through the leader session. Confirm
|
|
553
|
+
aggregate totals, then break one development edge and confirm that target is
|
|
554
|
+
reported unavailable without hiding healthy targets.
|
|
455
555
|
|
|
456
556
|
Only after this contract passes against development origins should the leader
|
|
457
557
|
target be changed to a production follower origin and the matching production
|
|
@@ -468,7 +568,7 @@ Legacy `?tab=` and path links still open, while `routing="query"` and
|
|
|
468
568
|
|
|
469
569
|
### Admin information architecture and migration
|
|
470
570
|
|
|
471
|
-
The default
|
|
571
|
+
The default follower console deliberately has only three top-level workspaces:
|
|
472
572
|
|
|
473
573
|
- **Dashboard** — Overview and Billing page tabs.
|
|
474
574
|
- **People** — configured CRM collections as page tabs, then a master/detail
|
|
@@ -476,6 +576,11 @@ The default chapter console deliberately has only three top-level workspaces:
|
|
|
476
576
|
Connections, Access, and Sharing record tabs when those capabilities apply.
|
|
477
577
|
- **Settings** — Calendar and Email page tabs.
|
|
478
578
|
|
|
579
|
+
A leader hub with configured targets adds **Portfolio** between People and
|
|
580
|
+
Settings and uses Dashboard Overview for the aggregate network rollup. A hub
|
|
581
|
+
without targets retains the minimal People-only default. These defaults compose
|
|
582
|
+
the same grammar; they do not introduce a second admin shell.
|
|
583
|
+
|
|
479
584
|
The default `chrome="embedded"` assumes the host supplies the site header.
|
|
480
585
|
Use `chrome="standalone"` for Chapter's packaged masthead or `renderHeader` for
|
|
481
586
|
a custom header rendered inside the Chapter theme boundary. `editorial`,
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
DEFAULT_CHAPTER_COPY,
|
|
5
5
|
formatChapterCopy,
|
|
6
6
|
useChapterCopy
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-OIWCKF2G.js";
|
|
8
8
|
|
|
9
9
|
// src/ui/admin.tsx
|
|
10
10
|
import { useEffect as useEffect8, useMemo, useRef, useState as useState12 } from "preact/hooks";
|
|
@@ -177,6 +177,7 @@ function Gate(props) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// src/ui/admin-route.ts
|
|
180
|
+
import { parseCrmDiscussionReference } from "@odla-ai/crm";
|
|
180
181
|
var DEFAULT_ALIASES = {
|
|
181
182
|
billing: { workspaceId: "dashboard", viewId: "billing" },
|
|
182
183
|
meetings: { workspaceId: "dashboard" },
|
|
@@ -218,6 +219,14 @@ function adminRouteFromUrl(url, basePath, workspaceIds, aliases = {}) {
|
|
|
218
219
|
const hashParts = url.hash.replace(/^#\/?/, "").split("/").filter(Boolean);
|
|
219
220
|
const fromHash = resolve(fromParts(hashParts), workspaceIds, aliases);
|
|
220
221
|
if (fromHash) return fromHash;
|
|
222
|
+
const reference = parseCrmDiscussionReference(url);
|
|
223
|
+
const fromReference = reference ? resolve({
|
|
224
|
+
workspaceId: "people",
|
|
225
|
+
viewId: reference.type,
|
|
226
|
+
recordId: reference.recordId,
|
|
227
|
+
detailTab: reference.kind === "crm:activity" ? "notes" : "profile"
|
|
228
|
+
}, workspaceIds, aliases) : null;
|
|
229
|
+
if (fromReference) return fromReference;
|
|
221
230
|
const tab = url.searchParams.get("tab");
|
|
222
231
|
const fromQuery = resolve(
|
|
223
232
|
tab ? {
|
|
@@ -843,12 +852,15 @@ function NetworkShareActions(props) {
|
|
|
843
852
|
// src/ui/admin-record-activity.tsx
|
|
844
853
|
import { useCallback as useCallback3, useEffect as useEffect5, useState as useState8 } from "preact/hooks";
|
|
845
854
|
import { ActivityFeed } from "@odla-ai/crm/ui";
|
|
855
|
+
import { parseCrmDiscussionReference as parseCrmDiscussionReference2 } from "@odla-ai/crm";
|
|
846
856
|
import { jsx as jsx10 } from "preact/jsx-runtime";
|
|
847
857
|
function RecordActivityPanel(props) {
|
|
848
858
|
const { client, recordId } = props;
|
|
849
859
|
const [activities, setActivities] = useState8([]);
|
|
850
860
|
const [busy, setBusy] = useState8(false);
|
|
851
861
|
const [error, setError] = useState8(null);
|
|
862
|
+
const reference = typeof window === "undefined" ? null : parseCrmDiscussionReference2(new URL(window.location.href));
|
|
863
|
+
const focusedActivityId = reference?.kind === "crm:activity" && reference.recordId === recordId ? reference.activityId : void 0;
|
|
852
864
|
const load = useCallback3(async () => {
|
|
853
865
|
try {
|
|
854
866
|
const result = await client.listActivities(recordId, { limit: 100 });
|
|
@@ -873,6 +885,7 @@ function RecordActivityPanel(props) {
|
|
|
873
885
|
ActivityFeed,
|
|
874
886
|
{
|
|
875
887
|
activities,
|
|
888
|
+
focusedActivityId,
|
|
876
889
|
busy,
|
|
877
890
|
onAddNote: (body) => void run(() => client.addActivity(recordId, { kind: "note", body })),
|
|
878
891
|
onAddTask: (task) => void run(() => client.addActivity(recordId, { kind: "task", ...task })),
|
|
@@ -1242,8 +1255,46 @@ function peopleSection(options) {
|
|
|
1242
1255
|
});
|
|
1243
1256
|
}
|
|
1244
1257
|
|
|
1258
|
+
// src/ui/admin-portfolio.tsx
|
|
1259
|
+
import { StatBand as StatBand2, StepPipeline as StepPipeline2 } from "@odla-ai/ui/components";
|
|
1260
|
+
import { jsx as jsx15, jsxs as jsxs12 } from "preact/jsx-runtime";
|
|
1261
|
+
function LeaderNetworkOverview({ ctx }) {
|
|
1262
|
+
const copy = useChapterCopy().admin.network;
|
|
1263
|
+
const { data, loading, error } = useAdminResource(
|
|
1264
|
+
ctx.getToken,
|
|
1265
|
+
"/api/admin/network/rollup"
|
|
1266
|
+
);
|
|
1267
|
+
if (loading) return /* @__PURE__ */ jsx15(AdminNote, { children: copy.loadingRollup });
|
|
1268
|
+
if (error || !data) return /* @__PURE__ */ jsxs12(AdminNote, { children: [
|
|
1269
|
+
copy.loadRollupFailed,
|
|
1270
|
+
error ? `: ${error}` : "",
|
|
1271
|
+
"."
|
|
1272
|
+
] });
|
|
1273
|
+
if (data.configured === 0) return /* @__PURE__ */ jsx15(AdminPage, { children: /* @__PURE__ */ jsx15(Panel, { title: copy.overview, children: /* @__PURE__ */ jsx15("p", { className: "muted", children: copy.noFollowers }) }) });
|
|
1274
|
+
const stats = [
|
|
1275
|
+
{ value: data.configured.toLocaleString(), label: copy.configuredFollowers },
|
|
1276
|
+
{ value: data.available.toLocaleString(), label: copy.reachableFollowers },
|
|
1277
|
+
{ value: data.totalRecords.toLocaleString(), label: copy.recordsAcrossFollowers }
|
|
1278
|
+
];
|
|
1279
|
+
const steps = data.types.map((type) => ({
|
|
1280
|
+
icon: /* @__PURE__ */ jsx15("span", { style: { fontSize: 22, fontWeight: 700 }, children: type.total }),
|
|
1281
|
+
title: type.type.replaceAll("_", " "),
|
|
1282
|
+
description: Object.entries(type.stages).filter(([, count]) => count > 0).map(([stage, count]) => `${stage.replaceAll("_", " ")} ${count}`).join(" \xB7 ") || copy.noRecordTotals
|
|
1283
|
+
}));
|
|
1284
|
+
return /* @__PURE__ */ jsxs12(AdminPage, { children: [
|
|
1285
|
+
/* @__PURE__ */ jsx15(StatBand2, { stats }),
|
|
1286
|
+
/* @__PURE__ */ jsx15(Panel, { title: copy.followers, children: /* @__PURE__ */ jsx15("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 10 }, children: data.targets.map((target) => /* @__PURE__ */ jsxs12("li", { style: { display: "flex", gap: 12, alignItems: "baseline", flexWrap: "wrap" }, children: [
|
|
1287
|
+
/* @__PURE__ */ jsx15("strong", { style: { minWidth: 180 }, children: target.name }),
|
|
1288
|
+
/* @__PURE__ */ jsx15("span", { className: "badge", children: target.available ? copy.available : copy.unavailable }),
|
|
1289
|
+
target.error ? /* @__PURE__ */ jsx15("span", { className: "muted", children: target.error }) : null,
|
|
1290
|
+
/* @__PURE__ */ jsx15("a", { href: target.url, target: "_blank", rel: "noreferrer", style: { marginLeft: "auto" }, children: target.url })
|
|
1291
|
+
] }, target.id)) }) }),
|
|
1292
|
+
/* @__PURE__ */ jsx15(Panel, { title: copy.recordTotals, children: steps.length ? /* @__PURE__ */ jsx15(StepPipeline2, { ariaLabel: copy.recordTotals, steps }) : /* @__PURE__ */ jsx15("p", { className: "muted", children: copy.noRecordTotals }) })
|
|
1293
|
+
] });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1245
1296
|
// src/ui/admin-workspaces.tsx
|
|
1246
|
-
import { jsx as
|
|
1297
|
+
import { jsx as jsx16 } from "preact/jsx-runtime";
|
|
1247
1298
|
var adminViewContext = (ctx, viewId, active) => {
|
|
1248
1299
|
const route = {
|
|
1249
1300
|
workspaceId: ctx.route.workspaceId,
|
|
@@ -1269,7 +1320,7 @@ function DashboardWorkspace({ chapter, ctx }) {
|
|
|
1269
1320
|
const active = ctx.route.viewId === "billing" ? "billing" : "overview";
|
|
1270
1321
|
const overview = dashboardSection({ id: "overview", label: copy.workspaces.overview });
|
|
1271
1322
|
const billing = billingSection({ label: copy.workspaces.billing });
|
|
1272
|
-
return /* @__PURE__ */
|
|
1323
|
+
return /* @__PURE__ */ jsx16(
|
|
1273
1324
|
Tabs2,
|
|
1274
1325
|
{
|
|
1275
1326
|
ariaLabel: copy.workspaces.dashboardViewsLabel,
|
|
@@ -1297,7 +1348,7 @@ function SettingsWorkspace({ chapter, ctx }) {
|
|
|
1297
1348
|
const active = ctx.route.viewId === "email" ? "email" : "calendar";
|
|
1298
1349
|
const calendar = availabilitySection({ id: "calendar", label: copy.workspaces.calendar });
|
|
1299
1350
|
const email = emailSection({ label: copy.workspaces.email });
|
|
1300
|
-
return /* @__PURE__ */
|
|
1351
|
+
return /* @__PURE__ */ jsx16(
|
|
1301
1352
|
Tabs2,
|
|
1302
1353
|
{
|
|
1303
1354
|
ariaLabel: copy.workspaces.settingsViewsLabel,
|
|
@@ -1321,11 +1372,11 @@ function SettingsWorkspace({ chapter, ctx }) {
|
|
|
1321
1372
|
);
|
|
1322
1373
|
}
|
|
1323
1374
|
function PeopleWorkspace({ chapter, ctx }) {
|
|
1324
|
-
const types = Object.entries(chapter.crm.config.types);
|
|
1375
|
+
const types = Object.entries(chapter.crm.config.types).filter(([, def]) => (def.workspace ?? "people") === "people");
|
|
1325
1376
|
const fallback = types[0]?.[0] ?? "person";
|
|
1326
1377
|
const active = types.some(([type]) => type === ctx.route.viewId) ? ctx.route.viewId : fallback;
|
|
1327
1378
|
const networkSharing = chapter.network.targets.length > 0;
|
|
1328
|
-
return /* @__PURE__ */
|
|
1379
|
+
return /* @__PURE__ */ jsx16(
|
|
1329
1380
|
Tabs2,
|
|
1330
1381
|
{
|
|
1331
1382
|
ariaLabel: chapter.copy.admin.workspaces.collectionsLabel,
|
|
@@ -1350,38 +1401,124 @@ function PeopleWorkspace({ chapter, ctx }) {
|
|
|
1350
1401
|
}
|
|
1351
1402
|
);
|
|
1352
1403
|
}
|
|
1404
|
+
function PortfolioWorkspace({ chapter, ctx }) {
|
|
1405
|
+
const copy = chapter.copy.admin;
|
|
1406
|
+
const types = Object.entries(chapter.crm.config.types).filter(([, def]) => def.workspace === "portfolio");
|
|
1407
|
+
const fallback = types[0]?.[0] ?? "";
|
|
1408
|
+
const active = types.some(([type]) => type === ctx.route.viewId) ? ctx.route.viewId : fallback;
|
|
1409
|
+
if (!active) return /* @__PURE__ */ jsx16(LeaderNetworkOverview, { ctx });
|
|
1410
|
+
return /* @__PURE__ */ jsx16(
|
|
1411
|
+
Tabs2,
|
|
1412
|
+
{
|
|
1413
|
+
ariaLabel: copy.workspaces.portfolioViewsLabel,
|
|
1414
|
+
value: active,
|
|
1415
|
+
variant: "pill",
|
|
1416
|
+
mount: "active",
|
|
1417
|
+
items: types.map(([type, def]) => {
|
|
1418
|
+
const section = collectionSection({
|
|
1419
|
+
crm: chapter.crm,
|
|
1420
|
+
type,
|
|
1421
|
+
networkSharing: chapter.network.targets.length > 0
|
|
1422
|
+
});
|
|
1423
|
+
return {
|
|
1424
|
+
value: type,
|
|
1425
|
+
label: def.labelPlural ?? def.label,
|
|
1426
|
+
href: ctx.href({ viewId: type, recordId: null, detailTab: null }),
|
|
1427
|
+
panel: section.render(adminViewContext(ctx, type, active === type))
|
|
1428
|
+
};
|
|
1429
|
+
})
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
function LeaderDashboardWorkspace({ chapter, ctx }) {
|
|
1434
|
+
return /* @__PURE__ */ jsx16(
|
|
1435
|
+
Tabs2,
|
|
1436
|
+
{
|
|
1437
|
+
ariaLabel: chapter.copy.admin.workspaces.dashboardViewsLabel,
|
|
1438
|
+
value: "overview",
|
|
1439
|
+
mount: "active",
|
|
1440
|
+
items: [{
|
|
1441
|
+
value: "overview",
|
|
1442
|
+
label: chapter.copy.admin.workspaces.overview,
|
|
1443
|
+
href: ctx.href({ viewId: "overview", recordId: null, detailTab: null }),
|
|
1444
|
+
panel: /* @__PURE__ */ jsx16(LeaderNetworkOverview, { ctx: adminViewContext(ctx, "overview", true) })
|
|
1445
|
+
}]
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
function LeaderSettingsWorkspace({ chapter, ctx }) {
|
|
1450
|
+
const email = emailSection({ label: chapter.copy.admin.workspaces.email });
|
|
1451
|
+
return /* @__PURE__ */ jsx16(
|
|
1452
|
+
Tabs2,
|
|
1453
|
+
{
|
|
1454
|
+
ariaLabel: chapter.copy.admin.workspaces.settingsViewsLabel,
|
|
1455
|
+
value: "email",
|
|
1456
|
+
mount: "active",
|
|
1457
|
+
items: [{
|
|
1458
|
+
value: "email",
|
|
1459
|
+
label: chapter.copy.admin.workspaces.email,
|
|
1460
|
+
href: ctx.href({ viewId: "email", recordId: null, detailTab: null }),
|
|
1461
|
+
panel: email.render(adminViewContext(ctx, "email", true))
|
|
1462
|
+
}]
|
|
1463
|
+
}
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1353
1466
|
function defaultAdminWorkspaces(chapter) {
|
|
1354
1467
|
const copy = chapter.copy.admin;
|
|
1355
1468
|
const people = {
|
|
1356
1469
|
id: "people",
|
|
1357
1470
|
label: copy.workspaces.people,
|
|
1358
1471
|
defaultViewId: Object.keys(chapter.crm.config.types)[0] ?? "person",
|
|
1359
|
-
render: (ctx) => /* @__PURE__ */
|
|
1472
|
+
render: (ctx) => /* @__PURE__ */ jsx16(PeopleWorkspace, { chapter, ctx })
|
|
1360
1473
|
};
|
|
1361
|
-
if (chapter.mode === "hub") return [people];
|
|
1474
|
+
if (chapter.mode === "hub" && chapter.network.targets.length === 0) return [people];
|
|
1475
|
+
if (chapter.mode === "hub") {
|
|
1476
|
+
const portfolioTypes = Object.entries(chapter.crm.config.types).filter(([, def]) => def.workspace === "portfolio");
|
|
1477
|
+
return [
|
|
1478
|
+
{
|
|
1479
|
+
id: "dashboard",
|
|
1480
|
+
label: copy.workspaces.dashboard,
|
|
1481
|
+
defaultViewId: "overview",
|
|
1482
|
+
render: (ctx) => /* @__PURE__ */ jsx16(LeaderDashboardWorkspace, { chapter, ctx })
|
|
1483
|
+
},
|
|
1484
|
+
people,
|
|
1485
|
+
{
|
|
1486
|
+
id: "portfolio",
|
|
1487
|
+
label: copy.workspaces.portfolio,
|
|
1488
|
+
defaultViewId: portfolioTypes[0]?.[0],
|
|
1489
|
+
render: (ctx) => /* @__PURE__ */ jsx16(PortfolioWorkspace, { chapter, ctx })
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
id: "settings",
|
|
1493
|
+
label: copy.workspaces.settings,
|
|
1494
|
+
defaultViewId: "email",
|
|
1495
|
+
render: (ctx) => /* @__PURE__ */ jsx16(LeaderSettingsWorkspace, { chapter, ctx })
|
|
1496
|
+
}
|
|
1497
|
+
];
|
|
1498
|
+
}
|
|
1362
1499
|
return [
|
|
1363
1500
|
{
|
|
1364
1501
|
id: "dashboard",
|
|
1365
1502
|
label: copy.workspaces.dashboard,
|
|
1366
1503
|
defaultViewId: "overview",
|
|
1367
|
-
render: (ctx) => /* @__PURE__ */
|
|
1504
|
+
render: (ctx) => /* @__PURE__ */ jsx16(DashboardWorkspace, { chapter, ctx })
|
|
1368
1505
|
},
|
|
1369
1506
|
people,
|
|
1370
1507
|
{
|
|
1371
1508
|
id: "settings",
|
|
1372
1509
|
label: copy.workspaces.settings,
|
|
1373
1510
|
defaultViewId: "calendar",
|
|
1374
|
-
render: (ctx) => /* @__PURE__ */
|
|
1511
|
+
render: (ctx) => /* @__PURE__ */ jsx16(SettingsWorkspace, { chapter, ctx })
|
|
1375
1512
|
}
|
|
1376
1513
|
];
|
|
1377
1514
|
}
|
|
1378
1515
|
|
|
1379
1516
|
// src/ui/admin-frame.tsx
|
|
1380
1517
|
import { ThemeScope as ThemeScope2 } from "@odla-ai/ui/components";
|
|
1381
|
-
import { jsx as
|
|
1518
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "preact/jsx-runtime";
|
|
1382
1519
|
function AdminTheme(props) {
|
|
1383
1520
|
const brand = props.chapter?.brand;
|
|
1384
|
-
return /* @__PURE__ */
|
|
1521
|
+
return /* @__PURE__ */ jsx17(ChapterCopyProvider, { copy: props.chapter?.copy, children: /* @__PURE__ */ jsxs13(
|
|
1385
1522
|
ThemeScope2,
|
|
1386
1523
|
{
|
|
1387
1524
|
ref: props.scopeRef,
|
|
@@ -1390,7 +1527,7 @@ function AdminTheme(props) {
|
|
|
1390
1527
|
accent: brand?.accent,
|
|
1391
1528
|
colorScheme: brand?.colorScheme ?? "light",
|
|
1392
1529
|
children: [
|
|
1393
|
-
/* @__PURE__ */
|
|
1530
|
+
/* @__PURE__ */ jsx17(BrandStyle, { brand, selector: "[data-chapter-admin]" }),
|
|
1394
1531
|
props.children
|
|
1395
1532
|
]
|
|
1396
1533
|
}
|
|
@@ -1445,7 +1582,7 @@ async function loadChapterAdminUser(adapter, apiBase, getToken) {
|
|
|
1445
1582
|
}
|
|
1446
1583
|
|
|
1447
1584
|
// src/ui/admin.tsx
|
|
1448
|
-
import { jsx as
|
|
1585
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "preact/jsx-runtime";
|
|
1449
1586
|
function Authed(props) {
|
|
1450
1587
|
const copy = useChapterCopy().admin;
|
|
1451
1588
|
const {
|
|
@@ -1492,17 +1629,17 @@ function Authed(props) {
|
|
|
1492
1629
|
live = false;
|
|
1493
1630
|
};
|
|
1494
1631
|
}, [getToken, apiBase, auth]);
|
|
1495
|
-
if (state.status === "checking") return /* @__PURE__ */
|
|
1632
|
+
if (state.status === "checking") return /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.auth.checking }) });
|
|
1496
1633
|
if (state.status === "denied") {
|
|
1497
|
-
return /* @__PURE__ */
|
|
1498
|
-
/* @__PURE__ */
|
|
1499
|
-
/* @__PURE__ */
|
|
1634
|
+
return /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsxs14("div", { className: "card", style: S.card, children: [
|
|
1635
|
+
/* @__PURE__ */ jsx18("h2", { style: { marginTop: 0 }, children: copy.auth.notAuthorized }),
|
|
1636
|
+
/* @__PURE__ */ jsx18("p", { style: S.muted, children: formatChapterCopy(copy.auth.accountNotAuthorized, {
|
|
1500
1637
|
account: state.email ?? copy.auth.thisAccount
|
|
1501
1638
|
}) }),
|
|
1502
|
-
/* @__PURE__ */
|
|
1639
|
+
/* @__PURE__ */ jsx18("button", { className: "btn secondary", onClick: () => signOut(), children: copy.auth.signOut })
|
|
1503
1640
|
] }) });
|
|
1504
1641
|
}
|
|
1505
|
-
return /* @__PURE__ */
|
|
1642
|
+
return /* @__PURE__ */ jsx18(
|
|
1506
1643
|
AdminShell,
|
|
1507
1644
|
{
|
|
1508
1645
|
workspaces,
|
|
@@ -1555,23 +1692,23 @@ function ChapterAdmin(props) {
|
|
|
1555
1692
|
}, [apiBase, auth]);
|
|
1556
1693
|
let content;
|
|
1557
1694
|
if (publishableKey === void 0 || publishableKey && !appearance) {
|
|
1558
|
-
content = /* @__PURE__ */
|
|
1695
|
+
content = /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.admin.auth.loading }) });
|
|
1559
1696
|
} else if (!publishableKey) {
|
|
1560
|
-
content = /* @__PURE__ */
|
|
1561
|
-
/* @__PURE__ */
|
|
1562
|
-
/* @__PURE__ */
|
|
1697
|
+
content = /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsxs14("div", { className: "card", style: S.card, children: [
|
|
1698
|
+
/* @__PURE__ */ jsx18("h2", { style: { marginTop: 0 }, children: copy.admin.auth.signInNotConfigured }),
|
|
1699
|
+
/* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.admin.auth.missingPublishableKey })
|
|
1563
1700
|
] }) });
|
|
1564
1701
|
} else if (workspaces.length === 0) {
|
|
1565
|
-
content = /* @__PURE__ */
|
|
1702
|
+
content = /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.admin.auth.noWorkspaces }) });
|
|
1566
1703
|
} else {
|
|
1567
|
-
content = /* @__PURE__ */
|
|
1704
|
+
content = /* @__PURE__ */ jsxs14(
|
|
1568
1705
|
ClerkGate,
|
|
1569
1706
|
{
|
|
1570
1707
|
publishableKey,
|
|
1571
1708
|
appearance,
|
|
1572
1709
|
afterSignOutUrl: "/",
|
|
1573
1710
|
children: [
|
|
1574
|
-
/* @__PURE__ */
|
|
1711
|
+
/* @__PURE__ */ jsx18(SignedOut, { children: /* @__PURE__ */ jsx18(Gate, { brand, tagline: copy.admin.auth.signInTagline, children: /* @__PURE__ */ jsx18(
|
|
1575
1712
|
SignIn,
|
|
1576
1713
|
{
|
|
1577
1714
|
routing: "hash",
|
|
@@ -1579,7 +1716,7 @@ function ChapterAdmin(props) {
|
|
|
1579
1716
|
signUpForceRedirectUrl: redirect
|
|
1580
1717
|
}
|
|
1581
1718
|
) }) }),
|
|
1582
|
-
/* @__PURE__ */
|
|
1719
|
+
/* @__PURE__ */ jsx18(SignedIn, { children: /* @__PURE__ */ jsx18(
|
|
1583
1720
|
Authed,
|
|
1584
1721
|
{
|
|
1585
1722
|
workspaces,
|
|
@@ -1599,7 +1736,7 @@ function ChapterAdmin(props) {
|
|
|
1599
1736
|
}
|
|
1600
1737
|
);
|
|
1601
1738
|
}
|
|
1602
|
-
return /* @__PURE__ */
|
|
1739
|
+
return /* @__PURE__ */ jsx18(AdminTheme, { chapter: props.chapter, scopeRef, children: content });
|
|
1603
1740
|
}
|
|
1604
1741
|
|
|
1605
1742
|
// src/ui/admin-defaults.ts
|
|
@@ -1610,15 +1747,15 @@ function defaultAdminSections(chapter) {
|
|
|
1610
1747
|
// src/ui/admin-meetings.tsx
|
|
1611
1748
|
import { useState as useState13 } from "preact/hooks";
|
|
1612
1749
|
import { Button as Button6 } from "@odla-ai/ui/components";
|
|
1613
|
-
import { Fragment as Fragment4, jsx as
|
|
1750
|
+
import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs15 } from "preact/jsx-runtime";
|
|
1614
1751
|
var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
|
|
1615
1752
|
function MeetingsBody({ ctx }) {
|
|
1616
1753
|
const copy = useChapterCopy().admin.meetings;
|
|
1617
1754
|
const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
|
|
1618
1755
|
const [busy, setBusy] = useState13(null);
|
|
1619
1756
|
const [note, setNote] = useState13(null);
|
|
1620
|
-
if (loading) return /* @__PURE__ */
|
|
1621
|
-
if (error || !data) return /* @__PURE__ */
|
|
1757
|
+
if (loading) return /* @__PURE__ */ jsx19(AdminNote, { children: copy.loading });
|
|
1758
|
+
if (error || !data) return /* @__PURE__ */ jsxs15(AdminNote, { children: [
|
|
1622
1759
|
copy.loadFailed,
|
|
1623
1760
|
error ? `: ${error}` : "",
|
|
1624
1761
|
"."
|
|
@@ -1649,32 +1786,32 @@ function MeetingsBody({ ctx }) {
|
|
|
1649
1786
|
}
|
|
1650
1787
|
void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
|
|
1651
1788
|
};
|
|
1652
|
-
return /* @__PURE__ */
|
|
1789
|
+
return /* @__PURE__ */ jsx19(AdminPage, { children: /* @__PURE__ */ jsx19(Panel, { title: /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
1653
1790
|
copy.agenda,
|
|
1654
1791
|
" ",
|
|
1655
|
-
/* @__PURE__ */
|
|
1792
|
+
/* @__PURE__ */ jsxs15("span", { className: "muted", style: { fontWeight: 400 }, children: [
|
|
1656
1793
|
"\xB7 ",
|
|
1657
1794
|
data.timezone
|
|
1658
1795
|
] })
|
|
1659
|
-
] }), actions: note ? /* @__PURE__ */
|
|
1660
|
-
/* @__PURE__ */
|
|
1661
|
-
/* @__PURE__ */
|
|
1662
|
-
/* @__PURE__ */
|
|
1663
|
-
m.drift && m.drift !== "none" ? /* @__PURE__ */
|
|
1796
|
+
] }), actions: note ? /* @__PURE__ */ jsx19("span", { className: "muted", children: note }) : void 0, children: data.meetings.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "muted", style: { margin: 0 }, children: copy.empty }) : /* @__PURE__ */ jsx19("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 12 }, children: data.meetings.map((m) => /* @__PURE__ */ jsxs15("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", opacity: m.status === "cancelled" ? 0.55 : 1 }, children: [
|
|
1797
|
+
/* @__PURE__ */ jsx19("strong", { style: { minWidth: 170 }, children: when2(m.startAt) }),
|
|
1798
|
+
/* @__PURE__ */ jsx19("span", { style: { flex: 1 }, children: m.applicant ? `${m.applicant.name} \xB7 ${m.applicant.email}` : copy.unknown }),
|
|
1799
|
+
/* @__PURE__ */ jsx19("span", { className: "badge", children: m.status }),
|
|
1800
|
+
m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs15("span", { className: "badge", children: [
|
|
1664
1801
|
copy.drift,
|
|
1665
1802
|
": ",
|
|
1666
1803
|
m.drift
|
|
1667
1804
|
] }) : null,
|
|
1668
|
-
m.meetUrl ? /* @__PURE__ */
|
|
1669
|
-
m.status === "scheduled" ? /* @__PURE__ */
|
|
1670
|
-
/* @__PURE__ */
|
|
1671
|
-
/* @__PURE__ */
|
|
1805
|
+
m.meetUrl ? /* @__PURE__ */ jsx19("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: copy.meet }) : null,
|
|
1806
|
+
m.status === "scheduled" ? /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
1807
|
+
/* @__PURE__ */ jsx19(Button6, { variant: "secondary", mini: true, disabled: busy === m.id, onClick: () => reschedule(m.id), children: copy.reschedule }),
|
|
1808
|
+
/* @__PURE__ */ jsx19(Button6, { variant: "danger", mini: true, disabled: busy === m.id, onClick: () => cancel(m.id), children: copy.cancel })
|
|
1672
1809
|
] }) : null
|
|
1673
1810
|
] }, m.id)) }) }) });
|
|
1674
1811
|
}
|
|
1675
1812
|
function meetingsSection(options = {}) {
|
|
1676
1813
|
const { id = "meetings", label = "Meetings" } = options;
|
|
1677
|
-
return { id, label, render: (ctx) => /* @__PURE__ */
|
|
1814
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx19(MeetingsBody, { ctx }) };
|
|
1678
1815
|
}
|
|
1679
1816
|
|
|
1680
1817
|
export {
|
|
@@ -1697,6 +1834,7 @@ export {
|
|
|
1697
1834
|
applicationLifecycleAdapter,
|
|
1698
1835
|
collectionSection,
|
|
1699
1836
|
peopleSection,
|
|
1837
|
+
LeaderNetworkOverview,
|
|
1700
1838
|
defaultAdminWorkspaces,
|
|
1701
1839
|
loadChapterAdminConfig,
|
|
1702
1840
|
loadChapterAdminUser,
|
|
@@ -1704,4 +1842,4 @@ export {
|
|
|
1704
1842
|
defaultAdminSections,
|
|
1705
1843
|
meetingsSection
|
|
1706
1844
|
};
|
|
1707
|
-
//# sourceMappingURL=chunk-
|
|
1845
|
+
//# sourceMappingURL=chunk-4QZEAWQL.js.map
|