@odla-ai/chapter 0.20.2 → 0.22.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.
@@ -1,922 +0,0 @@
1
- import {
2
- BrandStyle
3
- } from "./chunk-MKXHZMAP.js";
4
-
5
- // src/ui/admin.tsx
6
- import { useEffect as useEffect5, useMemo, useState as useState10 } from "react";
7
- import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
8
- import { CrmClient } from "@odla-ai/crm";
9
-
10
- // src/ui/chrome.tsx
11
- import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
12
- import { TopBar, TopBarLink } from "@odla-ai/ui/components";
13
-
14
- // src/ui/admin-layout.tsx
15
- import { useEffect, useState } from "react";
16
- import { jsx, jsxs } from "react/jsx-runtime";
17
- var PAGE = { maxWidth: 1120, margin: "0 auto", padding: "28px 20px 64px", display: "flex", flexDirection: "column", gap: 20 };
18
- var HEAD = { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap" };
19
- function AdminPage({ children }) {
20
- return /* @__PURE__ */ jsx("div", { style: PAGE, children });
21
- }
22
- function Panel({ title, actions, children }) {
23
- return /* @__PURE__ */ jsxs("section", { className: "panel", style: { display: "flex", flexDirection: "column", gap: 14 }, children: [
24
- title || actions ? /* @__PURE__ */ jsxs("header", { style: HEAD, children: [
25
- title ? /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 18 }, children: title }) : /* @__PURE__ */ jsx("span", {}),
26
- actions ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: actions }) : null
27
- ] }) : null,
28
- children
29
- ] });
30
- }
31
- function themeLooksMissing() {
32
- if (typeof document === "undefined") return false;
33
- const probe = document.createElement("div");
34
- probe.className = "panel";
35
- probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none";
36
- document.body.appendChild(probe);
37
- const bg = getComputedStyle(probe).backgroundColor;
38
- probe.remove();
39
- return !bg || bg === "transparent" || bg === "rgba(0, 0, 0, 0)";
40
- }
41
- function ThemeWarning() {
42
- const [missing, setMissing] = useState(false);
43
- useEffect(() => setMissing(themeLooksMissing()), []);
44
- if (!missing) return null;
45
- return /* @__PURE__ */ jsxs(
46
- "div",
47
- {
48
- role: "alert",
49
- style: { background: "#7f1d1d", color: "#fff", padding: "12px 20px", fontSize: 14, lineHeight: 1.5, textAlign: "center" },
50
- children: [
51
- /* @__PURE__ */ jsx("strong", { children: "odla-ui theme tokens are missing." }),
52
- " The console reads colors from a theme layer, not",
53
- " ",
54
- /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/index.css" }),
55
- " alone. Import a theme before it \u2014 e.g.",
56
- " ",
57
- /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/themes/salt/tokens.css" }),
58
- " and ",
59
- /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/themes/salt/ui.css" }),
60
- " \u2014 then",
61
- " ",
62
- /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/index.css" }),
63
- ". See the @odla-ai/chapter README (\u201CTheme tokens\u201D)."
64
- ]
65
- }
66
- );
67
- }
68
- function AdminNote({ children }) {
69
- return /* @__PURE__ */ jsx(AdminPage, { children: /* @__PURE__ */ jsx("section", { className: "panel", children: /* @__PURE__ */ jsx("p", { className: "muted", style: { margin: 0 }, children }) }) });
70
- }
71
-
72
- // src/ui/chrome.tsx
73
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
74
- var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
75
- var brandDisplay = (brand) => {
76
- const display = brand.wordmark ?? brand.name;
77
- if (typeof display !== "string" || !display.includes("&")) return display;
78
- return /* @__PURE__ */ jsx2(Fragment, { children: display.split("&").map((part, index, all) => /* @__PURE__ */ jsxs2("span", { children: [
79
- part,
80
- index < all.length - 1 ? /* @__PURE__ */ jsx2("span", { className: "mk-amp", children: "&" }) : null
81
- ] }, `${part}:${index}`)) });
82
- };
83
- var cleanBase = (basePath) => basePath.length > 1 ? basePath.replace(/\/+$/, "") : basePath;
84
- function adminSectionFromUrl(url, basePath, sectionIds) {
85
- const fallback = sectionIds[0] ?? "";
86
- const tab = url.searchParams.get("tab");
87
- if (tab && sectionIds.includes(tab)) return tab;
88
- const base = cleanBase(basePath);
89
- const rest = url.pathname.startsWith(base) ? url.pathname.slice(base.length).replace(/^\//, "") : "";
90
- const legacy = rest.split("/")[0] ?? "";
91
- return sectionIds.includes(legacy) ? legacy : fallback;
92
- }
93
- function adminSectionHref(current, basePath, id, routing = "query") {
94
- const next = new URL(current);
95
- const base = cleanBase(basePath);
96
- if (routing === "path") {
97
- next.pathname = `${base}/${id}`;
98
- next.searchParams.delete("tab");
99
- } else {
100
- const mountPaths = /* @__PURE__ */ new Set([base, `${base}/`, `${base}/index.html`]);
101
- if (!mountPaths.has(next.pathname)) next.pathname = `${base}/`;
102
- next.searchParams.set("tab", id);
103
- }
104
- next.hash = "";
105
- return `${next.pathname}${next.search}${next.hash}`;
106
- }
107
- var S = {
108
- gate: {
109
- minHeight: "100vh",
110
- display: "grid",
111
- placeItems: "center",
112
- padding: 24,
113
- 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%)"
114
- },
115
- gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
116
- brandBox: { textAlign: "center", marginBottom: 22 },
117
- badge: {
118
- width: 52,
119
- height: 52,
120
- margin: "0 auto 14px",
121
- display: "grid",
122
- placeItems: "center",
123
- fontSize: 15,
124
- fontWeight: 700,
125
- color: "var(--ui-on-accent)",
126
- background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
127
- borderRadius: 14,
128
- boxShadow: "0 10px 28px var(--ui-accent-soft)"
129
- },
130
- badgeSm: {
131
- width: 26,
132
- height: 26,
133
- display: "grid",
134
- placeItems: "center",
135
- fontSize: 10,
136
- fontWeight: 700,
137
- color: "var(--ui-on-accent)",
138
- background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
139
- borderRadius: 7,
140
- marginRight: 9
141
- },
142
- h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
143
- muted: { color: "var(--ui-text-muted)" },
144
- role: {
145
- fontFamily: "var(--ui-font-mono)",
146
- fontSize: 11,
147
- textTransform: "uppercase",
148
- letterSpacing: "0.04em",
149
- padding: "2px 8px",
150
- borderRadius: 999,
151
- background: "var(--ui-accent-soft)",
152
- border: "1px solid var(--ui-accent)",
153
- color: "var(--ui-accent-strong)"
154
- },
155
- whoami: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
156
- // Sections own their own width/padding (e.g. a .wrap container).
157
- main: { minHeight: "calc(100vh - 61px)" },
158
- card: { width: "100%", textAlign: "center" }
159
- };
160
- function Gate(props) {
161
- const { brand, tagline, children } = props;
162
- return /* @__PURE__ */ jsx2("div", { style: S.gate, children: /* @__PURE__ */ jsxs2("div", { style: S.gateInner, children: [
163
- /* @__PURE__ */ jsxs2("div", { style: S.brandBox, children: [
164
- /* @__PURE__ */ jsx2("div", { style: S.badge, children: badgeText(brand) }),
165
- /* @__PURE__ */ jsx2("h1", { style: S.h1, children: brandDisplay(brand) }),
166
- tagline ? /* @__PURE__ */ jsx2("p", { style: { ...S.muted, margin: "8px 0 0", fontSize: 14 }, children: tagline }) : null
167
- ] }),
168
- children
169
- ] }) });
170
- }
171
- function AdminShell(props) {
172
- const { sections, basePath, brand, client, getToken, signOut, email, routing = "query" } = props;
173
- const sectionFromPath = useCallback(() => {
174
- if (typeof window === "undefined") return sections[0]?.id ?? "";
175
- return adminSectionFromUrl(new URL(window.location.href), basePath, sections.map((section2) => section2.id));
176
- }, [sections, basePath]);
177
- const [section, setSection] = useState2(sectionFromPath);
178
- const go = useCallback(
179
- (id) => {
180
- window.history.pushState(null, "", adminSectionHref(new URL(window.location.href), basePath, id, routing));
181
- setSection(id);
182
- },
183
- [basePath, routing]
184
- );
185
- useEffect2(() => {
186
- const onPop = () => setSection(sectionFromPath());
187
- window.addEventListener("popstate", onPop);
188
- return () => window.removeEventListener("popstate", onPop);
189
- }, [sectionFromPath]);
190
- const active = sections.find((s) => s.id === section) ?? sections[0];
191
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
192
- /* @__PURE__ */ jsxs2(TopBar, { children: [
193
- /* @__PURE__ */ jsxs2("a", { className: "topbar-brand", href: "/", style: { display: "inline-flex", alignItems: "center" }, children: [
194
- /* @__PURE__ */ jsx2("span", { style: S.badgeSm, children: badgeText(brand) }),
195
- brandDisplay(brand)
196
- ] }),
197
- /* @__PURE__ */ jsx2("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: sections.map((s) => /* @__PURE__ */ jsx2(TopBarLink, { active: section === s.id, onClick: () => go(s.id), children: s.label }, s.id)) }),
198
- /* @__PURE__ */ jsx2("div", { className: "topbar-spacer" }),
199
- /* @__PURE__ */ jsxs2("div", { className: "topbar-actions", children: [
200
- /* @__PURE__ */ jsxs2("span", { style: S.whoami, children: [
201
- /* @__PURE__ */ jsx2("span", { style: S.role, children: "admin" }),
202
- email ? /* @__PURE__ */ jsx2("span", { style: S.muted, children: email }) : null
203
- ] }),
204
- /* @__PURE__ */ jsx2("button", { className: "btn secondary mini", onClick: () => signOut(), children: "Sign out" })
205
- ] })
206
- ] }),
207
- /* @__PURE__ */ jsxs2("main", { className: "shell-main", style: S.main, children: [
208
- /* @__PURE__ */ jsx2(ThemeWarning, {}),
209
- active ? active.render({ client, getToken, navigate: go }) : null
210
- ] })
211
- ] });
212
- }
213
-
214
- // src/ui/admin-availability.tsx
215
- import { useState as useState4 } from "react";
216
- import { Button, Checkbox, Field, Input } from "@odla-ai/ui/components";
217
-
218
- // src/ui/admin-api.ts
219
- import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
220
- async function adminFetch(getToken, path, init) {
221
- const token = await getToken();
222
- const headers = new Headers(init?.headers);
223
- if (token) headers.set("authorization", `Bearer ${token}`);
224
- if (init?.body) headers.set("content-type", "application/json");
225
- const res = await fetch(path, { ...init, headers });
226
- const body = await res.json().catch(() => ({}));
227
- if (!res.ok) throw new Error(typeof body.error === "string" ? body.error : `request failed (${res.status})`);
228
- return body;
229
- }
230
- function useAdminResource(getToken, path) {
231
- const [data, setData] = useState3(null);
232
- const [loading, setLoading] = useState3(true);
233
- const [error, setError] = useState3(null);
234
- const [nonce, setNonce] = useState3(0);
235
- useEffect3(() => {
236
- let live = true;
237
- setLoading(true);
238
- setError(null);
239
- adminFetch(getToken, path).then((d) => live && setData(d)).catch((e) => live && setError(e instanceof Error ? e.message : String(e))).finally(() => live && setLoading(false));
240
- return () => {
241
- live = false;
242
- };
243
- }, [getToken, path, nonce]);
244
- const refresh = useCallback2(() => setNonce((n) => n + 1), []);
245
- return { data, loading, error, refresh };
246
- }
247
-
248
- // src/ui/admin-availability.tsx
249
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
250
- var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
251
- function AvailabilityBody({ ctx }) {
252
- const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/scheduling");
253
- const [draft, setDraft] = useState4(null);
254
- const [busy, setBusy] = useState4(false);
255
- const [note, setNote] = useState4(null);
256
- const [errors, setErrors] = useState4({});
257
- const cfg = draft ?? data?.scheduling ?? null;
258
- if (loading) return /* @__PURE__ */ jsx3(AdminNote, { children: "Loading\u2026" });
259
- if (error || !cfg) return /* @__PURE__ */ jsxs3(AdminNote, { children: [
260
- "Couldn\u2019t load availability",
261
- error ? `: ${error}` : "",
262
- "."
263
- ] });
264
- const edit = (patch) => setDraft({ ...cfg, ...patch });
265
- const toggleDay = (d) => edit({ days: cfg.days.includes(d) ? cfg.days.filter((x) => x !== d) : [...cfg.days, d].sort() });
266
- const num = (v, fallback) => Number.isFinite(Number(v)) ? Number(v) : fallback;
267
- const save = async () => {
268
- setBusy(true);
269
- setNote(null);
270
- setErrors({});
271
- const token = await ctx.getToken();
272
- try {
273
- const r = await fetch("/api/admin/scheduling", { method: "PUT", headers: { authorization: `Bearer ${token ?? ""}`, "content-type": "application/json" }, body: JSON.stringify(cfg) });
274
- const body = await r.json().catch(() => ({}));
275
- if (!r.ok) {
276
- setErrors(body.errors ?? {});
277
- setNote(body.error ?? `save failed (${r.status})`);
278
- return;
279
- }
280
- setDraft(null);
281
- refresh();
282
- setNote("Saved.");
283
- } catch (e) {
284
- setNote(e instanceof Error ? e.message : String(e));
285
- } finally {
286
- setBusy(false);
287
- }
288
- };
289
- const numField = (key, label) => /* @__PURE__ */ jsx3(Field, { label, error: errors[key], children: /* @__PURE__ */ jsx3(Input, { type: "number", value: cfg[key], onChange: (e) => edit({ [key]: num(e.target.value, cfg[key]) }) }) });
290
- return /* @__PURE__ */ jsx3(AdminPage, { children: /* @__PURE__ */ jsxs3(Panel, { title: "Booking availability", actions: note ? /* @__PURE__ */ jsx3("span", { className: "muted", children: note }) : void 0, children: [
291
- /* @__PURE__ */ jsx3(Field, { label: "Days", error: errors.days, children: /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 14, flexWrap: "wrap" }, children: DAYS.map((d, i) => /* @__PURE__ */ jsx3(Checkbox, { label: d, checked: cfg.days.includes(i), onChange: () => toggleDay(i) }, d)) }) }),
292
- /* @__PURE__ */ jsxs3("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 14 }, children: [
293
- numField("startHour", "Start hour (0\u201324)"),
294
- numField("endHour", "End hour (0\u201324)"),
295
- numField("slotMinutes", "Slot length (minutes)"),
296
- numField("minNoticeHours", "Minimum notice (hours)"),
297
- numField("windowDays", "Booking window (days)"),
298
- /* @__PURE__ */ jsx3(Field, { label: "Timezone", error: errors.timezone, children: /* @__PURE__ */ jsx3(Input, { value: cfg.timezone, onChange: (e) => edit({ timezone: e.target.value }) }) })
299
- ] }),
300
- /* @__PURE__ */ jsx3(Field, { label: "Event summary template", error: errors.summaryTemplate, children: /* @__PURE__ */ jsx3(Input, { value: cfg.summaryTemplate, onChange: (e) => edit({ summaryTemplate: e.target.value }) }) }),
301
- /* @__PURE__ */ jsx3("div", { children: /* @__PURE__ */ jsx3(Button, { disabled: !draft || busy, onClick: () => void save(), children: "Save availability" }) })
302
- ] }) });
303
- }
304
- function availabilitySection(options = {}) {
305
- const { id = "availability", label = "Availability" } = options;
306
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(AvailabilityBody, { ctx }) };
307
- }
308
-
309
- // src/ui/admin-billing.tsx
310
- import { DataTable, StatBand } from "@odla-ai/ui/components";
311
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
312
- var money = (cents) => `$${(cents / 100).toLocaleString()}`;
313
- var date = (t) => t ? new Date(t).toLocaleDateString([], { dateStyle: "medium" }) : "\u2014";
314
- var COLUMNS = [
315
- { key: "name", header: "Name", cell: (r) => r.name, sortAs: "string" },
316
- { key: "email", header: "Email", cell: (r) => r.email },
317
- { key: "applicationStatus", header: "Application", cell: (r) => r.applicationStatus },
318
- { key: "subscriptionStatus", header: "Subscription", cell: (r) => r.subscriptionStatus ? `${r.subscriptionStatus}${r.cancelAtPeriodEnd ? " \xB7 cancelling" : ""}` : "\u2014" },
319
- { key: "amountCents", header: "Amount", cell: (r) => `${money(r.amountCents)}/${r.interval === "month" ? "mo" : "yr"}`, sortAs: "number", sortValue: (r) => r.amountCents },
320
- { key: "renewalAt", header: "Renews", cell: (r) => date(r.renewalAt), sortAs: "number", sortValue: (r) => r.renewalAt ?? 0 }
321
- ];
322
- function BillingBody({ ctx }) {
323
- const { data, loading, error } = useAdminResource(ctx.getToken, "/api/admin/billing");
324
- if (loading) return /* @__PURE__ */ jsx4(AdminNote, { children: "Loading\u2026" });
325
- if (error || !data) return /* @__PURE__ */ jsxs4(AdminNote, { children: [
326
- "Couldn\u2019t load billing",
327
- error ? `: ${error}` : "",
328
- "."
329
- ] });
330
- if (!data.billingReady) return /* @__PURE__ */ jsx4(AdminNote, { children: "Billing isn\u2019t configured \u2014 no Stripe key is vaulted for this group." });
331
- const s = data.summary;
332
- const stats = s ? [
333
- { value: s.activeCount, label: "Active" },
334
- { value: money(s.annualizedCents), label: "Annualized" },
335
- { value: s.renewingSoonCount, label: "Renewing \u226460d" },
336
- { value: s.pastDueCount, label: "Past due" }
337
- ] : [];
338
- return /* @__PURE__ */ jsxs4(AdminPage, { children: [
339
- s ? /* @__PURE__ */ jsx4(StatBand, { stats }) : null,
340
- /* @__PURE__ */ jsxs4(Panel, { title: "Subscriptions", actions: data.testMode ? /* @__PURE__ */ jsx4("span", { className: "badge", children: "Stripe test mode" }) : void 0, children: [
341
- data.truncated ? /* @__PURE__ */ jsx4("p", { className: "badge", children: "Showing the first 100 subscriptions \u2014 the list is truncated." }) : null,
342
- /* @__PURE__ */ jsx4(DataTable, { rows: data.rows, columns: COLUMNS, rowKey: (r) => r.id })
343
- ] })
344
- ] });
345
- }
346
- function billingSection(options = {}) {
347
- const { id = "billing", label = "Billing" } = options;
348
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx4(BillingBody, { ctx }) };
349
- }
350
-
351
- // src/ui/admin-dashboard.tsx
352
- import { StatBand as StatBand2 } from "@odla-ai/ui/components";
353
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
354
- var money2 = (cents) => `$${Math.round(cents / 100).toLocaleString()}`;
355
- var when = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
356
- function DashboardBody({ ctx }) {
357
- const { data, loading, error } = useAdminResource(ctx.getToken, "/api/admin/dashboard");
358
- if (loading) return /* @__PURE__ */ jsx5(AdminNote, { children: "Loading\u2026" });
359
- if (error || !data) return /* @__PURE__ */ jsxs5(AdminNote, { children: [
360
- "Couldn\u2019t load the dashboard",
361
- error ? `: ${error}` : "",
362
- "."
363
- ] });
364
- const kpis = [
365
- { value: data.applications.total, label: "Applications", description: `+${data.applications.last7} in the last 7 days` },
366
- { value: data.calls.upcoming, label: "Upcoming calls", description: data.calls.needsAttention ? `${data.calls.needsAttention} need attention` : "all on track" }
367
- ];
368
- if (data.revenue.billingReady) {
369
- kpis.push({ value: data.revenue.activeCount ?? 0, label: "Active members", description: "paid subscriptions" });
370
- kpis.push({ value: money2(data.revenue.annualRunRateCents ?? 0), label: "Annual run rate", description: "from active subscriptions" });
371
- }
372
- const stages = Object.entries(data.pipeline).map(([stage, count]) => ({
373
- value: count,
374
- label: stage,
375
- description: data.pipelineDelta[stage] ? `+${data.pipelineDelta[stage]} this week` : void 0
376
- }));
377
- return /* @__PURE__ */ jsxs5(AdminPage, { children: [
378
- /* @__PURE__ */ jsx5(StatBand2, { stats: kpis }),
379
- /* @__PURE__ */ jsx5(Panel, { title: "Pipeline", children: /* @__PURE__ */ jsx5(StatBand2, { stats: stages }) }),
380
- /* @__PURE__ */ jsx5(Panel, { title: /* @__PURE__ */ jsxs5(Fragment2, { children: [
381
- "Upcoming calls ",
382
- /* @__PURE__ */ jsxs5("span", { className: "muted", style: { fontWeight: 400 }, children: [
383
- "\xB7 ",
384
- data.timezone
385
- ] })
386
- ] }), children: data.agenda.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "muted", style: { margin: 0 }, children: "No calls scheduled." }) : /* @__PURE__ */ jsx5("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 10 }, children: data.agenda.map((m) => /* @__PURE__ */ jsxs5("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
387
- /* @__PURE__ */ jsx5("strong", { style: { minWidth: 170 }, children: when(m.startAt) }),
388
- /* @__PURE__ */ jsxs5("span", { children: [
389
- m.name,
390
- m.email ? ` \xB7 ${m.email}` : ""
391
- ] }),
392
- m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs5("span", { className: "badge", children: [
393
- "drift: ",
394
- m.drift
395
- ] }) : null,
396
- m.meetUrl ? /* @__PURE__ */ jsx5("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null
397
- ] }, m.id)) }) })
398
- ] });
399
- }
400
- function dashboardSection(options = {}) {
401
- const { id = "dashboard", label = "Dashboard" } = options;
402
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx5(DashboardBody, { ctx }) };
403
- }
404
-
405
- // src/ui/admin-email.tsx
406
- import { useState as useState5 } from "react";
407
- import { Button as Button2, Checkbox as Checkbox2, DataTable as DataTable2, Field as Field2, Input as Input2, Textarea } from "@odla-ai/ui/components";
408
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
409
- var LOG_COLUMNS = [
410
- { key: "sentAt", header: "Sent", cell: (r) => new Date(r.sentAt).toLocaleString(), sortAs: "number", sortValue: (r) => r.sentAt },
411
- { key: "template", header: "Template", cell: (r) => r.template },
412
- { key: "to", header: "To", cell: (r) => r.to },
413
- { key: "status", header: "Status", cell: (r) => r.error ? /* @__PURE__ */ jsx6("span", { className: "badge", children: "failed" }) : r.redirected ? /* @__PURE__ */ jsx6("span", { className: "badge", children: "redirected" }) : "delivered" }
414
- ];
415
- function EmailBody({ ctx }) {
416
- const cfg = useAdminResource(ctx.getToken, "/api/admin/group/email");
417
- const log = useAdminResource(ctx.getToken, "/api/admin/email/log");
418
- const [draft, setDraft] = useState5(null);
419
- const [busy, setBusy] = useState5(null);
420
- const [note, setNote] = useState5(null);
421
- const data = draft ?? cfg.data;
422
- if (cfg.loading) return /* @__PURE__ */ jsx6(AdminNote, { children: "Loading\u2026" });
423
- if (cfg.error || !data) return /* @__PURE__ */ jsxs6(AdminNote, { children: [
424
- "Couldn\u2019t load email config",
425
- cfg.error ? `: ${cfg.error}` : "",
426
- "."
427
- ] });
428
- const edit = (patch) => setDraft({ ...data, ...patch });
429
- const editTemplate = (key, patch) => edit({ emailTemplates: { ...data.emailTemplates, [key]: { ...data.emailTemplates[key], ...patch } } });
430
- const run = async (key, fn) => {
431
- setBusy(key);
432
- setNote(null);
433
- try {
434
- await fn();
435
- } catch (e) {
436
- setNote(e instanceof Error ? e.message : String(e));
437
- } finally {
438
- setBusy(null);
439
- }
440
- };
441
- const save = () => run("save", async () => {
442
- await adminFetch(ctx.getToken, "/api/admin/group/email", { method: "PUT", body: JSON.stringify(data) });
443
- setDraft(null);
444
- cfg.refresh();
445
- setNote("Saved.");
446
- });
447
- const test = (template) => run(`test:${template}`, async () => {
448
- const r = await adminFetch(ctx.getToken, "/api/admin/email/test", { method: "POST", body: JSON.stringify({ template }) });
449
- setNote(r.ok ? `Sent ${template} to ${r.to}${r.redirected ? " (dev-redirected)" : ""}.` : "Test send did not deliver.");
450
- log.refresh();
451
- });
452
- return /* @__PURE__ */ jsxs6(AdminPage, { children: [
453
- /* @__PURE__ */ jsxs6(
454
- Panel,
455
- {
456
- title: "Delivery",
457
- actions: /* @__PURE__ */ jsxs6("span", { className: "muted", children: [
458
- data.envName,
459
- " \xB7 ",
460
- data.transport,
461
- data.fromEmail ? ` \xB7 ${data.fromEmail}` : ""
462
- ] }),
463
- children: [
464
- /* @__PURE__ */ jsx6(Field2, { label: "Notification address", children: /* @__PURE__ */ jsx6(Input2, { value: data.notificationEmail, onChange: (e) => edit({ notificationEmail: e.target.value }) }) }),
465
- /* @__PURE__ */ jsx6(Field2, { label: "Reply-to", children: /* @__PURE__ */ jsx6(Input2, { value: data.replyTo, onChange: (e) => edit({ replyTo: e.target.value }) }) }),
466
- /* @__PURE__ */ jsx6(Field2, { label: "Debug inbox", hint: "Non-prod sends redirect here.", children: /* @__PURE__ */ jsx6(Input2, { value: data.debugEmail, onChange: (e) => edit({ debugEmail: e.target.value }) }) })
467
- ]
468
- }
469
- ),
470
- Object.entries(data.emailTemplates).map(([key, t]) => /* @__PURE__ */ jsxs6(Panel, { title: key, actions: /* @__PURE__ */ jsx6(Button2, { variant: "secondary", mini: true, disabled: busy === `test:${key}`, onClick: () => void test(key), children: "Send test" }), children: [
471
- /* @__PURE__ */ jsx6(Checkbox2, { label: "Enabled", checked: t.enabled, onChange: (e) => editTemplate(key, { enabled: e.target.checked }) }),
472
- /* @__PURE__ */ jsx6(Field2, { label: "Subject", children: /* @__PURE__ */ jsx6(Input2, { value: t.subject, onChange: (e) => editTemplate(key, { subject: e.target.value }) }) }),
473
- /* @__PURE__ */ jsx6(Field2, { label: "Body", children: /* @__PURE__ */ jsx6(Textarea, { prose: true, rows: 5, value: t.text, onChange: (e) => editTemplate(key, { text: e.target.value }) }) })
474
- ] }, key)),
475
- /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
476
- /* @__PURE__ */ jsx6(Button2, { disabled: !draft || busy === "save", onClick: () => void save(), children: "Save changes" }),
477
- note ? /* @__PURE__ */ jsx6("span", { className: "muted", children: note }) : null
478
- ] }),
479
- /* @__PURE__ */ jsx6(Panel, { title: "Send log", children: /* @__PURE__ */ jsx6(DataTable2, { rows: log.data?.sends ?? [], columns: LOG_COLUMNS, rowKey: (r) => r.id }) })
480
- ] });
481
- }
482
- function emailSection(options = {}) {
483
- const { id = "email", label = "Email" } = options;
484
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx6(EmailBody, { ctx }) };
485
- }
486
-
487
- // src/ui/admin-meetings.tsx
488
- import { useState as useState6 } from "react";
489
- import { Button as Button3 } from "@odla-ai/ui/components";
490
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
491
- var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
492
- function MeetingsBody({ ctx }) {
493
- const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
494
- const [busy, setBusy] = useState6(null);
495
- const [note, setNote] = useState6(null);
496
- if (loading) return /* @__PURE__ */ jsx7(AdminNote, { children: "Loading\u2026" });
497
- if (error || !data) return /* @__PURE__ */ jsxs7(AdminNote, { children: [
498
- "Couldn\u2019t load meetings",
499
- error ? `: ${error}` : "",
500
- "."
501
- ] });
502
- const run = async (id, fn) => {
503
- setBusy(id);
504
- setNote(null);
505
- try {
506
- await fn();
507
- refresh();
508
- } catch (e) {
509
- setNote(e instanceof Error ? e.message : String(e));
510
- } finally {
511
- setBusy(null);
512
- }
513
- };
514
- const cancel = (id) => {
515
- if (!globalThis.confirm?.("Cancel this call? Google notifies the attendee.")) return;
516
- void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/cancel`, { method: "POST" }));
517
- };
518
- const reschedule = (id) => {
519
- const input = globalThis.prompt?.("New start time (e.g. 2026-08-01 14:00):");
520
- if (!input) return;
521
- const startAt = Date.parse(input);
522
- if (!Number.isFinite(startAt)) {
523
- setNote("Couldn\u2019t parse that time.");
524
- return;
525
- }
526
- void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
527
- };
528
- return /* @__PURE__ */ jsx7(AdminPage, { children: /* @__PURE__ */ jsx7(Panel, { title: /* @__PURE__ */ jsxs7(Fragment3, { children: [
529
- "Agenda ",
530
- /* @__PURE__ */ jsxs7("span", { className: "muted", style: { fontWeight: 400 }, children: [
531
- "\xB7 ",
532
- data.timezone
533
- ] })
534
- ] }), actions: note ? /* @__PURE__ */ jsx7("span", { className: "muted", children: note }) : void 0, children: data.meetings.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "muted", style: { margin: 0 }, children: "No meetings." }) : /* @__PURE__ */ jsx7("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 12 }, children: data.meetings.map((m) => /* @__PURE__ */ jsxs7("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", opacity: m.status === "cancelled" ? 0.55 : 1 }, children: [
535
- /* @__PURE__ */ jsx7("strong", { style: { minWidth: 170 }, children: when2(m.startAt) }),
536
- /* @__PURE__ */ jsx7("span", { style: { flex: 1 }, children: m.applicant ? `${m.applicant.name} \xB7 ${m.applicant.email}` : "(unknown)" }),
537
- /* @__PURE__ */ jsx7("span", { className: "badge", children: m.status }),
538
- m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs7("span", { className: "badge", children: [
539
- "drift: ",
540
- m.drift
541
- ] }) : null,
542
- m.meetUrl ? /* @__PURE__ */ jsx7("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null,
543
- m.status === "scheduled" ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
544
- /* @__PURE__ */ jsx7(Button3, { variant: "secondary", mini: true, disabled: busy === m.id, onClick: () => reschedule(m.id), children: "Reschedule" }),
545
- /* @__PURE__ */ jsx7(Button3, { variant: "danger", mini: true, disabled: busy === m.id, onClick: () => cancel(m.id), children: "Cancel" })
546
- ] }) : null
547
- ] }, m.id)) }) }) });
548
- }
549
- function meetingsSection(options = {}) {
550
- const { id = "meetings", label = "Meetings" } = options;
551
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx7(MeetingsBody, { ctx }) };
552
- }
553
-
554
- // src/ui/admin-people.tsx
555
- import { useState as useState9 } from "react";
556
- import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
557
-
558
- // src/ui/admin-record-actions.tsx
559
- import { useEffect as useEffect4, useState as useState7 } from "react";
560
- import { Button as Button4, Field as Field3, Select } from "@odla-ai/ui/components";
561
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
562
- function RecordActions({ getToken, record, roles, onChanged }) {
563
- const userId = typeof record.clerkUserId === "string" ? record.clerkUserId : null;
564
- const applicationId = typeof record.fields?.applicationId === "string" ? record.fields.applicationId : null;
565
- const [access, setAccess] = useState7(null);
566
- const [busy, setBusy] = useState7(null);
567
- const [note, setNote] = useState7(null);
568
- useEffect4(() => {
569
- if (!userId) {
570
- setAccess(null);
571
- return;
572
- }
573
- let live = true;
574
- adminFetch(getToken, `/api/admin/people/access?userId=${encodeURIComponent(userId)}`).then((a) => live && setAccess(a)).catch(() => live && setAccess(null));
575
- return () => {
576
- live = false;
577
- };
578
- }, [getToken, userId]);
579
- const act = async (key, run, ok) => {
580
- setBusy(key);
581
- setNote(null);
582
- try {
583
- await run();
584
- setNote(ok);
585
- onChanged();
586
- } catch (e) {
587
- setNote(e instanceof Error ? e.message : String(e));
588
- } finally {
589
- setBusy(null);
590
- }
591
- };
592
- const setRole = (role) => act("role", () => adminFetch(getToken, "/api/admin/people/role", { method: "POST", body: JSON.stringify({ userId, role }) }), `Role set to ${role}.`);
593
- const approve = () => act("approve", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/approve`, { method: "POST" }), "Approved.");
594
- const refund = () => act("refund", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/refund`, { method: "POST" }), "Refund issued.");
595
- if (!userId && !applicationId) return null;
596
- return /* @__PURE__ */ jsxs8("section", { className: "panel", style: { display: "flex", flexDirection: "column", gap: 14, marginTop: 16 }, children: [
597
- /* @__PURE__ */ jsx8("h3", { style: { margin: 0, fontSize: 16 }, children: "Access & lifecycle" }),
598
- userId ? /* @__PURE__ */ jsx8(Field3, { label: "Role", hint: access?.superAdmin ? "Super-admin \u2014 managed in odla Studio." : void 0, children: /* @__PURE__ */ jsx8(
599
- Select,
600
- {
601
- value: access?.role ?? "",
602
- disabled: busy === "role" || access?.superAdmin,
603
- options: roles.map((r) => ({ value: r, label: r })),
604
- onChange: (e) => void setRole(e.target.value)
605
- }
606
- ) }) : /* @__PURE__ */ jsx8("p", { className: "muted", style: { margin: 0 }, children: "No linked account yet \u2014 role is set once they sign in." }),
607
- applicationId ? /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 8 }, children: [
608
- /* @__PURE__ */ jsx8(Button4, { variant: "secondary", mini: true, disabled: busy === "approve", onClick: () => void approve(), children: "Approve" }),
609
- /* @__PURE__ */ jsx8(Button4, { variant: "danger", mini: true, disabled: busy === "refund", onClick: () => void refund(), children: "Refund" })
610
- ] }) : null,
611
- note ? /* @__PURE__ */ jsx8("p", { className: "muted", style: { margin: 0 }, children: note }) : null
612
- ] });
613
- }
614
-
615
- // src/ui/admin-network.tsx
616
- import { useState as useState8 } from "react";
617
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
618
- function NetworkShareActions(props) {
619
- const { recordId, recordType, getToken } = props;
620
- const targets = useAdminResource(getToken, "/api/admin/network/targets");
621
- const [busy, setBusy] = useState8(null);
622
- const [message, setMessage] = useState8(null);
623
- const compatible = (targets.data?.targets ?? []).filter((target) => target.types.includes(recordType));
624
- if (targets.loading || targets.error || compatible.length === 0) return null;
625
- const push = async (target) => {
626
- setBusy(target.id);
627
- setMessage(null);
628
- try {
629
- const result = await adminFetch(getToken, "/api/admin/network/push", {
630
- method: "POST",
631
- body: JSON.stringify({ recordId, targetIds: [target.id] })
632
- });
633
- const delivery = result.results[0];
634
- setMessage(delivery?.ok ? `Shared with ${target.name}.` : delivery?.error ?? `Could not share with ${target.name}.`);
635
- } catch (err) {
636
- setMessage(err instanceof Error ? err.message : "Delivery failed.");
637
- } finally {
638
- setBusy(null);
639
- }
640
- };
641
- return /* @__PURE__ */ jsxs9("section", { className: "panel", style: { marginTop: 14, display: "flex", flexDirection: "column", gap: 10 }, children: [
642
- /* @__PURE__ */ jsxs9("div", { children: [
643
- /* @__PURE__ */ jsx9("h3", { style: { margin: 0, fontSize: 16 }, children: "Share to a follower" }),
644
- /* @__PURE__ */ jsx9("p", { className: "muted", style: { margin: "4px 0 0", fontSize: 13 }, children: "Sends the target's allowlisted fields. Its pipeline and account state remain local." })
645
- ] }),
646
- /* @__PURE__ */ jsx9("div", { style: { display: "flex", flexWrap: "wrap", gap: 8 }, children: compatible.map((target) => /* @__PURE__ */ jsx9("button", { className: "btn secondary mini", disabled: busy !== null, onClick: () => void push(target), children: busy === target.id ? "Sharing\u2026" : `Share with ${target.name}` }, target.id)) }),
647
- message ? /* @__PURE__ */ jsx9("p", { role: "status", style: { margin: 0, fontSize: 13 }, children: message }) : null
648
- ] });
649
- }
650
-
651
- // src/ui/admin-people.tsx
652
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
653
- var DEFAULT_ROLES = ["provisional", "member", "admin"];
654
- function PeopleBody(props) {
655
- const { crm, client, type, getToken, roles, lifecycle } = props;
656
- const hookClient = client;
657
- const query = useCrmQuery(hookClient, type);
658
- const [openId, setOpenId] = useState9(null);
659
- const record = useCrmRecord(hookClient, openId);
660
- const [saving, setSaving] = useState9(false);
661
- const saveFields = async (input) => {
662
- if (!openId) return;
663
- setSaving(true);
664
- try {
665
- await client.updateRecord(openId, { input });
666
- record.refresh();
667
- query.refresh();
668
- } finally {
669
- setSaving(false);
670
- }
671
- };
672
- const moveStage = async (to) => {
673
- if (!openId) return;
674
- await client.setStage(openId, to);
675
- record.refresh();
676
- query.refresh();
677
- };
678
- const addTag = async (tag) => {
679
- if (openId) {
680
- await client.addTag(openId, tag);
681
- record.refresh();
682
- }
683
- };
684
- const removeTag = async (tag) => {
685
- if (openId) {
686
- await client.removeTag(openId, tag);
687
- record.refresh();
688
- }
689
- };
690
- return /* @__PURE__ */ jsxs10(AdminPage, { children: [
691
- /* @__PURE__ */ jsx10(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
692
- record.detail ? /* @__PURE__ */ jsxs10("div", { children: [
693
- /* @__PURE__ */ jsx10(
694
- RecordPanel,
695
- {
696
- crm,
697
- detail: record.detail,
698
- onSaveFields: saveFields,
699
- onMoveStage: moveStage,
700
- onAddTag: addTag,
701
- onRemoveTag: removeTag,
702
- saving
703
- }
704
- ),
705
- lifecycle ? /* @__PURE__ */ jsx10(
706
- RecordActions,
707
- {
708
- getToken,
709
- record: record.detail.record,
710
- roles,
711
- onChanged: () => {
712
- record.refresh();
713
- query.refresh();
714
- }
715
- }
716
- ) : null,
717
- /* @__PURE__ */ jsx10(
718
- NetworkShareActions,
719
- {
720
- recordId: record.detail.record.id,
721
- recordType: record.detail.record.type,
722
- getToken
723
- }
724
- )
725
- ] }) : null
726
- ] });
727
- }
728
- function collectionSection(options) {
729
- const { crm, type } = options;
730
- let fallbackLabel = type;
731
- try {
732
- fallbackLabel = crm.type(type).labelPlural ?? crm.type(type).label ?? type;
733
- } catch {
734
- }
735
- const { id = type, label = fallbackLabel, lifecycle = type === "person", roles = DEFAULT_ROLES } = options;
736
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx10(PeopleBody, { crm, client: ctx.client, type, getToken: ctx.getToken, roles, lifecycle }) };
737
- }
738
- function peopleSection(options) {
739
- const { crm, id = "people", label = "People", type = "person", roles } = options;
740
- return collectionSection({ crm, type, id, label, ...roles ? { roles } : {} });
741
- }
742
-
743
- // src/ui/admin-defaults.ts
744
- function defaultAdminSections(chapter) {
745
- const collections = Object.entries(chapter.crm.config.types).map(
746
- ([type, def]) => collectionSection({
747
- crm: chapter.crm,
748
- type,
749
- id: type === "person" ? "people" : type,
750
- label: def.labelPlural ?? def.label,
751
- lifecycle: chapter.mode === "chapter" && type === "person",
752
- roles: chapter.auth.ladder
753
- })
754
- );
755
- if (chapter.mode === "hub") return collections;
756
- return [
757
- dashboardSection(),
758
- ...collections,
759
- meetingsSection(),
760
- availabilitySection(),
761
- billingSection(),
762
- emailSection()
763
- ];
764
- }
765
-
766
- // src/ui/admin.tsx
767
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
768
- function Authed(props) {
769
- const { sections, basePath, brand, crmBasePath, apiBase, routing } = props;
770
- const { getToken, signOut } = useClerkAuth();
771
- const client = useMemo(
772
- () => new CrmClient({
773
- basePath: crmBasePath,
774
- headers: async () => {
775
- const t = await getToken();
776
- return t ? { authorization: `Bearer ${t}` } : {};
777
- }
778
- }),
779
- [getToken, crmBasePath]
780
- );
781
- const [state, setState] = useState10({
782
- status: "checking"
783
- });
784
- useEffect5(() => {
785
- let live = true;
786
- void (async () => {
787
- try {
788
- const t = await getToken();
789
- const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
790
- const body = await res.json().catch(() => ({}));
791
- if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
792
- } catch {
793
- if (live) setState({ status: "denied" });
794
- }
795
- })();
796
- return () => {
797
- live = false;
798
- };
799
- }, [getToken, apiBase]);
800
- if (state.status === "checking") {
801
- return /* @__PURE__ */ jsx11(Gate, { brand, children: /* @__PURE__ */ jsx11("p", { style: S.muted, children: "Checking access\u2026" }) });
802
- }
803
- if (state.status === "denied") {
804
- return /* @__PURE__ */ jsx11(Gate, { brand, children: /* @__PURE__ */ jsxs11("div", { className: "card", style: S.card, children: [
805
- /* @__PURE__ */ jsx11("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
806
- /* @__PURE__ */ jsxs11("p", { style: S.muted, children: [
807
- state.email ? `${state.email} isn't` : "This account isn't",
808
- " on the admin list. Ask an existing admin to add you in odla Studio."
809
- ] }),
810
- /* @__PURE__ */ jsx11("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
811
- ] }) });
812
- }
813
- return /* @__PURE__ */ jsx11(
814
- AdminShell,
815
- {
816
- sections,
817
- basePath,
818
- brand,
819
- client,
820
- getToken,
821
- signOut,
822
- email: state.email ?? null,
823
- routing
824
- }
825
- );
826
- }
827
- function ChapterAdmin(props) {
828
- const sections = useMemo(
829
- () => props.sections ?? (props.chapter ? defaultAdminSections(props.chapter) : []),
830
- [props.sections, props.chapter]
831
- );
832
- const basePath = props.basePath ?? "/admin";
833
- const brand = props.brand ?? {
834
- name: props.chapter?.name ?? "Admin",
835
- badge: props.chapter?.brand.badge,
836
- wordmark: props.chapter?.brand.wordmark
837
- };
838
- const crmBasePath = props.crmBasePath ?? "/api/crm";
839
- const apiBase = props.apiBase ?? "";
840
- const routing = props.routing ?? "query";
841
- const brandStyle = /* @__PURE__ */ jsx11(BrandStyle, { brand: props.chapter?.brand });
842
- const [pk, setPk] = useState10(void 0);
843
- useEffect5(() => {
844
- let live = true;
845
- void (async () => {
846
- try {
847
- const res = await fetch(`${apiBase}/api/config`);
848
- const body = await res.json();
849
- if (live) setPk(body.clerkPublishableKey ?? null);
850
- } catch {
851
- if (live) setPk(null);
852
- }
853
- })();
854
- return () => {
855
- live = false;
856
- };
857
- }, [apiBase]);
858
- if (pk === void 0) {
859
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
860
- brandStyle,
861
- /* @__PURE__ */ jsx11(Gate, { brand, children: /* @__PURE__ */ jsx11("p", { style: S.muted, children: "Loading\u2026" }) })
862
- ] });
863
- }
864
- if (!pk) {
865
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
866
- brandStyle,
867
- /* @__PURE__ */ jsx11(Gate, { brand, children: /* @__PURE__ */ jsxs11("div", { className: "card", style: S.card, children: [
868
- /* @__PURE__ */ jsx11("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
869
- /* @__PURE__ */ jsx11("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
870
- ] }) })
871
- ] });
872
- }
873
- if (sections.length === 0) {
874
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
875
- brandStyle,
876
- /* @__PURE__ */ jsx11(Gate, { brand, children: /* @__PURE__ */ jsxs11("div", { className: "card", style: S.card, children: [
877
- /* @__PURE__ */ jsx11("h2", { style: { marginTop: 0 }, children: "No admin sections" }),
878
- /* @__PURE__ */ jsx11("p", { style: S.muted, children: "Pass a chapter or at least one explicit section." })
879
- ] }) })
880
- ] });
881
- }
882
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
883
- brandStyle,
884
- /* @__PURE__ */ jsxs11(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
885
- /* @__PURE__ */ jsx11(SignedOut, { children: /* @__PURE__ */ jsx11(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx11(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
886
- /* @__PURE__ */ jsx11(SignedIn, { children: /* @__PURE__ */ jsx11(
887
- Authed,
888
- {
889
- sections,
890
- basePath,
891
- brand,
892
- crmBasePath,
893
- apiBase,
894
- routing
895
- }
896
- ) })
897
- ] })
898
- ] });
899
- }
900
-
901
- export {
902
- AdminPage,
903
- Panel,
904
- ThemeWarning,
905
- AdminNote,
906
- adminSectionFromUrl,
907
- adminSectionHref,
908
- adminFetch,
909
- useAdminResource,
910
- availabilitySection,
911
- billingSection,
912
- dashboardSection,
913
- emailSection,
914
- meetingsSection,
915
- RecordActions,
916
- NetworkShareActions,
917
- collectionSection,
918
- peopleSection,
919
- defaultAdminSections,
920
- ChapterAdmin
921
- };
922
- //# sourceMappingURL=chunk-UIWFEESI.js.map