@odla-ai/chapter 0.25.8 → 0.26.1
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 +148 -15
- package/dist/{chunk-4FW35E3P.js → chunk-4QZEAWQL.js} +170 -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-Vof2eTMt.d.ts} +78 -42
- package/dist/index.cjs +335 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -58
- package/dist/index.d.ts +175 -58
- package/dist/index.js +329 -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 -216
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +78 -42
- package/dist/worker/index.d.ts +78 -42
- package/dist/worker/index.js +557 -216
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
- package/runbooks/adopt-existing.md +12 -1
- package/runbooks/greenfield.md +27 -6
- package/dist/chunk-4FW35E3P.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
|
|
@@ -300,6 +373,7 @@ export const chapter = defineChapter({
|
|
|
300
373
|
id: "example-chapter",
|
|
301
374
|
name: "Example Chapter",
|
|
302
375
|
url: "https://chapter.example.com",
|
|
376
|
+
binding: "EXAMPLE_CHAPTER",
|
|
303
377
|
fields: {
|
|
304
378
|
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
305
379
|
company: ["name", "domain", "industry", "location", "linkedin"],
|
|
@@ -314,21 +388,50 @@ Treat the network as websites connected by explicit delivery edges:
|
|
|
314
388
|
- each website is a node with its own `appId`, ODLA tenants/keys, CRM config,
|
|
315
389
|
Clerk application, publishable keys, issuer, roles, and local users;
|
|
316
390
|
- each `network.targets[]` entry is a directed edge from one leader to one
|
|
317
|
-
follower: `{ id, name, url, secretName?, fields }`;
|
|
391
|
+
follower: `{ id, name, url, binding?, secretName?, fields }`;
|
|
318
392
|
- the payload is a versioned, allowlisted business record
|
|
319
|
-
`{ version,
|
|
320
|
-
another site's pipeline/account/billing state, or
|
|
393
|
+
`{ version: 2, source: { siteId, recordId }, type, input }`, never an
|
|
394
|
+
identity-provider session, another site's pipeline/account/billing state, or
|
|
395
|
+
an ODLA admin key;
|
|
321
396
|
- retries address the follower record by leader provenance, so the same edge
|
|
322
397
|
updates rather than duplicates.
|
|
323
398
|
|
|
324
399
|
The authorization chain has three separate hops. The leader's browser sends
|
|
325
400
|
its own Clerk session only to the leader's admin route. The leader Worker
|
|
326
401
|
authorizes that local operator, reads the edge's share secret from its own
|
|
327
|
-
vault, and
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Clerk
|
|
402
|
+
vault, and signs the exact method, path/query, sender id, timestamp, nonce, and
|
|
403
|
+
body digest. The follower validates that HMAC envelope against its vaulted
|
|
404
|
+
copy, requires the signed sender to match the v2 payload source, validates the
|
|
405
|
+
payload against its own CRM config, and writes with its own server-side
|
|
406
|
+
`ODLA_API_KEY`. The share secret itself is never sent. A follower Clerk token
|
|
407
|
+
is never needed, the leader never receives the follower's database key, and
|
|
408
|
+
sites do not share a Clerk application.
|
|
409
|
+
|
|
410
|
+
When both sites are Cloudflare Workers, configure `binding` and add the matching
|
|
411
|
+
service binding to the leader's Wrangler environment. Cloudflare does not
|
|
412
|
+
reliably dispatch same-account `workers.dev` subrequests into the target
|
|
413
|
+
Worker; Chapter therefore signs against `url` but sends through the bound
|
|
414
|
+
Worker:
|
|
415
|
+
|
|
416
|
+
```jsonc
|
|
417
|
+
{
|
|
418
|
+
"env": {
|
|
419
|
+
"dev": {
|
|
420
|
+
"services": [{
|
|
421
|
+
"binding": "EXAMPLE_CHAPTER",
|
|
422
|
+
"service": "example-chapter-dev"
|
|
423
|
+
}]
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The example assumes the follower sets its development Worker `name` explicitly
|
|
430
|
+
to `example-chapter-dev`. If it instead uses Cloudflare's named-environment
|
|
431
|
+
addressing, declare the base `service` plus `"environment": "dev"`. The binding
|
|
432
|
+
name is public configuration, not a credential. If a configured binding is
|
|
433
|
+
missing at runtime, the target fails explicitly instead of falling back to a
|
|
434
|
+
potentially misrouted public fetch.
|
|
332
435
|
|
|
333
436
|
Vault the same random value as `network_share_secret` in the follower and as
|
|
334
437
|
`network_share_example_chapter` in the leader (or set a different
|
|
@@ -337,6 +440,25 @@ config. The standard collection drawer discovers the target and calls the
|
|
|
337
440
|
admin-gated push route. A follower must declare the receiving CRM type/fields;
|
|
338
441
|
otherwise it rejects the record cleanly instead of dropping fields.
|
|
339
442
|
|
|
443
|
+
The signed v2 protocol is the contract for new integrations. The follower
|
|
444
|
+
temporarily accepts the old bearer/v1 envelope so an existing edge can be
|
|
445
|
+
upgraded without downtime; new leaders must not use it.
|
|
446
|
+
|
|
447
|
+
Every received v2 record gets a `crm_record_origin` row keyed by the upstream
|
|
448
|
+
site and record id. Every push attempt gets a `crm_record_delivery` row keyed by
|
|
449
|
+
the local record and target, including attempt count, current status, payload
|
|
450
|
+
version, remote record id, and bounded latest error. Record detail responses
|
|
451
|
+
include both collections and the standard record drawer adds a Network tab
|
|
452
|
+
when either exists.
|
|
453
|
+
|
|
454
|
+
`GET /api/network/snapshot` is an HMAC-signed, aggregate-only follower endpoint.
|
|
455
|
+
It returns type totals and pipeline-stage counts, never record fields, people,
|
|
456
|
+
sessions, Clerk tokens, share secrets, or ODLA keys. The leader's admin-gated
|
|
457
|
+
`GET /api/admin/network/rollup` fans out to every configured target, verifies
|
|
458
|
+
that each returned site id equals the target id, and returns combined totals
|
|
459
|
+
plus explicit per-target failures. Consequently each target `id` must equal the
|
|
460
|
+
follower's `chapter.id`.
|
|
461
|
+
|
|
340
462
|
#### Existing follower CRMs must opt into the shared graph
|
|
341
463
|
|
|
342
464
|
When `crm` is omitted, chapter's default already contains compatible `person`
|
|
@@ -436,8 +558,10 @@ data.
|
|
|
436
558
|
|
|
437
559
|
Use distinct development tenants and follower origins for the first delivery:
|
|
438
560
|
|
|
439
|
-
1.
|
|
440
|
-
|
|
561
|
+
1. Confirm every target `id` exactly equals the follower's `chapter.id`. For
|
|
562
|
+
Cloudflare Workers, confirm its `binding` exists in the leader's development
|
|
563
|
+
Wrangler environment. Vault one random value as `network_share_secret` in
|
|
564
|
+
the follower and under the target's resolved `secretName` in the leader.
|
|
441
565
|
2. Confirm `GET /api/admin/network/targets` lists the development follower with
|
|
442
566
|
the expected compatible record types.
|
|
443
567
|
3. Share one test person and one test business from the leader's record drawer.
|
|
@@ -451,7 +575,11 @@ Use distinct development tenants and follower origins for the first delivery:
|
|
|
451
575
|
without writing CRM data.
|
|
452
576
|
7. Prove each browser JWT is accepted only by its own website. Inspect the
|
|
453
577
|
network request and client bundle to confirm neither site's Clerk token nor
|
|
454
|
-
either `ODLA_API_KEY` crosses the delivery edge
|
|
578
|
+
either `ODLA_API_KEY` crosses the delivery edge, and confirm the share
|
|
579
|
+
secret itself is absent from the request.
|
|
580
|
+
8. Load `GET /api/admin/network/rollup` through the leader session. Confirm
|
|
581
|
+
aggregate totals, then break one development edge and confirm that target is
|
|
582
|
+
reported unavailable without hiding healthy targets.
|
|
455
583
|
|
|
456
584
|
Only after this contract passes against development origins should the leader
|
|
457
585
|
target be changed to a production follower origin and the matching production
|
|
@@ -468,7 +596,7 @@ Legacy `?tab=` and path links still open, while `routing="query"` and
|
|
|
468
596
|
|
|
469
597
|
### Admin information architecture and migration
|
|
470
598
|
|
|
471
|
-
The default
|
|
599
|
+
The default follower console deliberately has only three top-level workspaces:
|
|
472
600
|
|
|
473
601
|
- **Dashboard** — Overview and Billing page tabs.
|
|
474
602
|
- **People** — configured CRM collections as page tabs, then a master/detail
|
|
@@ -476,6 +604,11 @@ The default chapter console deliberately has only three top-level workspaces:
|
|
|
476
604
|
Connections, Access, and Sharing record tabs when those capabilities apply.
|
|
477
605
|
- **Settings** — Calendar and Email page tabs.
|
|
478
606
|
|
|
607
|
+
A leader hub with configured targets adds **Portfolio** between People and
|
|
608
|
+
Settings and uses Dashboard Overview for the aggregate network rollup. A hub
|
|
609
|
+
without targets retains the minimal People-only default. These defaults compose
|
|
610
|
+
the same grammar; they do not introduce a second admin shell.
|
|
611
|
+
|
|
479
612
|
The default `chrome="embedded"` assumes the host supplies the site header.
|
|
480
613
|
Use `chrome="standalone"` for Chapter's packaged masthead or `renderHeader` for
|
|
481
614
|
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";
|
|
@@ -1255,8 +1255,46 @@ function peopleSection(options) {
|
|
|
1255
1255
|
});
|
|
1256
1256
|
}
|
|
1257
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
|
+
|
|
1258
1296
|
// src/ui/admin-workspaces.tsx
|
|
1259
|
-
import { jsx as
|
|
1297
|
+
import { jsx as jsx16 } from "preact/jsx-runtime";
|
|
1260
1298
|
var adminViewContext = (ctx, viewId, active) => {
|
|
1261
1299
|
const route = {
|
|
1262
1300
|
workspaceId: ctx.route.workspaceId,
|
|
@@ -1282,7 +1320,7 @@ function DashboardWorkspace({ chapter, ctx }) {
|
|
|
1282
1320
|
const active = ctx.route.viewId === "billing" ? "billing" : "overview";
|
|
1283
1321
|
const overview = dashboardSection({ id: "overview", label: copy.workspaces.overview });
|
|
1284
1322
|
const billing = billingSection({ label: copy.workspaces.billing });
|
|
1285
|
-
return /* @__PURE__ */
|
|
1323
|
+
return /* @__PURE__ */ jsx16(
|
|
1286
1324
|
Tabs2,
|
|
1287
1325
|
{
|
|
1288
1326
|
ariaLabel: copy.workspaces.dashboardViewsLabel,
|
|
@@ -1310,7 +1348,7 @@ function SettingsWorkspace({ chapter, ctx }) {
|
|
|
1310
1348
|
const active = ctx.route.viewId === "email" ? "email" : "calendar";
|
|
1311
1349
|
const calendar = availabilitySection({ id: "calendar", label: copy.workspaces.calendar });
|
|
1312
1350
|
const email = emailSection({ label: copy.workspaces.email });
|
|
1313
|
-
return /* @__PURE__ */
|
|
1351
|
+
return /* @__PURE__ */ jsx16(
|
|
1314
1352
|
Tabs2,
|
|
1315
1353
|
{
|
|
1316
1354
|
ariaLabel: copy.workspaces.settingsViewsLabel,
|
|
@@ -1334,11 +1372,11 @@ function SettingsWorkspace({ chapter, ctx }) {
|
|
|
1334
1372
|
);
|
|
1335
1373
|
}
|
|
1336
1374
|
function PeopleWorkspace({ chapter, ctx }) {
|
|
1337
|
-
const types = Object.entries(chapter.crm.config.types);
|
|
1375
|
+
const types = Object.entries(chapter.crm.config.types).filter(([, def]) => (def.workspace ?? "people") === "people");
|
|
1338
1376
|
const fallback = types[0]?.[0] ?? "person";
|
|
1339
1377
|
const active = types.some(([type]) => type === ctx.route.viewId) ? ctx.route.viewId : fallback;
|
|
1340
1378
|
const networkSharing = chapter.network.targets.length > 0;
|
|
1341
|
-
return /* @__PURE__ */
|
|
1379
|
+
return /* @__PURE__ */ jsx16(
|
|
1342
1380
|
Tabs2,
|
|
1343
1381
|
{
|
|
1344
1382
|
ariaLabel: chapter.copy.admin.workspaces.collectionsLabel,
|
|
@@ -1363,38 +1401,124 @@ function PeopleWorkspace({ chapter, ctx }) {
|
|
|
1363
1401
|
}
|
|
1364
1402
|
);
|
|
1365
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
|
+
}
|
|
1366
1466
|
function defaultAdminWorkspaces(chapter) {
|
|
1367
1467
|
const copy = chapter.copy.admin;
|
|
1368
1468
|
const people = {
|
|
1369
1469
|
id: "people",
|
|
1370
1470
|
label: copy.workspaces.people,
|
|
1371
1471
|
defaultViewId: Object.keys(chapter.crm.config.types)[0] ?? "person",
|
|
1372
|
-
render: (ctx) => /* @__PURE__ */
|
|
1472
|
+
render: (ctx) => /* @__PURE__ */ jsx16(PeopleWorkspace, { chapter, ctx })
|
|
1373
1473
|
};
|
|
1374
|
-
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
|
+
}
|
|
1375
1499
|
return [
|
|
1376
1500
|
{
|
|
1377
1501
|
id: "dashboard",
|
|
1378
1502
|
label: copy.workspaces.dashboard,
|
|
1379
1503
|
defaultViewId: "overview",
|
|
1380
|
-
render: (ctx) => /* @__PURE__ */
|
|
1504
|
+
render: (ctx) => /* @__PURE__ */ jsx16(DashboardWorkspace, { chapter, ctx })
|
|
1381
1505
|
},
|
|
1382
1506
|
people,
|
|
1383
1507
|
{
|
|
1384
1508
|
id: "settings",
|
|
1385
1509
|
label: copy.workspaces.settings,
|
|
1386
1510
|
defaultViewId: "calendar",
|
|
1387
|
-
render: (ctx) => /* @__PURE__ */
|
|
1511
|
+
render: (ctx) => /* @__PURE__ */ jsx16(SettingsWorkspace, { chapter, ctx })
|
|
1388
1512
|
}
|
|
1389
1513
|
];
|
|
1390
1514
|
}
|
|
1391
1515
|
|
|
1392
1516
|
// src/ui/admin-frame.tsx
|
|
1393
1517
|
import { ThemeScope as ThemeScope2 } from "@odla-ai/ui/components";
|
|
1394
|
-
import { jsx as
|
|
1518
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "preact/jsx-runtime";
|
|
1395
1519
|
function AdminTheme(props) {
|
|
1396
1520
|
const brand = props.chapter?.brand;
|
|
1397
|
-
return /* @__PURE__ */
|
|
1521
|
+
return /* @__PURE__ */ jsx17(ChapterCopyProvider, { copy: props.chapter?.copy, children: /* @__PURE__ */ jsxs13(
|
|
1398
1522
|
ThemeScope2,
|
|
1399
1523
|
{
|
|
1400
1524
|
ref: props.scopeRef,
|
|
@@ -1403,7 +1527,7 @@ function AdminTheme(props) {
|
|
|
1403
1527
|
accent: brand?.accent,
|
|
1404
1528
|
colorScheme: brand?.colorScheme ?? "light",
|
|
1405
1529
|
children: [
|
|
1406
|
-
/* @__PURE__ */
|
|
1530
|
+
/* @__PURE__ */ jsx17(BrandStyle, { brand, selector: "[data-chapter-admin]" }),
|
|
1407
1531
|
props.children
|
|
1408
1532
|
]
|
|
1409
1533
|
}
|
|
@@ -1458,7 +1582,7 @@ async function loadChapterAdminUser(adapter, apiBase, getToken) {
|
|
|
1458
1582
|
}
|
|
1459
1583
|
|
|
1460
1584
|
// src/ui/admin.tsx
|
|
1461
|
-
import { jsx as
|
|
1585
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "preact/jsx-runtime";
|
|
1462
1586
|
function Authed(props) {
|
|
1463
1587
|
const copy = useChapterCopy().admin;
|
|
1464
1588
|
const {
|
|
@@ -1505,17 +1629,17 @@ function Authed(props) {
|
|
|
1505
1629
|
live = false;
|
|
1506
1630
|
};
|
|
1507
1631
|
}, [getToken, apiBase, auth]);
|
|
1508
|
-
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 }) });
|
|
1509
1633
|
if (state.status === "denied") {
|
|
1510
|
-
return /* @__PURE__ */
|
|
1511
|
-
/* @__PURE__ */
|
|
1512
|
-
/* @__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, {
|
|
1513
1637
|
account: state.email ?? copy.auth.thisAccount
|
|
1514
1638
|
}) }),
|
|
1515
|
-
/* @__PURE__ */
|
|
1639
|
+
/* @__PURE__ */ jsx18("button", { className: "btn secondary", onClick: () => signOut(), children: copy.auth.signOut })
|
|
1516
1640
|
] }) });
|
|
1517
1641
|
}
|
|
1518
|
-
return /* @__PURE__ */
|
|
1642
|
+
return /* @__PURE__ */ jsx18(
|
|
1519
1643
|
AdminShell,
|
|
1520
1644
|
{
|
|
1521
1645
|
workspaces,
|
|
@@ -1568,23 +1692,23 @@ function ChapterAdmin(props) {
|
|
|
1568
1692
|
}, [apiBase, auth]);
|
|
1569
1693
|
let content;
|
|
1570
1694
|
if (publishableKey === void 0 || publishableKey && !appearance) {
|
|
1571
|
-
content = /* @__PURE__ */
|
|
1695
|
+
content = /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.admin.auth.loading }) });
|
|
1572
1696
|
} else if (!publishableKey) {
|
|
1573
|
-
content = /* @__PURE__ */
|
|
1574
|
-
/* @__PURE__ */
|
|
1575
|
-
/* @__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 })
|
|
1576
1700
|
] }) });
|
|
1577
1701
|
} else if (workspaces.length === 0) {
|
|
1578
|
-
content = /* @__PURE__ */
|
|
1702
|
+
content = /* @__PURE__ */ jsx18(Gate, { brand, children: /* @__PURE__ */ jsx18("p", { style: S.muted, children: copy.admin.auth.noWorkspaces }) });
|
|
1579
1703
|
} else {
|
|
1580
|
-
content = /* @__PURE__ */
|
|
1704
|
+
content = /* @__PURE__ */ jsxs14(
|
|
1581
1705
|
ClerkGate,
|
|
1582
1706
|
{
|
|
1583
1707
|
publishableKey,
|
|
1584
1708
|
appearance,
|
|
1585
1709
|
afterSignOutUrl: "/",
|
|
1586
1710
|
children: [
|
|
1587
|
-
/* @__PURE__ */
|
|
1711
|
+
/* @__PURE__ */ jsx18(SignedOut, { children: /* @__PURE__ */ jsx18(Gate, { brand, tagline: copy.admin.auth.signInTagline, children: /* @__PURE__ */ jsx18(
|
|
1588
1712
|
SignIn,
|
|
1589
1713
|
{
|
|
1590
1714
|
routing: "hash",
|
|
@@ -1592,7 +1716,7 @@ function ChapterAdmin(props) {
|
|
|
1592
1716
|
signUpForceRedirectUrl: redirect
|
|
1593
1717
|
}
|
|
1594
1718
|
) }) }),
|
|
1595
|
-
/* @__PURE__ */
|
|
1719
|
+
/* @__PURE__ */ jsx18(SignedIn, { children: /* @__PURE__ */ jsx18(
|
|
1596
1720
|
Authed,
|
|
1597
1721
|
{
|
|
1598
1722
|
workspaces,
|
|
@@ -1612,7 +1736,7 @@ function ChapterAdmin(props) {
|
|
|
1612
1736
|
}
|
|
1613
1737
|
);
|
|
1614
1738
|
}
|
|
1615
|
-
return /* @__PURE__ */
|
|
1739
|
+
return /* @__PURE__ */ jsx18(AdminTheme, { chapter: props.chapter, scopeRef, children: content });
|
|
1616
1740
|
}
|
|
1617
1741
|
|
|
1618
1742
|
// src/ui/admin-defaults.ts
|
|
@@ -1623,15 +1747,15 @@ function defaultAdminSections(chapter) {
|
|
|
1623
1747
|
// src/ui/admin-meetings.tsx
|
|
1624
1748
|
import { useState as useState13 } from "preact/hooks";
|
|
1625
1749
|
import { Button as Button6 } from "@odla-ai/ui/components";
|
|
1626
|
-
import { Fragment as Fragment4, jsx as
|
|
1750
|
+
import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs15 } from "preact/jsx-runtime";
|
|
1627
1751
|
var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
|
|
1628
1752
|
function MeetingsBody({ ctx }) {
|
|
1629
1753
|
const copy = useChapterCopy().admin.meetings;
|
|
1630
1754
|
const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
|
|
1631
1755
|
const [busy, setBusy] = useState13(null);
|
|
1632
1756
|
const [note, setNote] = useState13(null);
|
|
1633
|
-
if (loading) return /* @__PURE__ */
|
|
1634
|
-
if (error || !data) return /* @__PURE__ */
|
|
1757
|
+
if (loading) return /* @__PURE__ */ jsx19(AdminNote, { children: copy.loading });
|
|
1758
|
+
if (error || !data) return /* @__PURE__ */ jsxs15(AdminNote, { children: [
|
|
1635
1759
|
copy.loadFailed,
|
|
1636
1760
|
error ? `: ${error}` : "",
|
|
1637
1761
|
"."
|
|
@@ -1662,32 +1786,32 @@ function MeetingsBody({ ctx }) {
|
|
|
1662
1786
|
}
|
|
1663
1787
|
void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
|
|
1664
1788
|
};
|
|
1665
|
-
return /* @__PURE__ */
|
|
1789
|
+
return /* @__PURE__ */ jsx19(AdminPage, { children: /* @__PURE__ */ jsx19(Panel, { title: /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
1666
1790
|
copy.agenda,
|
|
1667
1791
|
" ",
|
|
1668
|
-
/* @__PURE__ */
|
|
1792
|
+
/* @__PURE__ */ jsxs15("span", { className: "muted", style: { fontWeight: 400 }, children: [
|
|
1669
1793
|
"\xB7 ",
|
|
1670
1794
|
data.timezone
|
|
1671
1795
|
] })
|
|
1672
|
-
] }), actions: note ? /* @__PURE__ */
|
|
1673
|
-
/* @__PURE__ */
|
|
1674
|
-
/* @__PURE__ */
|
|
1675
|
-
/* @__PURE__ */
|
|
1676
|
-
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: [
|
|
1677
1801
|
copy.drift,
|
|
1678
1802
|
": ",
|
|
1679
1803
|
m.drift
|
|
1680
1804
|
] }) : null,
|
|
1681
|
-
m.meetUrl ? /* @__PURE__ */
|
|
1682
|
-
m.status === "scheduled" ? /* @__PURE__ */
|
|
1683
|
-
/* @__PURE__ */
|
|
1684
|
-
/* @__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 })
|
|
1685
1809
|
] }) : null
|
|
1686
1810
|
] }, m.id)) }) }) });
|
|
1687
1811
|
}
|
|
1688
1812
|
function meetingsSection(options = {}) {
|
|
1689
1813
|
const { id = "meetings", label = "Meetings" } = options;
|
|
1690
|
-
return { id, label, render: (ctx) => /* @__PURE__ */
|
|
1814
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx19(MeetingsBody, { ctx }) };
|
|
1691
1815
|
}
|
|
1692
1816
|
|
|
1693
1817
|
export {
|
|
@@ -1710,6 +1834,7 @@ export {
|
|
|
1710
1834
|
applicationLifecycleAdapter,
|
|
1711
1835
|
collectionSection,
|
|
1712
1836
|
peopleSection,
|
|
1837
|
+
LeaderNetworkOverview,
|
|
1713
1838
|
defaultAdminWorkspaces,
|
|
1714
1839
|
loadChapterAdminConfig,
|
|
1715
1840
|
loadChapterAdminUser,
|
|
@@ -1717,4 +1842,4 @@ export {
|
|
|
1717
1842
|
defaultAdminSections,
|
|
1718
1843
|
meetingsSection
|
|
1719
1844
|
};
|
|
1720
|
-
//# sourceMappingURL=chunk-
|
|
1845
|
+
//# sourceMappingURL=chunk-4QZEAWQL.js.map
|