@odla-ai/chapter 0.8.0 → 0.10.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 +55 -17
- package/dist/chunk-YO6DY5DL.js +281 -0
- package/dist/chunk-YO6DY5DL.js.map +1 -0
- package/dist/chunk-ZCZK6QLC.js +537 -0
- package/dist/chunk-ZCZK6QLC.js.map +1 -0
- package/dist/index.cjs +61 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -14
- package/dist/index.d.ts +60 -14
- package/dist/index.js +61 -16
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +71 -0
- package/dist/ui/admin/index.js +9 -0
- package/dist/ui/admin/index.js.map +1 -0
- package/dist/ui/index.d.ts +4 -219
- package/dist/ui/index.js +20 -795
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/member/index.d.ts +152 -0
- package/dist/ui/member/index.js +33 -0
- package/dist/ui/member/index.js.map +1 -0
- package/dist/worker/index.cjs +105 -30
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -0
- package/dist/worker/index.d.ts +17 -0
- package/dist/worker/index.js +105 -30
- package/dist/worker/index.js.map +1 -1
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -55,11 +55,21 @@ npm i @odla-ai/chapter
|
|
|
55
55
|
the crm namespaces + the chapter namespaces (`applications`, `groups`,
|
|
56
56
|
`meetings`, `emailLog`, plus the auth tables) + a guarded group-row seed. Drop
|
|
57
57
|
it in `odla.config.mjs` `integrations: [...]`.
|
|
58
|
-
- **The UI kit
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
- **The UI kit, adoptable in pieces.** Three entries, split on the dependency
|
|
59
|
+
boundary so you never pay for what you don't use:
|
|
60
|
+
- `@odla-ai/chapter/ui/member` — **react-only**: `SlotPicker`, the date/timezone
|
|
61
|
+
helpers, `JoinIsland` (form → payment → booking), `MembersArea`,
|
|
62
|
+
`Rescheduler`, `PaymentStep`, `<BrandStyle>`. It never imports
|
|
63
|
+
`@odla-ai/auth-clerk` or `@odla-ai/crm`, so you can adopt a single
|
|
64
|
+
presentational component without the Clerk browser SDK. (A bundle-graph test
|
|
65
|
+
enforces this.)
|
|
66
|
+
- `@odla-ai/chapter/ui/admin` — the Clerk-gated console `ChapterAdmin` + the
|
|
67
|
+
section catalog (`peopleSection`). This one does need auth-clerk + crm/ui.
|
|
68
|
+
- `@odla-ai/chapter/ui` — the full barrel, for back-compat.
|
|
69
|
+
|
|
70
|
+
Authored against React, rendered as Preact via `preact/compat` in the reference
|
|
71
|
+
sites. Brand tokens (`brandTokens`/`<BrandStyle>`) re-skin all of it from
|
|
72
|
+
`brand`.
|
|
63
73
|
|
|
64
74
|
## Quick start
|
|
65
75
|
|
|
@@ -125,10 +135,22 @@ These bite silently — a smoke test won't catch them:
|
|
|
125
135
|
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
126
136
|
against the platform, so set `services` explicitly if you don't run the o11y
|
|
127
137
|
collector.
|
|
128
|
-
- **Which email fires from which route.**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
- **Which email fires from which route.** Content and addressing are owner-editable
|
|
139
|
+
on the `groups` row; the *trigger* is config:
|
|
140
|
+
|
|
141
|
+
| Template | Fires from | Configurable |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `adminNotification` | `POST /api/applications` (submit) — or the Stripe webhook (first payment) | `sends.adminNotification: "submit" \| "payment" \| "never"` (default `"submit"`) |
|
|
144
|
+
| `prepEmail` | `POST /api/schedule/book` | template `enabled` flag |
|
|
145
|
+
| `paymentConfirmation` | Stripe webhook, first successful invoice | template `enabled` flag |
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
defineChapter({ /* … */ sends: { adminNotification: "payment" } });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Re-check this on every chapter upgrade.** Wiring a send changes a site's
|
|
152
|
+
outbound mail with no local diff — release notes call out send changes
|
|
153
|
+
explicitly for that reason.
|
|
132
154
|
- **Account model.** `account: "invite"` (default) mints a Clerk invitation,
|
|
133
155
|
`"create"` makes the account server-side (so join can say the account is
|
|
134
156
|
ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
|
|
@@ -143,14 +165,30 @@ These bite silently — a smoke test won't catch them:
|
|
|
143
165
|
|
|
144
166
|
### Install + scope notes
|
|
145
167
|
|
|
146
|
-
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
- **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
|
|
169
|
+
this package's manifest, because only one entry imports it: the worker verifies
|
|
170
|
+
JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
|
|
171
|
+
react-only. **Install `@odla-ai/auth-clerk` yourself if (and only if) you adopt
|
|
172
|
+
`@odla-ai/chapter/ui/admin`** or want the themed sign-in components; reach for
|
|
173
|
+
`@odla-ai/auth-clerk/invitations` when you want to send your own branded
|
|
174
|
+
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
|
|
175
|
+
half, so prefer the narrower entry.
|
|
176
|
+
- **Known-good version set** (installs clean, no flags): `@odla-ai/chapter` 0.10.0,
|
|
177
|
+
`@odla-ai/ui` 0.9.0, `@odla-ai/crm` 0.1.4, `@odla-ai/db` 0.6.6,
|
|
178
|
+
`@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1, CLI 0.16.7, plus `jose` and
|
|
179
|
+
react (or `preact` + `preact/compat`). chapter is the package that pulls the
|
|
180
|
+
others together, so this is the line to check when an install complains.
|
|
181
|
+
- **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
|
|
182
|
+
the peer conflict that tells you a pair is unsupported. If you need it, find out
|
|
183
|
+
why first.
|
|
184
|
+
- **The admin surface is intentionally minimal, and staying that way.** chapter
|
|
185
|
+
ships `/api/admin/scheduling` (booking rules, with field-keyed validation
|
|
186
|
+
messages) and `/api/admin/meetings` (the reconciled agenda, with the applicant
|
|
187
|
+
joined, `?all=1`, `from`/`to`, drift fields, Meet/Calendar links and the group
|
|
188
|
+
timezone), plus `peopleSection`. **Dashboard, billing, email config/log/test,
|
|
189
|
+
per-person comms, people list, role changes, approve and refund are out of
|
|
190
|
+
scope** — they are yours as host routes via the seam. Don't plan around them
|
|
191
|
+
shipping.
|
|
154
192
|
|
|
155
193
|
### Verify from the types, not this file
|
|
156
194
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// src/ui/admin.tsx
|
|
2
|
+
import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
3
|
+
import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
|
|
4
|
+
import { CrmClient } from "@odla-ai/crm";
|
|
5
|
+
|
|
6
|
+
// src/ui/chrome.tsx
|
|
7
|
+
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { TopBar, TopBarLink } from "@odla-ai/ui/components";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
|
|
11
|
+
var brandDisplay = (brand) => brand.wordmark ?? brand.name;
|
|
12
|
+
var S = {
|
|
13
|
+
gate: {
|
|
14
|
+
minHeight: "100vh",
|
|
15
|
+
display: "grid",
|
|
16
|
+
placeItems: "center",
|
|
17
|
+
padding: 24,
|
|
18
|
+
background: "radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%), radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)"
|
|
19
|
+
},
|
|
20
|
+
gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
|
|
21
|
+
brandBox: { textAlign: "center", marginBottom: 22 },
|
|
22
|
+
badge: {
|
|
23
|
+
width: 52,
|
|
24
|
+
height: 52,
|
|
25
|
+
margin: "0 auto 14px",
|
|
26
|
+
display: "grid",
|
|
27
|
+
placeItems: "center",
|
|
28
|
+
fontSize: 15,
|
|
29
|
+
fontWeight: 700,
|
|
30
|
+
color: "var(--ui-on-accent)",
|
|
31
|
+
background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
|
|
32
|
+
borderRadius: 14,
|
|
33
|
+
boxShadow: "0 10px 28px var(--ui-accent-soft)"
|
|
34
|
+
},
|
|
35
|
+
badgeSm: {
|
|
36
|
+
width: 26,
|
|
37
|
+
height: 26,
|
|
38
|
+
display: "grid",
|
|
39
|
+
placeItems: "center",
|
|
40
|
+
fontSize: 10,
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
color: "var(--ui-on-accent)",
|
|
43
|
+
background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
|
|
44
|
+
borderRadius: 7,
|
|
45
|
+
marginRight: 9
|
|
46
|
+
},
|
|
47
|
+
h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
|
|
48
|
+
muted: { color: "var(--ui-text-muted)" },
|
|
49
|
+
role: {
|
|
50
|
+
fontFamily: "var(--ui-font-mono)",
|
|
51
|
+
fontSize: 11,
|
|
52
|
+
textTransform: "uppercase",
|
|
53
|
+
letterSpacing: "0.04em",
|
|
54
|
+
padding: "2px 8px",
|
|
55
|
+
borderRadius: 999,
|
|
56
|
+
background: "var(--ui-accent-soft)",
|
|
57
|
+
border: "1px solid var(--ui-accent)",
|
|
58
|
+
color: "var(--ui-accent-strong)"
|
|
59
|
+
},
|
|
60
|
+
whoami: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
|
|
61
|
+
// Sections own their own width/padding (e.g. a .wrap container).
|
|
62
|
+
main: { minHeight: "calc(100vh - 61px)" },
|
|
63
|
+
card: { width: "100%", textAlign: "center" }
|
|
64
|
+
};
|
|
65
|
+
function Gate(props) {
|
|
66
|
+
const { brand, tagline, children } = props;
|
|
67
|
+
return /* @__PURE__ */ jsx("div", { style: S.gate, children: /* @__PURE__ */ jsxs("div", { style: S.gateInner, children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { style: S.brandBox, children: [
|
|
69
|
+
/* @__PURE__ */ jsx("div", { style: S.badge, children: badgeText(brand) }),
|
|
70
|
+
/* @__PURE__ */ jsx("h1", { style: S.h1, children: brandDisplay(brand) }),
|
|
71
|
+
tagline ? /* @__PURE__ */ jsx("p", { style: { ...S.muted, margin: "8px 0 0", fontSize: 14 }, children: tagline }) : null
|
|
72
|
+
] }),
|
|
73
|
+
children
|
|
74
|
+
] }) });
|
|
75
|
+
}
|
|
76
|
+
function AdminShell(props) {
|
|
77
|
+
const { sections, basePath, brand, client, getToken, signOut, email } = props;
|
|
78
|
+
const sectionFromPath = useCallback(() => {
|
|
79
|
+
const fallback = sections[0]?.id ?? "";
|
|
80
|
+
if (typeof window === "undefined") return fallback;
|
|
81
|
+
const rest = window.location.pathname.slice(basePath.length).replace(/^\//, "");
|
|
82
|
+
const id = rest.split("/")[0] ?? "";
|
|
83
|
+
return sections.some((s) => s.id === id) ? id : fallback;
|
|
84
|
+
}, [sections, basePath]);
|
|
85
|
+
const [section, setSection] = useState(sectionFromPath);
|
|
86
|
+
const go = useCallback(
|
|
87
|
+
(id) => {
|
|
88
|
+
window.history.pushState(null, "", `${basePath}/${id}`);
|
|
89
|
+
setSection(id);
|
|
90
|
+
},
|
|
91
|
+
[basePath]
|
|
92
|
+
);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const onPop = () => setSection(sectionFromPath());
|
|
95
|
+
window.addEventListener("popstate", onPop);
|
|
96
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
97
|
+
}, [sectionFromPath]);
|
|
98
|
+
const active = sections.find((s) => s.id === section) ?? sections[0];
|
|
99
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
/* @__PURE__ */ jsxs(TopBar, { children: [
|
|
101
|
+
/* @__PURE__ */ jsxs("a", { className: "topbar-brand", href: "/", style: { display: "inline-flex", alignItems: "center" }, children: [
|
|
102
|
+
/* @__PURE__ */ jsx("span", { style: S.badgeSm, children: badgeText(brand) }),
|
|
103
|
+
brandDisplay(brand)
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsx("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: sections.map((s) => /* @__PURE__ */ jsx(TopBarLink, { active: section === s.id, onClick: () => go(s.id), children: s.label }, s.id)) }),
|
|
106
|
+
/* @__PURE__ */ jsx("div", { className: "topbar-spacer" }),
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { className: "topbar-actions", children: [
|
|
108
|
+
/* @__PURE__ */ jsxs("span", { style: S.whoami, children: [
|
|
109
|
+
/* @__PURE__ */ jsx("span", { style: S.role, children: "admin" }),
|
|
110
|
+
email ? /* @__PURE__ */ jsx("span", { style: S.muted, children: email }) : null
|
|
111
|
+
] }),
|
|
112
|
+
/* @__PURE__ */ jsx("button", { className: "btn secondary mini", onClick: () => signOut(), children: "Sign out" })
|
|
113
|
+
] })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsx("main", { className: "shell-main", style: S.main, children: active ? active.render({ client, getToken, navigate: go }) : null })
|
|
116
|
+
] });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/ui/admin.tsx
|
|
120
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
121
|
+
function Authed(props) {
|
|
122
|
+
const { sections, basePath, brand, crmBasePath, apiBase } = props;
|
|
123
|
+
const { getToken, signOut } = useClerkAuth();
|
|
124
|
+
const client = useMemo(
|
|
125
|
+
() => new CrmClient({
|
|
126
|
+
basePath: crmBasePath,
|
|
127
|
+
headers: async () => {
|
|
128
|
+
const t = await getToken();
|
|
129
|
+
return t ? { authorization: `Bearer ${t}` } : {};
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
[getToken, crmBasePath]
|
|
133
|
+
);
|
|
134
|
+
const [state, setState] = useState2({
|
|
135
|
+
status: "checking"
|
|
136
|
+
});
|
|
137
|
+
useEffect2(() => {
|
|
138
|
+
let live = true;
|
|
139
|
+
void (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const t = await getToken();
|
|
142
|
+
const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
|
|
143
|
+
const body = await res.json().catch(() => ({}));
|
|
144
|
+
if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
|
|
145
|
+
} catch {
|
|
146
|
+
if (live) setState({ status: "denied" });
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
return () => {
|
|
150
|
+
live = false;
|
|
151
|
+
};
|
|
152
|
+
}, [getToken, apiBase]);
|
|
153
|
+
if (state.status === "checking") {
|
|
154
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Checking access\u2026" }) });
|
|
155
|
+
}
|
|
156
|
+
if (state.status === "denied") {
|
|
157
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
|
|
158
|
+
/* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
|
|
159
|
+
/* @__PURE__ */ jsxs2("p", { style: S.muted, children: [
|
|
160
|
+
state.email ? `${state.email} isn't` : "This account isn't",
|
|
161
|
+
" on the admin list. Ask an existing admin to add you in odla Studio."
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsx2("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
|
|
164
|
+
] }) });
|
|
165
|
+
}
|
|
166
|
+
return /* @__PURE__ */ jsx2(
|
|
167
|
+
AdminShell,
|
|
168
|
+
{
|
|
169
|
+
sections,
|
|
170
|
+
basePath,
|
|
171
|
+
brand,
|
|
172
|
+
client,
|
|
173
|
+
getToken,
|
|
174
|
+
signOut,
|
|
175
|
+
email: state.email ?? null
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function ChapterAdmin(props) {
|
|
180
|
+
const sections = props.sections;
|
|
181
|
+
const basePath = props.basePath ?? "/admin";
|
|
182
|
+
const brand = props.brand ?? { name: "Admin" };
|
|
183
|
+
const crmBasePath = props.crmBasePath ?? "/api/crm";
|
|
184
|
+
const apiBase = props.apiBase ?? "";
|
|
185
|
+
const [pk, setPk] = useState2(void 0);
|
|
186
|
+
useEffect2(() => {
|
|
187
|
+
let live = true;
|
|
188
|
+
void (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`${apiBase}/api/config`);
|
|
191
|
+
const body = await res.json();
|
|
192
|
+
if (live) setPk(body.clerkPublishableKey ?? null);
|
|
193
|
+
} catch {
|
|
194
|
+
if (live) setPk(null);
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
return () => {
|
|
198
|
+
live = false;
|
|
199
|
+
};
|
|
200
|
+
}, [apiBase]);
|
|
201
|
+
if (pk === void 0) {
|
|
202
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Loading\u2026" }) });
|
|
203
|
+
}
|
|
204
|
+
if (!pk) {
|
|
205
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
|
|
206
|
+
/* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
|
|
207
|
+
/* @__PURE__ */ jsx2("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
|
|
208
|
+
] }) });
|
|
209
|
+
}
|
|
210
|
+
return /* @__PURE__ */ jsxs2(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
|
|
211
|
+
/* @__PURE__ */ jsx2(SignedOut, { children: /* @__PURE__ */ jsx2(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx2(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
|
|
212
|
+
/* @__PURE__ */ jsx2(SignedIn, { children: /* @__PURE__ */ jsx2(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/ui/admin-people.tsx
|
|
217
|
+
import { useState as useState3 } from "react";
|
|
218
|
+
import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
|
|
219
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
220
|
+
function PeopleBody(props) {
|
|
221
|
+
const { crm, client, type } = props;
|
|
222
|
+
const hookClient = client;
|
|
223
|
+
const query = useCrmQuery(hookClient, type);
|
|
224
|
+
const [openId, setOpenId] = useState3(null);
|
|
225
|
+
const record = useCrmRecord(hookClient, openId);
|
|
226
|
+
const [saving, setSaving] = useState3(false);
|
|
227
|
+
const saveFields = async (input) => {
|
|
228
|
+
if (!openId) return;
|
|
229
|
+
setSaving(true);
|
|
230
|
+
try {
|
|
231
|
+
await client.updateRecord(openId, { input });
|
|
232
|
+
record.refresh();
|
|
233
|
+
query.refresh();
|
|
234
|
+
} finally {
|
|
235
|
+
setSaving(false);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const moveStage = async (to) => {
|
|
239
|
+
if (!openId) return;
|
|
240
|
+
await client.setStage(openId, to);
|
|
241
|
+
record.refresh();
|
|
242
|
+
query.refresh();
|
|
243
|
+
};
|
|
244
|
+
const addTag = async (tag) => {
|
|
245
|
+
if (openId) {
|
|
246
|
+
await client.addTag(openId, tag);
|
|
247
|
+
record.refresh();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const removeTag = async (tag) => {
|
|
251
|
+
if (openId) {
|
|
252
|
+
await client.removeTag(openId, tag);
|
|
253
|
+
record.refresh();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return /* @__PURE__ */ jsxs3("div", { className: "wrap", children: [
|
|
257
|
+
/* @__PURE__ */ jsx3(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
|
|
258
|
+
record.detail ? /* @__PURE__ */ jsx3(
|
|
259
|
+
RecordPanel,
|
|
260
|
+
{
|
|
261
|
+
crm,
|
|
262
|
+
detail: record.detail,
|
|
263
|
+
onSaveFields: saveFields,
|
|
264
|
+
onMoveStage: moveStage,
|
|
265
|
+
onAddTag: addTag,
|
|
266
|
+
onRemoveTag: removeTag,
|
|
267
|
+
saving
|
|
268
|
+
}
|
|
269
|
+
) : null
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
function peopleSection(options) {
|
|
273
|
+
const { crm, id = "people", label = "People", type = "person" } = options;
|
|
274
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(PeopleBody, { crm, client: ctx.client, type }) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export {
|
|
278
|
+
ChapterAdmin,
|
|
279
|
+
peopleSection
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=chunk-YO6DY5DL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/admin.tsx","../src/ui/chrome.tsx","../src/ui/admin-people.tsx"],"sourcesContent":["// ChapterAdmin — the whole admin console for a chapter/hub, owned by the package\n// so no site re-wires (or re-bugs) it: the Clerk publishable-key fetch\n// (/api/config), the themed sign-in with the post-sign-in redirect DERIVED from\n// basePath (so it never bounces to \"/\"), the odla-db admins-allowlist check\n// (/api/me), and a TopBar shell (chrome.tsx) that renders the caller's sections.\nimport { useEffect, useMemo, useState } from \"react\";\nimport { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from \"@odla-ai/auth-clerk\";\nimport { CrmClient } from \"@odla-ai/crm\";\nimport { S, Gate, AdminShell } from \"./chrome.js\";\nimport type { AdminSection, Brand } from \"./chrome.js\";\n\nexport type { AdminSection, AdminSectionContext } from \"./chrome.js\";\n\n/** Props for {@link ChapterAdmin}. */\nexport interface ChapterAdminProps {\n /** The admin sections to render (nav label + body). */\n sections: AdminSection[];\n /** Admin mount path. The sign-in redirect + section routing derive from it,\n * so the \"bounce to home after sign-in\" bug is impossible. Default \"/admin\". */\n basePath?: string;\n /** Wordmark shown in the gate + top bar. */\n brand?: Brand;\n /** CRM route mount. Default \"/api/crm\". */\n crmBasePath?: string;\n /** Origin for /api/config and /api/me. Default same origin. */\n apiBase?: string;\n}\n\n// After sign-in: build a token-bound CrmClient, confirm the account is an\n// allowlisted admin (/api/me), then render the shell.\nfunction Authed(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n crmBasePath: string;\n apiBase: string;\n}) {\n const { sections, basePath, brand, crmBasePath, apiBase } = props;\n const { getToken, signOut } = useClerkAuth();\n const client = useMemo(\n () =>\n new CrmClient({\n basePath: crmBasePath,\n headers: async (): Promise<Record<string, string>> => {\n const t = await getToken();\n return t ? { authorization: `Bearer ${t}` } : {};\n },\n }),\n [getToken, crmBasePath],\n );\n const [state, setState] = useState<{ status: \"checking\" | \"ok\" | \"denied\"; email?: string | null }>({\n status: \"checking\",\n });\n\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const t = await getToken();\n const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });\n const body = (await res.json().catch(() => ({}))) as { authorized?: boolean; email?: string | null };\n if (live) setState({ status: body.authorized ? \"ok\" : \"denied\", email: body.email ?? null });\n } catch {\n if (live) setState({ status: \"denied\" });\n }\n })();\n return () => {\n live = false;\n };\n }, [getToken, apiBase]);\n\n if (state.status === \"checking\") {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Checking access…</p>\n </Gate>\n );\n }\n if (state.status === \"denied\") {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Not authorized</h2>\n <p style={S.muted}>\n {state.email ? `${state.email} isn't` : \"This account isn't\"} on the admin list. Ask an existing admin to add\n you in odla Studio.\n </p>\n <button className=\"btn secondary\" onClick={() => signOut()} style={{ marginTop: 12 }}>\n Sign out\n </button>\n </div>\n </Gate>\n );\n }\n return (\n <AdminShell\n sections={sections}\n basePath={basePath}\n brand={brand}\n client={client}\n getToken={getToken}\n signOut={signOut}\n email={state.email ?? null}\n />\n );\n}\n\n/**\n * The whole admin console for a chapter/hub: fetches the Clerk publishable key\n * (/api/config), gates on sign-in with the post-sign-in redirect derived from\n * `basePath`, checks the odla-db admins allowlist (/api/me), and renders a\n * TopBar shell over the caller's `sections`. The gate/shell/redirect are owned\n * here so no site re-wires (or re-bugs) them.\n */\nexport function ChapterAdmin(props: ChapterAdminProps) {\n const sections = props.sections;\n const basePath = props.basePath ?? \"/admin\";\n const brand = props.brand ?? { name: \"Admin\" };\n const crmBasePath = props.crmBasePath ?? \"/api/crm\";\n const apiBase = props.apiBase ?? \"\";\n\n const [pk, setPk] = useState<string | null | undefined>(undefined);\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const res = await fetch(`${apiBase}/api/config`);\n const body = (await res.json()) as { clerkPublishableKey?: string | null };\n if (live) setPk(body.clerkPublishableKey ?? null);\n } catch {\n if (live) setPk(null);\n }\n })();\n return () => {\n live = false;\n };\n }, [apiBase]);\n\n if (pk === undefined) {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Loading…</p>\n </Gate>\n );\n }\n if (!pk) {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Sign-in not configured</h2>\n <p style={S.muted}>No Clerk publishable key is set for this environment yet.</p>\n </div>\n </Gate>\n );\n }\n return (\n <ClerkGate publishableKey={pk} appearance={clerkAppearanceFromTokens()} afterSignOutUrl=\"/\">\n <SignedOut>\n <Gate brand={brand} tagline=\"Admin sign-in — invite only\">\n <SignIn routing=\"hash\" forceRedirectUrl={basePath} signUpForceRedirectUrl={basePath} />\n </Gate>\n </SignedOut>\n <SignedIn>\n <Authed sections={sections} basePath={basePath} brand={brand} crmBasePath={crmBasePath} apiBase={apiBase} />\n </SignedIn>\n </ClerkGate>\n );\n}\n","// Shared chrome for ChapterAdmin: the section contract, the centered gate\n// screen, the signed-in TopBar shell, and the inline style tokens. Split out of\n// admin.tsx to stay under the repo's per-file LOC cap. Uses only @odla-ai/ui\n// classes + --ui-* tokens, so a consumer needs nothing beyond the odla-ui sheet.\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { TopBar, TopBarLink } from \"@odla-ai/ui/components\";\nimport type { CrmClient } from \"@odla-ai/crm\";\n\n/** What each admin section receives. `client` is a CrmClient bound to the\n * signed-in admin's bearer token; `navigate` switches sections. */\nexport interface AdminSectionContext {\n client: CrmClient;\n getToken: () => Promise<string | null>;\n /** Switch to another section by id (e.g. Overview \"jump in\" buttons). */\n navigate: (sectionId: string) => void;\n}\n\n/** One admin-console section: a nav label and a body renderer. */\nexport interface AdminSection {\n id: string;\n label: string;\n render: (ctx: AdminSectionContext) => ReactNode;\n}\n\n/** Wordmark shown in the gate and top bar. `name` is the plain-text identity (and\n * the badge fallback); `wordmark` is an optional rich display node for brands\n * whose typography can't be a plain string (e.g. an ampersand wrapped to render\n * upright), and `badge` overrides the glyph. */\nexport type Brand = { name: string; badge?: string; wordmark?: ReactNode };\n\n/** The badge glyph: the explicit `badge`, else the first 3 letters of `name`. */\nexport const badgeText = (brand: Brand): string => brand.badge ?? brand.name.slice(0, 3).toUpperCase();\n\n/** The display node: the rich `wordmark` if given, else the plain `name`. */\nexport const brandDisplay = (brand: Brand): ReactNode => brand.wordmark ?? brand.name;\n\n/** Inline style tokens (keyed to --ui-* custom properties). */\nexport const S: Record<string, CSSProperties> = {\n gate: {\n minHeight: \"100vh\",\n display: \"grid\",\n placeItems: \"center\",\n padding: 24,\n background:\n \"radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%),\" +\n \" radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)\",\n },\n gateInner: { width: \"100%\", maxWidth: 400, display: \"flex\", flexDirection: \"column\", alignItems: \"center\" },\n brandBox: { textAlign: \"center\", marginBottom: 22 },\n badge: {\n width: 52,\n height: 52,\n margin: \"0 auto 14px\",\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 15,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 14,\n boxShadow: \"0 10px 28px var(--ui-accent-soft)\",\n },\n badgeSm: {\n width: 26,\n height: 26,\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 10,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 7,\n marginRight: 9,\n },\n h1: { margin: 0, fontSize: 28, letterSpacing: \"-0.02em\", fontWeight: 700 },\n muted: { color: \"var(--ui-text-muted)\" },\n role: {\n fontFamily: \"var(--ui-font-mono)\",\n fontSize: 11,\n textTransform: \"uppercase\",\n letterSpacing: \"0.04em\",\n padding: \"2px 8px\",\n borderRadius: 999,\n background: \"var(--ui-accent-soft)\",\n border: \"1px solid var(--ui-accent)\",\n color: \"var(--ui-accent-strong)\",\n },\n whoami: { display: \"flex\", alignItems: \"center\", gap: 8, fontSize: 12 },\n // Sections own their own width/padding (e.g. a .wrap container).\n main: { minHeight: \"calc(100vh - 61px)\" },\n card: { width: \"100%\", textAlign: \"center\" },\n};\n\n/** Centered gate screen used for loading / sign-in / denied / not-configured. */\nexport function Gate(props: { brand: Brand; tagline?: string; children: ReactNode }) {\n const { brand, tagline, children } = props;\n return (\n <div style={S.gate}>\n <div style={S.gateInner}>\n <div style={S.brandBox}>\n <div style={S.badge}>{badgeText(brand)}</div>\n <h1 style={S.h1}>{brandDisplay(brand)}</h1>\n {tagline ? <p style={{ ...S.muted, margin: \"8px 0 0\", fontSize: 14 }}>{tagline}</p> : null}\n </div>\n {children}\n </div>\n </div>\n );\n}\n\n/** The signed-in shell: a TopBar of the caller's sections + the active body. */\nexport function AdminShell(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n client: CrmClient;\n getToken: () => Promise<string | null>;\n signOut: () => void;\n email: string | null;\n}) {\n const { sections, basePath, brand, client, getToken, signOut, email } = props;\n\n const sectionFromPath = useCallback(() => {\n const fallback = sections[0]?.id ?? \"\";\n if (typeof window === \"undefined\") return fallback;\n const rest = window.location.pathname.slice(basePath.length).replace(/^\\//, \"\");\n const id = rest.split(\"/\")[0] ?? \"\";\n return sections.some((s) => s.id === id) ? id : fallback;\n }, [sections, basePath]);\n\n const [section, setSection] = useState(sectionFromPath);\n\n const go = useCallback(\n (id: string) => {\n window.history.pushState(null, \"\", `${basePath}/${id}`);\n setSection(id);\n },\n [basePath],\n );\n\n useEffect(() => {\n const onPop = () => setSection(sectionFromPath());\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, [sectionFromPath]);\n\n const active = sections.find((s) => s.id === section) ?? sections[0];\n\n return (\n <>\n <TopBar>\n <a className=\"topbar-brand\" href=\"/\" style={{ display: \"inline-flex\", alignItems: \"center\" }}>\n <span style={S.badgeSm}>{badgeText(brand)}</span>\n {brandDisplay(brand)}\n </a>\n <nav className=\"topbar-nav\" aria-label=\"Admin navigation\">\n {sections.map((s) => (\n <TopBarLink key={s.id} active={section === s.id} onClick={() => go(s.id)}>\n {s.label}\n </TopBarLink>\n ))}\n </nav>\n <div className=\"topbar-spacer\" />\n <div className=\"topbar-actions\">\n <span style={S.whoami}>\n <span style={S.role}>admin</span>\n {email ? <span style={S.muted}>{email}</span> : null}\n </span>\n <button className=\"btn secondary mini\" onClick={() => signOut()}>\n Sign out\n </button>\n </div>\n </TopBar>\n <main className=\"shell-main\" style={S.main}>\n {active ? active.render({ client, getToken, navigate: go }) : null}\n </main>\n </>\n );\n}\n","// People/CRM admin section — the core admin workflow, assembled from the shipped\n// @odla-ai/crm/ui kit against the /api/crm/* routes the worker already mounts.\n// It's a FACTORY: peopleSection({ crm }) → an AdminSection you drop into\n// ChapterAdmin's `sections`. Every lifecycle action (edit fields, move pipeline\n// stage, tag) goes through the token-bound CrmClient — never a direct db write,\n// so the package's operational invariants hold (G7).\nimport { useState } from \"react\";\nimport { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from \"@odla-ai/crm/ui\";\nimport type { Crm, CrmClient, CrmRecord } from \"@odla-ai/crm\";\nimport type { AdminSection } from \"./chrome.js\";\n\nfunction PeopleBody(props: { crm: Crm; client: CrmClient; type: string }) {\n const { crm, client, type } = props;\n // The crm/ui hooks type their client against @odla-ai/crm's ui-entry bundled\n // declaration; ctx.client is the core-entry declaration. Same runtime class,\n // but their private field makes the two .d.ts copies nominally distinct — cast\n // to the param type the hooks expect.\n const hookClient = client as unknown as Parameters<typeof useCrmQuery>[0];\n const query = useCrmQuery(hookClient, type);\n const [openId, setOpenId] = useState<string | null>(null);\n const record = useCrmRecord(hookClient, openId);\n const [saving, setSaving] = useState(false);\n\n const saveFields = async (input: Record<string, unknown>) => {\n if (!openId) return;\n setSaving(true);\n try {\n await client.updateRecord(openId, { input });\n record.refresh();\n query.refresh();\n } finally {\n setSaving(false);\n }\n };\n const moveStage = async (to: string) => {\n if (!openId) return;\n await client.setStage(openId, to);\n record.refresh();\n query.refresh();\n };\n const addTag = async (tag: string) => {\n if (openId) {\n await client.addTag(openId, tag);\n record.refresh();\n }\n };\n const removeTag = async (tag: string) => {\n if (openId) {\n await client.removeTag(openId, tag);\n record.refresh();\n }\n };\n\n return (\n <div className=\"wrap\">\n <CrmList crm={crm} type={type} query={query} onOpenRecord={(r: CrmRecord) => setOpenId(r.id)} />\n {record.detail ? (\n <RecordPanel\n crm={crm}\n detail={record.detail}\n onSaveFields={saveFields}\n onMoveStage={moveStage}\n onAddTag={addTag}\n onRemoveTag={removeTag}\n saving={saving}\n />\n ) : null}\n </div>\n );\n}\n\n/** Options for {@link peopleSection}. */\nexport interface PeopleSectionOptions {\n /** The chapter's resolved CRM engine — `chapter.crm` from defineChapter. */\n crm: Crm;\n /** Section id / route segment. Default \"people\". */\n id?: string;\n /** Nav label. Default \"People\". */\n label?: string;\n /** The record type to list. Default \"person\". */\n type?: string;\n}\n\n/**\n * Build the People/CRM admin section: a server-driven records table plus a\n * facet-composed detail drawer (fields, pipeline stage, tags) over the\n * token-bound CrmClient. Drop the result into `ChapterAdmin`'s `sections`.\n * (Notes/activity feed and saved-view navigation are noted follow-ups.)\n */\nexport function peopleSection(options: PeopleSectionOptions): AdminSection {\n const { crm, id = \"people\", label = \"People\", type = \"person\" } = options;\n return { id, label, render: (ctx) => <PeopleBody crm={crm} client={ctx.client} type={type} /> };\n}\n"],"mappings":"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","names":["useEffect","useState","jsx","jsxs","useState","useEffect","useState","jsx","jsxs"]}
|