@odla-ai/chapter 0.20.2 → 0.21.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.
@@ -0,0 +1,1481 @@
1
+ import {
2
+ BrandStyle
3
+ } from "./chunk-3JG5X2LT.js";
4
+
5
+ // src/ui/admin.tsx
6
+ import { useEffect as useEffect7, useMemo, useState as useState11 } from "react";
7
+ import {
8
+ ClerkGate,
9
+ SignedIn,
10
+ SignedOut,
11
+ SignIn,
12
+ useClerkAuth,
13
+ clerkAppearanceFromTokens
14
+ } from "@odla-ai/auth-clerk";
15
+ import { CrmClient } from "@odla-ai/crm";
16
+ import { ThemeScope } from "@odla-ai/ui/components";
17
+
18
+ // src/ui/chrome.tsx
19
+ import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
20
+ import { Tabs, TopBar, TopBarLink } from "@odla-ai/ui/components";
21
+
22
+ // src/ui/admin-layout.tsx
23
+ import { useEffect, useState } from "react";
24
+ import { jsx, jsxs } from "react/jsx-runtime";
25
+ var PAGE = { maxWidth: 1120, margin: "0 auto", padding: "28px 20px 64px", display: "flex", flexDirection: "column", gap: 20 };
26
+ var HEAD = { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap" };
27
+ function AdminPage({ children }) {
28
+ return /* @__PURE__ */ jsx("div", { style: PAGE, children });
29
+ }
30
+ function Panel({ title, actions, children }) {
31
+ return /* @__PURE__ */ jsxs("section", { className: "panel", style: { display: "flex", flexDirection: "column", gap: 14 }, children: [
32
+ title || actions ? /* @__PURE__ */ jsxs("header", { style: HEAD, children: [
33
+ title ? /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 18 }, children: title }) : /* @__PURE__ */ jsx("span", {}),
34
+ actions ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: actions }) : null
35
+ ] }) : null,
36
+ children
37
+ ] });
38
+ }
39
+ function themeLooksMissing() {
40
+ if (typeof document === "undefined") return false;
41
+ const probe = document.createElement("div");
42
+ probe.className = "panel";
43
+ probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none";
44
+ document.body.appendChild(probe);
45
+ const bg = getComputedStyle(probe).backgroundColor;
46
+ probe.remove();
47
+ return !bg || bg === "transparent" || bg === "rgba(0, 0, 0, 0)";
48
+ }
49
+ function ThemeWarning() {
50
+ const [missing, setMissing] = useState(false);
51
+ useEffect(() => setMissing(themeLooksMissing()), []);
52
+ if (!missing) return null;
53
+ return /* @__PURE__ */ jsxs(
54
+ "div",
55
+ {
56
+ role: "alert",
57
+ style: { background: "#7f1d1d", color: "#fff", padding: "12px 20px", fontSize: 14, lineHeight: 1.5, textAlign: "center" },
58
+ children: [
59
+ /* @__PURE__ */ jsx("strong", { children: "odla-ui theme tokens are missing." }),
60
+ " The console reads colors from a theme layer, not",
61
+ " ",
62
+ /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/index.css" }),
63
+ " alone. Import a theme before it \u2014 e.g.",
64
+ " ",
65
+ /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/themes/paper/tokens.css" }),
66
+ " and ",
67
+ /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/themes/paper/ui.css" }),
68
+ " \u2014 then",
69
+ " ",
70
+ /* @__PURE__ */ jsx("code", { children: "@odla-ai/ui/index.css" }),
71
+ ". See the @odla-ai/chapter README (\u201CTheme tokens\u201D)."
72
+ ]
73
+ }
74
+ );
75
+ }
76
+ function AdminNote({ children }) {
77
+ return /* @__PURE__ */ jsx(AdminPage, { children: /* @__PURE__ */ jsx("section", { className: "panel", children: /* @__PURE__ */ jsx("p", { className: "muted", style: { margin: 0 }, children }) }) });
78
+ }
79
+
80
+ // src/ui/admin-brand.tsx
81
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
82
+ var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
83
+ var brandDisplay = (brand) => {
84
+ const display = brand.wordmark ?? brand.name;
85
+ if (typeof display !== "string" || !display.includes("&")) return display;
86
+ return /* @__PURE__ */ jsx2("span", { children: display.split("&").map((part, index, all) => /* @__PURE__ */ jsxs2("span", { children: [
87
+ part,
88
+ index < all.length - 1 ? /* @__PURE__ */ jsx2("span", { className: "mk-amp", children: "&" }) : null
89
+ ] }, `${part}:${index}`)) });
90
+ };
91
+ var S = {
92
+ gate: {
93
+ minHeight: "100vh",
94
+ display: "grid",
95
+ placeItems: "center",
96
+ padding: 24,
97
+ 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%)"
98
+ },
99
+ gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
100
+ brandBox: { textAlign: "center", marginBottom: 22 },
101
+ badge: {
102
+ width: 52,
103
+ height: 52,
104
+ margin: "0 auto 14px",
105
+ display: "grid",
106
+ placeItems: "center",
107
+ fontSize: 15,
108
+ fontWeight: 700,
109
+ color: "var(--ui-on-accent)",
110
+ background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
111
+ borderRadius: 14,
112
+ boxShadow: "0 10px 28px var(--ui-accent-soft)"
113
+ },
114
+ badgeSm: {
115
+ width: 30,
116
+ height: 30,
117
+ display: "grid",
118
+ placeItems: "center",
119
+ fontSize: 10,
120
+ fontWeight: 700,
121
+ color: "var(--ui-on-accent)",
122
+ background: "var(--ui-accent)",
123
+ borderRadius: 8,
124
+ marginRight: 10
125
+ },
126
+ h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
127
+ muted: { color: "var(--ui-text-muted)" },
128
+ role: {
129
+ fontFamily: "var(--ui-font-mono)",
130
+ fontSize: 11,
131
+ textTransform: "uppercase",
132
+ letterSpacing: "0.04em",
133
+ padding: "2px 8px",
134
+ borderRadius: 999,
135
+ background: "var(--ui-accent-soft)",
136
+ border: "1px solid var(--ui-accent)",
137
+ color: "var(--ui-accent-strong)"
138
+ },
139
+ main: { minHeight: "calc(100vh - 61px)", background: "var(--ui-bg)" },
140
+ card: { width: "100%", textAlign: "center" },
141
+ masthead: {
142
+ display: "flex",
143
+ alignItems: "center",
144
+ gap: 16,
145
+ flexWrap: "wrap",
146
+ padding: "18px max(20px, calc((100% - 1120px) / 2))",
147
+ color: "var(--ui-bg)",
148
+ background: "var(--ui-text)"
149
+ },
150
+ mastheadBrand: {
151
+ display: "inline-flex",
152
+ alignItems: "center",
153
+ color: "inherit",
154
+ fontFamily: "var(--ui-font-display)",
155
+ fontSize: 22,
156
+ textDecoration: "none"
157
+ },
158
+ workspaceTabs: { maxWidth: 1160, margin: "0 auto", padding: "18px 20px 0" }
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
+
172
+ // src/ui/admin-route.ts
173
+ var DEFAULT_ALIASES = {
174
+ billing: { workspaceId: "dashboard", viewId: "billing" },
175
+ meetings: { workspaceId: "dashboard" },
176
+ calls: { workspaceId: "dashboard" },
177
+ availability: { workspaceId: "settings", viewId: "calendar" },
178
+ calendar: { workspaceId: "settings", viewId: "calendar" },
179
+ email: { workspaceId: "settings", viewId: "email" }
180
+ };
181
+ var cleanBase = (basePath) => basePath.length > 1 ? basePath.replace(/\/+$/, "") : basePath;
182
+ var decode = (segment) => {
183
+ if (!segment) return void 0;
184
+ try {
185
+ return decodeURIComponent(segment);
186
+ } catch {
187
+ return void 0;
188
+ }
189
+ };
190
+ var fromParts = (parts) => {
191
+ const workspaceId = decode(parts[0]);
192
+ if (!workspaceId) return null;
193
+ const viewId = decode(parts[1]);
194
+ const recordId = decode(parts[2]);
195
+ const detailTab = decode(parts[3]);
196
+ return {
197
+ workspaceId,
198
+ ...viewId ? { viewId } : {},
199
+ ...recordId ? { recordId } : {},
200
+ ...detailTab ? { detailTab } : {}
201
+ };
202
+ };
203
+ var resolve = (candidate, workspaceIds, aliases) => {
204
+ if (!candidate) return null;
205
+ if (workspaceIds.includes(candidate.workspaceId)) return candidate;
206
+ const alias = aliases[candidate.workspaceId] ?? DEFAULT_ALIASES[candidate.workspaceId];
207
+ return alias && workspaceIds.includes(alias.workspaceId) ? alias : null;
208
+ };
209
+ function adminRouteFromUrl(url, basePath, workspaceIds, aliases = {}) {
210
+ const fallback = { workspaceId: workspaceIds[0] ?? "" };
211
+ const hashParts = url.hash.replace(/^#\/?/, "").split("/").filter(Boolean);
212
+ const fromHash = resolve(fromParts(hashParts), workspaceIds, aliases);
213
+ if (fromHash) return fromHash;
214
+ const tab = url.searchParams.get("tab");
215
+ const fromQuery = resolve(
216
+ tab ? {
217
+ workspaceId: tab,
218
+ ...url.searchParams.get("view") ? { viewId: url.searchParams.get("view") } : {},
219
+ ...url.searchParams.get("record") ? { recordId: url.searchParams.get("record") } : {},
220
+ ...url.searchParams.get("detail") ? { detailTab: url.searchParams.get("detail") } : {}
221
+ } : null,
222
+ workspaceIds,
223
+ aliases
224
+ );
225
+ if (fromQuery) return fromQuery;
226
+ const base = cleanBase(basePath);
227
+ const rest = url.pathname.startsWith(base) ? url.pathname.slice(base.length).replace(/^\//, "") : "";
228
+ return resolve(fromParts(rest.split("/").filter(Boolean)), workspaceIds, aliases) ?? fallback;
229
+ }
230
+ var encodedParts = (route) => [route.workspaceId, route.viewId, route.recordId, route.detailTab].filter((part) => Boolean(part)).map(encodeURIComponent);
231
+ function adminRouteHref(current, basePath, route, routing = "fragment") {
232
+ const next = new URL(current);
233
+ const base = cleanBase(basePath);
234
+ const mountPaths = /* @__PURE__ */ new Set([base, `${base}/`, `${base}/index.html`]);
235
+ if (!mountPaths.has(next.pathname)) next.pathname = `${base}/`;
236
+ for (const key of ["tab", "view", "record", "detail"]) next.searchParams.delete(key);
237
+ if (routing === "fragment") {
238
+ next.hash = encodedParts(route).join("/");
239
+ } else if (routing === "query") {
240
+ next.hash = "";
241
+ next.searchParams.set("tab", route.workspaceId);
242
+ if (route.viewId) next.searchParams.set("view", route.viewId);
243
+ if (route.recordId) next.searchParams.set("record", route.recordId);
244
+ if (route.detailTab) next.searchParams.set("detail", route.detailTab);
245
+ } else {
246
+ next.hash = "";
247
+ next.pathname = `${base}/${encodeURIComponent(route.workspaceId)}`;
248
+ if (route.viewId) next.searchParams.set("view", route.viewId);
249
+ if (route.recordId) next.searchParams.set("record", route.recordId);
250
+ if (route.detailTab) next.searchParams.set("detail", route.detailTab);
251
+ }
252
+ return `${next.pathname}${next.search}${next.hash}`;
253
+ }
254
+ function adminSectionFromUrl(url, basePath, sectionIds) {
255
+ return adminRouteFromUrl(url, basePath, sectionIds).workspaceId;
256
+ }
257
+ function adminSectionHref(current, basePath, id, routing = "fragment") {
258
+ return adminRouteHref(current, basePath, { workspaceId: id }, routing);
259
+ }
260
+
261
+ // src/ui/chrome.tsx
262
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
263
+ var mergeRoute = (current, target) => {
264
+ if (typeof target === "string") return { workspaceId: target };
265
+ const changingWorkspace = target.workspaceId != null && target.workspaceId !== current.workspaceId;
266
+ const next = changingWorkspace ? { workspaceId: target.workspaceId } : { ...current, ...target.workspaceId ? { workspaceId: target.workspaceId } : {} };
267
+ for (const key of ["viewId", "recordId", "detailTab"]) {
268
+ const value = target[key];
269
+ if (value === null) delete next[key];
270
+ else if (value !== void 0) next[key] = value;
271
+ }
272
+ if (target.viewId !== void 0 && target.recordId === void 0) {
273
+ delete next.recordId;
274
+ delete next.detailTab;
275
+ }
276
+ if (target.recordId === null) delete next.detailTab;
277
+ return next;
278
+ };
279
+ function AdminShell({
280
+ workspaces,
281
+ basePath,
282
+ brand,
283
+ client,
284
+ getToken,
285
+ signOut,
286
+ email,
287
+ routing = "fragment",
288
+ chrome = "editorial",
289
+ renderHeader
290
+ }) {
291
+ const routeFromLocation = useCallback(() => {
292
+ if (typeof window === "undefined") return { workspaceId: workspaces[0]?.id ?? "" };
293
+ return adminRouteFromUrl(new URL(window.location.href), basePath, workspaces.map((workspace) => workspace.id));
294
+ }, [workspaces, basePath]);
295
+ const [route, setRoute] = useState2(routeFromLocation);
296
+ const href = useCallback(
297
+ (target) => typeof window === "undefined" ? `#${mergeRoute(route, target).workspaceId}` : adminRouteHref(new URL(window.location.href), basePath, mergeRoute(route, target), routing),
298
+ [route, basePath, routing]
299
+ );
300
+ const navigate = useCallback(
301
+ (target) => {
302
+ const next = mergeRoute(route, target);
303
+ if (typeof window !== "undefined") {
304
+ window.history.pushState(null, "", adminRouteHref(new URL(window.location.href), basePath, next, routing));
305
+ }
306
+ setRoute(next);
307
+ },
308
+ [route, basePath, routing]
309
+ );
310
+ useEffect2(() => {
311
+ const sync = () => setRoute(routeFromLocation());
312
+ window.addEventListener("popstate", sync);
313
+ window.addEventListener("hashchange", sync);
314
+ return () => {
315
+ window.removeEventListener("popstate", sync);
316
+ window.removeEventListener("hashchange", sync);
317
+ };
318
+ }, [routeFromLocation]);
319
+ useEffect2(() => {
320
+ if (typeof window === "undefined") return;
321
+ const canonical = adminRouteHref(new URL(window.location.href), basePath, route, routing);
322
+ const actual = `${window.location.pathname}${window.location.search}${window.location.hash}`;
323
+ if (canonical !== actual) window.history.replaceState(null, "", canonical);
324
+ }, []);
325
+ const context = { client, getToken, route, navigate, href };
326
+ const active = workspaces.find((workspace) => workspace.id === route.workspaceId) ?? workspaces[0];
327
+ const identity = /* @__PURE__ */ jsxs3(Fragment, { children: [
328
+ /* @__PURE__ */ jsx3("span", { style: { ...S.role, color: "inherit", borderColor: "currentColor", background: "transparent" }, children: "admin" }),
329
+ email ? /* @__PURE__ */ jsx3("span", { style: { opacity: 0.72, fontSize: 12 }, children: email }) : null,
330
+ /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" })
331
+ ] });
332
+ if (chrome === "topbar") {
333
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
334
+ /* @__PURE__ */ jsxs3(TopBar, { children: [
335
+ /* @__PURE__ */ jsxs3("a", { className: "topbar-brand", href: "/", children: [
336
+ /* @__PURE__ */ jsx3("span", { style: S.badgeSm, children: badgeText(brand) }),
337
+ brandDisplay(brand)
338
+ ] }),
339
+ /* @__PURE__ */ jsx3("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: workspaces.map((workspace) => /* @__PURE__ */ jsx3(TopBarLink, { active: workspace.id === route.workspaceId, onClick: () => navigate(workspace.id), children: workspace.label }, workspace.id)) }),
340
+ /* @__PURE__ */ jsx3("span", { className: "topbar-spacer" }),
341
+ /* @__PURE__ */ jsx3("div", { className: "topbar-actions", children: identity })
342
+ ] }),
343
+ /* @__PURE__ */ jsxs3("main", { className: "shell-main", style: S.main, children: [
344
+ /* @__PURE__ */ jsx3(ThemeWarning, {}),
345
+ active?.render(context)
346
+ ] })
347
+ ] });
348
+ }
349
+ const header = renderHeader ? renderHeader({ brand, email, signOut }) : chrome === "editorial" ? /* @__PURE__ */ jsxs3("header", { style: S.masthead, children: [
350
+ /* @__PURE__ */ jsxs3("a", { href: "/", style: S.mastheadBrand, children: [
351
+ /* @__PURE__ */ jsx3("span", { style: S.badgeSm, children: badgeText(brand) }),
352
+ brandDisplay(brand)
353
+ ] }),
354
+ /* @__PURE__ */ jsx3("span", { style: { opacity: 0.68 }, children: "Admin Console" }),
355
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1 } }),
356
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: identity })
357
+ ] }) : null;
358
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
359
+ header,
360
+ /* @__PURE__ */ jsxs3("main", { className: "shell-main", style: S.main, children: [
361
+ /* @__PURE__ */ jsx3(ThemeWarning, {}),
362
+ /* @__PURE__ */ jsx3(
363
+ Tabs,
364
+ {
365
+ style: S.workspaceTabs,
366
+ ariaLabel: "Admin workspaces",
367
+ value: route.workspaceId,
368
+ onValueChange: (workspaceId) => setRoute({ workspaceId }),
369
+ items: workspaces.map((workspace) => ({
370
+ value: workspace.id,
371
+ label: workspace.label,
372
+ href: href(workspace.id),
373
+ panel: workspace.render(context)
374
+ }))
375
+ }
376
+ )
377
+ ] })
378
+ ] });
379
+ }
380
+
381
+ // src/ui/admin-workspaces.tsx
382
+ import { Tabs as Tabs2 } from "@odla-ai/ui/components";
383
+
384
+ // src/ui/admin-availability.tsx
385
+ import { useState as useState4 } from "react";
386
+ import { Button, Checkbox, Field, Input } from "@odla-ai/ui/components";
387
+
388
+ // src/ui/admin-api.ts
389
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
390
+ async function adminFetch(getToken, path, init) {
391
+ const token = await getToken();
392
+ const headers = new Headers(init?.headers);
393
+ if (token) headers.set("authorization", `Bearer ${token}`);
394
+ if (init?.body) headers.set("content-type", "application/json");
395
+ const res = await fetch(path, { ...init, headers });
396
+ const body = await res.json().catch(() => ({}));
397
+ if (!res.ok) throw new Error(typeof body.error === "string" ? body.error : `request failed (${res.status})`);
398
+ return body;
399
+ }
400
+ function useAdminResource(getToken, path) {
401
+ const [data, setData] = useState3(null);
402
+ const [loading, setLoading] = useState3(true);
403
+ const [error, setError] = useState3(null);
404
+ const [nonce, setNonce] = useState3(0);
405
+ useEffect3(() => {
406
+ let live = true;
407
+ setLoading(true);
408
+ setError(null);
409
+ adminFetch(getToken, path).then((d) => live && setData(d)).catch((e) => live && setError(e instanceof Error ? e.message : String(e))).finally(() => live && setLoading(false));
410
+ return () => {
411
+ live = false;
412
+ };
413
+ }, [getToken, path, nonce]);
414
+ const refresh = useCallback2(() => setNonce((n) => n + 1), []);
415
+ return { data, loading, error, refresh };
416
+ }
417
+
418
+ // src/ui/admin-availability.tsx
419
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
420
+ var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
421
+ function AvailabilityBody({ ctx }) {
422
+ const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/scheduling");
423
+ const [draft, setDraft] = useState4(null);
424
+ const [busy, setBusy] = useState4(false);
425
+ const [note, setNote] = useState4(null);
426
+ const [errors, setErrors] = useState4({});
427
+ const cfg = draft ?? data?.scheduling ?? null;
428
+ if (loading) return /* @__PURE__ */ jsx4(AdminNote, { children: "Loading\u2026" });
429
+ if (error || !cfg) return /* @__PURE__ */ jsxs4(AdminNote, { children: [
430
+ "Couldn\u2019t load availability",
431
+ error ? `: ${error}` : "",
432
+ "."
433
+ ] });
434
+ const edit = (patch) => setDraft({ ...cfg, ...patch });
435
+ const toggleDay = (d) => edit({ days: cfg.days.includes(d) ? cfg.days.filter((x) => x !== d) : [...cfg.days, d].sort() });
436
+ const num = (v, fallback) => Number.isFinite(Number(v)) ? Number(v) : fallback;
437
+ const save = async () => {
438
+ setBusy(true);
439
+ setNote(null);
440
+ setErrors({});
441
+ const token = await ctx.getToken();
442
+ try {
443
+ const r = await fetch("/api/admin/scheduling", { method: "PUT", headers: { authorization: `Bearer ${token ?? ""}`, "content-type": "application/json" }, body: JSON.stringify(cfg) });
444
+ const body = await r.json().catch(() => ({}));
445
+ if (!r.ok) {
446
+ setErrors(body.errors ?? {});
447
+ setNote(body.error ?? `save failed (${r.status})`);
448
+ return;
449
+ }
450
+ setDraft(null);
451
+ refresh();
452
+ setNote("Saved.");
453
+ } catch (e) {
454
+ setNote(e instanceof Error ? e.message : String(e));
455
+ } finally {
456
+ setBusy(false);
457
+ }
458
+ };
459
+ const numField = (key, label) => /* @__PURE__ */ jsx4(Field, { label, error: errors[key], children: /* @__PURE__ */ jsx4(Input, { type: "number", value: cfg[key], onChange: (e) => edit({ [key]: num(e.target.value, cfg[key]) }) }) });
460
+ return /* @__PURE__ */ jsx4(AdminPage, { children: /* @__PURE__ */ jsxs4(Panel, { title: "Booking availability", actions: note ? /* @__PURE__ */ jsx4("span", { className: "muted", children: note }) : void 0, children: [
461
+ /* @__PURE__ */ jsx4(Field, { label: "Days", error: errors.days, children: /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 14, flexWrap: "wrap" }, children: DAYS.map((d, i) => /* @__PURE__ */ jsx4(Checkbox, { label: d, checked: cfg.days.includes(i), onChange: () => toggleDay(i) }, d)) }) }),
462
+ /* @__PURE__ */ jsxs4("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 14 }, children: [
463
+ numField("startHour", "Start hour (0\u201324)"),
464
+ numField("endHour", "End hour (0\u201324)"),
465
+ numField("slotMinutes", "Slot length (minutes)"),
466
+ numField("minNoticeHours", "Minimum notice (hours)"),
467
+ numField("windowDays", "Booking window (days)"),
468
+ /* @__PURE__ */ jsx4(Field, { label: "Timezone", error: errors.timezone, children: /* @__PURE__ */ jsx4(Input, { value: cfg.timezone, onChange: (e) => edit({ timezone: e.target.value }) }) })
469
+ ] }),
470
+ /* @__PURE__ */ jsx4(Field, { label: "Event summary template", error: errors.summaryTemplate, children: /* @__PURE__ */ jsx4(Input, { value: cfg.summaryTemplate, onChange: (e) => edit({ summaryTemplate: e.target.value }) }) }),
471
+ /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(Button, { disabled: !draft || busy, onClick: () => void save(), children: "Save availability" }) })
472
+ ] }) });
473
+ }
474
+ function availabilitySection(options = {}) {
475
+ const { id = "availability", label = "Availability" } = options;
476
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx4(AvailabilityBody, { ctx }) };
477
+ }
478
+
479
+ // src/ui/admin-billing.tsx
480
+ import { DataTable, StatBand } from "@odla-ai/ui/components";
481
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
482
+ var money = (cents) => `$${(cents / 100).toLocaleString()}`;
483
+ var date = (t) => t ? new Date(t).toLocaleDateString([], { dateStyle: "medium" }) : "\u2014";
484
+ var COLUMNS = [
485
+ { key: "name", header: "Name", cell: (r) => r.name, sortAs: "string" },
486
+ { key: "email", header: "Email", cell: (r) => r.email },
487
+ { key: "applicationStatus", header: "Application", cell: (r) => r.applicationStatus },
488
+ { key: "subscriptionStatus", header: "Subscription", cell: (r) => r.subscriptionStatus ? `${r.subscriptionStatus}${r.cancelAtPeriodEnd ? " \xB7 cancelling" : ""}` : "\u2014" },
489
+ { key: "amountCents", header: "Amount", cell: (r) => `${money(r.amountCents)}/${r.interval === "month" ? "mo" : "yr"}`, sortAs: "number", sortValue: (r) => r.amountCents },
490
+ { key: "renewalAt", header: "Renews", cell: (r) => date(r.renewalAt), sortAs: "number", sortValue: (r) => r.renewalAt ?? 0 }
491
+ ];
492
+ function BillingBody({ ctx }) {
493
+ const { data, loading, error } = useAdminResource(ctx.getToken, "/api/admin/billing");
494
+ if (loading) return /* @__PURE__ */ jsx5(AdminNote, { children: "Loading\u2026" });
495
+ if (error || !data) return /* @__PURE__ */ jsxs5(AdminNote, { children: [
496
+ "Couldn\u2019t load billing",
497
+ error ? `: ${error}` : "",
498
+ "."
499
+ ] });
500
+ if (!data.billingReady) return /* @__PURE__ */ jsx5(AdminNote, { children: "Billing isn\u2019t configured \u2014 no Stripe key is vaulted for this group." });
501
+ const s = data.summary;
502
+ const stats = s ? [
503
+ { value: s.activeCount, label: "Active" },
504
+ { value: money(s.annualizedCents), label: "Annualized" },
505
+ { value: s.renewingSoonCount, label: "Renewing \u226460d" },
506
+ { value: s.pastDueCount, label: "Past due" }
507
+ ] : [];
508
+ return /* @__PURE__ */ jsxs5(AdminPage, { children: [
509
+ s ? /* @__PURE__ */ jsx5(StatBand, { stats }) : null,
510
+ /* @__PURE__ */ jsxs5(Panel, { title: "Subscriptions", actions: data.testMode ? /* @__PURE__ */ jsx5("span", { className: "badge", children: "Stripe test mode" }) : void 0, children: [
511
+ data.truncated ? /* @__PURE__ */ jsx5("p", { className: "badge", children: "Showing the first 100 subscriptions \u2014 the list is truncated." }) : null,
512
+ /* @__PURE__ */ jsx5(DataTable, { rows: data.rows, columns: COLUMNS, rowKey: (r) => r.id })
513
+ ] })
514
+ ] });
515
+ }
516
+ function billingSection(options = {}) {
517
+ const { id = "billing", label = "Billing" } = options;
518
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx5(BillingBody, { ctx }) };
519
+ }
520
+
521
+ // src/ui/admin-dashboard.tsx
522
+ import { MetricWidget, StepPipeline } from "@odla-ai/ui/components";
523
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
524
+ var number = (value) => Math.round(value).toLocaleString();
525
+ var money2 = (cents) => `$${Math.round(cents / 100).toLocaleString()}`;
526
+ var when = (time) => new Date(time).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
527
+ function DashboardBody({ ctx }) {
528
+ const { data, loading, error } = useAdminResource(
529
+ ctx.getToken,
530
+ "/api/admin/dashboard"
531
+ );
532
+ if (loading) return /* @__PURE__ */ jsx6(AdminNote, { children: "Loading dashboard\u2026" });
533
+ if (error || !data) return /* @__PURE__ */ jsxs6(AdminNote, { children: [
534
+ "Couldn\u2019t load the dashboard",
535
+ error ? `: ${error}` : "",
536
+ "."
537
+ ] });
538
+ const pipelineSteps = Object.entries(data.pipeline).filter(([stage]) => !["declined", "refunded"].includes(stage)).map(([stage, count]) => ({
539
+ icon: /* @__PURE__ */ jsx6("span", { style: { fontSize: 22, fontWeight: 700 }, children: count }),
540
+ title: stage.replaceAll("_", " "),
541
+ description: data.pipelineDelta[stage] ? `+${data.pipelineDelta[stage]} this week` : "no change"
542
+ }));
543
+ const exceptions = ["declined", "refunded"].filter((stage) => stage in data.pipeline).map((stage) => `${stage} ${data.pipeline[stage]}`).join(" \xB7 ");
544
+ return /* @__PURE__ */ jsxs6(AdminPage, { children: [
545
+ /* @__PURE__ */ jsx6(
546
+ Panel,
547
+ {
548
+ title: /* @__PURE__ */ jsxs6(Fragment2, { children: [
549
+ "Upcoming calls",
550
+ " ",
551
+ /* @__PURE__ */ jsxs6("span", { className: "muted", style: { fontWeight: 400 }, children: [
552
+ "\xB7 ",
553
+ data.timezone
554
+ ] })
555
+ ] }),
556
+ actions: data.calls.needsAttention ? /* @__PURE__ */ jsxs6("span", { className: "badge", children: [
557
+ data.calls.needsAttention,
558
+ " need attention"
559
+ ] }) : void 0,
560
+ children: data.agenda.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "muted", style: { margin: 0 }, children: "No upcoming calls." }) : /* @__PURE__ */ jsx6("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 10 }, children: data.agenda.map((meeting) => /* @__PURE__ */ jsxs6("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
561
+ /* @__PURE__ */ jsx6("strong", { style: { minWidth: 170 }, children: when(meeting.startAt) }),
562
+ /* @__PURE__ */ jsxs6("span", { style: { flex: 1 }, children: [
563
+ meeting.name,
564
+ meeting.email ? ` \xB7 ${meeting.email}` : ""
565
+ ] }),
566
+ meeting.drift && meeting.drift !== "none" ? /* @__PURE__ */ jsx6("span", { className: "badge", children: "drift" }) : null,
567
+ meeting.htmlLink ? /* @__PURE__ */ jsx6("a", { href: meeting.htmlLink, target: "_blank", rel: "noreferrer", children: "Open" }) : null,
568
+ meeting.meetUrl ? /* @__PURE__ */ jsx6("a", { href: meeting.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null
569
+ ] }, meeting.id)) })
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxs6("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 18 }, children: [
573
+ /* @__PURE__ */ jsx6(MetricWidget, { label: "Applications", data: data.applicationsSeries, format: number }),
574
+ data.membersSeries ? /* @__PURE__ */ jsx6(MetricWidget, { label: "New members", data: data.membersSeries, format: number }) : /* @__PURE__ */ jsx6(Panel, { title: "New members", children: /* @__PURE__ */ jsx6("p", { className: "muted", children: "Appears once billing is connected." }) })
575
+ ] }),
576
+ data.revenueSeries && data.revenue.billingReady ? /* @__PURE__ */ jsxs6("div", { children: [
577
+ /* @__PURE__ */ jsx6(
578
+ MetricWidget,
579
+ {
580
+ label: "Revenue \xB7 annual run rate added",
581
+ data: data.revenueSeries,
582
+ format: money2,
583
+ size: "large"
584
+ }
585
+ ),
586
+ /* @__PURE__ */ jsxs6("p", { className: "muted", children: [
587
+ "Active memberships: ",
588
+ data.revenue.activeCount ?? 0,
589
+ " \xB7 Annual run rate:",
590
+ " ",
591
+ money2(data.revenue.annualRunRateCents ?? 0),
592
+ data.revenue.testMode ? " \xB7 test mode" : ""
593
+ ] })
594
+ ] }) : /* @__PURE__ */ jsx6(Panel, { title: "Revenue", children: /* @__PURE__ */ jsx6("p", { className: "muted", children: "Appears once billing is connected." }) }),
595
+ /* @__PURE__ */ jsxs6(Panel, { title: "Pipeline", children: [
596
+ /* @__PURE__ */ jsx6(StepPipeline, { ariaLabel: "Membership pipeline", steps: pipelineSteps }),
597
+ exceptions ? /* @__PURE__ */ jsx6("p", { className: "muted", style: { margin: 0 }, children: exceptions }) : null
598
+ ] })
599
+ ] });
600
+ }
601
+ function dashboardSection(options = {}) {
602
+ const { id = "dashboard", label = "Dashboard" } = options;
603
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx6(DashboardBody, { ctx }) };
604
+ }
605
+
606
+ // src/ui/admin-email.tsx
607
+ import { useState as useState5 } from "react";
608
+ import { Button as Button2, Checkbox as Checkbox2, DataTable as DataTable2, Field as Field2, Input as Input2, Textarea } from "@odla-ai/ui/components";
609
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
610
+ var LOG_COLUMNS = [
611
+ { key: "sentAt", header: "Sent", cell: (r) => new Date(r.sentAt).toLocaleString(), sortAs: "number", sortValue: (r) => r.sentAt },
612
+ { key: "template", header: "Template", cell: (r) => r.template },
613
+ { key: "to", header: "To", cell: (r) => r.to },
614
+ { key: "status", header: "Status", cell: (r) => r.error ? /* @__PURE__ */ jsx7("span", { className: "badge", children: "failed" }) : r.redirected ? /* @__PURE__ */ jsx7("span", { className: "badge", children: "redirected" }) : "delivered" }
615
+ ];
616
+ function EmailBody({ ctx }) {
617
+ const cfg = useAdminResource(ctx.getToken, "/api/admin/group/email");
618
+ const log = useAdminResource(ctx.getToken, "/api/admin/email/log");
619
+ const [draft, setDraft] = useState5(null);
620
+ const [busy, setBusy] = useState5(null);
621
+ const [note, setNote] = useState5(null);
622
+ const data = draft ?? cfg.data;
623
+ if (cfg.loading) return /* @__PURE__ */ jsx7(AdminNote, { children: "Loading\u2026" });
624
+ if (cfg.error || !data) return /* @__PURE__ */ jsxs7(AdminNote, { children: [
625
+ "Couldn\u2019t load email config",
626
+ cfg.error ? `: ${cfg.error}` : "",
627
+ "."
628
+ ] });
629
+ const edit = (patch) => setDraft({ ...data, ...patch });
630
+ const editTemplate = (key, patch) => edit({ emailTemplates: { ...data.emailTemplates, [key]: { ...data.emailTemplates[key], ...patch } } });
631
+ const run = async (key, fn) => {
632
+ setBusy(key);
633
+ setNote(null);
634
+ try {
635
+ await fn();
636
+ } catch (e) {
637
+ setNote(e instanceof Error ? e.message : String(e));
638
+ } finally {
639
+ setBusy(null);
640
+ }
641
+ };
642
+ const save = () => run("save", async () => {
643
+ await adminFetch(ctx.getToken, "/api/admin/group/email", { method: "PUT", body: JSON.stringify(data) });
644
+ setDraft(null);
645
+ cfg.refresh();
646
+ setNote("Saved.");
647
+ });
648
+ const test = (template) => run(`test:${template}`, async () => {
649
+ const r = await adminFetch(ctx.getToken, "/api/admin/email/test", { method: "POST", body: JSON.stringify({ template }) });
650
+ setNote(r.ok ? `Sent ${template} to ${r.to}${r.redirected ? " (dev-redirected)" : ""}.` : "Test send did not deliver.");
651
+ log.refresh();
652
+ });
653
+ return /* @__PURE__ */ jsxs7(AdminPage, { children: [
654
+ /* @__PURE__ */ jsxs7(
655
+ Panel,
656
+ {
657
+ title: "Delivery",
658
+ actions: /* @__PURE__ */ jsxs7("span", { className: "muted", children: [
659
+ data.envName,
660
+ " \xB7 ",
661
+ data.transport,
662
+ data.fromEmail ? ` \xB7 ${data.fromEmail}` : ""
663
+ ] }),
664
+ children: [
665
+ /* @__PURE__ */ jsx7(Field2, { label: "Notification address", children: /* @__PURE__ */ jsx7(Input2, { value: data.notificationEmail, onChange: (e) => edit({ notificationEmail: e.target.value }) }) }),
666
+ /* @__PURE__ */ jsx7(Field2, { label: "Reply-to", children: /* @__PURE__ */ jsx7(Input2, { value: data.replyTo, onChange: (e) => edit({ replyTo: e.target.value }) }) }),
667
+ /* @__PURE__ */ jsx7(Field2, { label: "Debug inbox", hint: "Non-prod sends redirect here.", children: /* @__PURE__ */ jsx7(Input2, { value: data.debugEmail, onChange: (e) => edit({ debugEmail: e.target.value }) }) })
668
+ ]
669
+ }
670
+ ),
671
+ Object.entries(data.emailTemplates).map(([key, t]) => /* @__PURE__ */ jsxs7(Panel, { title: key, actions: /* @__PURE__ */ jsx7(Button2, { variant: "secondary", mini: true, disabled: busy === `test:${key}`, onClick: () => void test(key), children: "Send test" }), children: [
672
+ /* @__PURE__ */ jsx7(Checkbox2, { label: "Enabled", checked: t.enabled, onChange: (e) => editTemplate(key, { enabled: e.target.checked }) }),
673
+ /* @__PURE__ */ jsx7(Field2, { label: "Subject", children: /* @__PURE__ */ jsx7(Input2, { value: t.subject, onChange: (e) => editTemplate(key, { subject: e.target.value }) }) }),
674
+ /* @__PURE__ */ jsx7(Field2, { label: "Body", children: /* @__PURE__ */ jsx7(Textarea, { prose: true, rows: 5, value: t.text, onChange: (e) => editTemplate(key, { text: e.target.value }) }) })
675
+ ] }, key)),
676
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
677
+ /* @__PURE__ */ jsx7(Button2, { disabled: !draft || busy === "save", onClick: () => void save(), children: "Save changes" }),
678
+ note ? /* @__PURE__ */ jsx7("span", { className: "muted", children: note }) : null
679
+ ] }),
680
+ /* @__PURE__ */ jsx7(Panel, { title: "Send log", children: /* @__PURE__ */ jsx7(DataTable2, { rows: log.data?.sends ?? [], columns: LOG_COLUMNS, rowKey: (r) => r.id }) })
681
+ ] });
682
+ }
683
+ function emailSection(options = {}) {
684
+ const { id = "email", label = "Email" } = options;
685
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx7(EmailBody, { ctx }) };
686
+ }
687
+
688
+ // src/ui/admin-people.tsx
689
+ import { BillingCard } from "@odla-ai/crm/ui";
690
+ import {
691
+ CrmWorkspace
692
+ } from "@odla-ai/crm/ui";
693
+
694
+ // src/ui/admin-record-actions.tsx
695
+ import { useEffect as useEffect4, useState as useState6 } from "react";
696
+ import { Button as Button3, Field as Field3, Select } from "@odla-ai/ui/components";
697
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
698
+ function RecordActions({ getToken, record, roles, onChanged }) {
699
+ const userId = typeof record.clerkUserId === "string" ? record.clerkUserId : null;
700
+ const applicationId = typeof record.fields?.applicationId === "string" ? record.fields.applicationId : null;
701
+ const [access, setAccess] = useState6(null);
702
+ const [busy, setBusy] = useState6(null);
703
+ const [note, setNote] = useState6(null);
704
+ useEffect4(() => {
705
+ if (!userId) {
706
+ setAccess(null);
707
+ return;
708
+ }
709
+ let live = true;
710
+ adminFetch(getToken, `/api/admin/people/access?userId=${encodeURIComponent(userId)}`).then((a) => live && setAccess(a)).catch(() => live && setAccess(null));
711
+ return () => {
712
+ live = false;
713
+ };
714
+ }, [getToken, userId]);
715
+ const act = async (key, run, ok) => {
716
+ setBusy(key);
717
+ setNote(null);
718
+ try {
719
+ await run();
720
+ setNote(ok);
721
+ onChanged();
722
+ } catch (e) {
723
+ setNote(e instanceof Error ? e.message : String(e));
724
+ } finally {
725
+ setBusy(null);
726
+ }
727
+ };
728
+ const setRole = (role) => act("role", () => adminFetch(getToken, "/api/admin/people/role", { method: "POST", body: JSON.stringify({ userId, role }) }), `Role set to ${role}.`);
729
+ const approve = () => act("approve", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/approve`, { method: "POST" }), "Approved.");
730
+ const refund = () => act("refund", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/refund`, { method: "POST" }), "Refund issued.");
731
+ if (!userId && !applicationId) return null;
732
+ return /* @__PURE__ */ jsxs8("section", { className: "panel", style: { display: "flex", flexDirection: "column", gap: 14, marginTop: 16 }, children: [
733
+ /* @__PURE__ */ jsx8("h3", { style: { margin: 0, fontSize: 16 }, children: "Access & lifecycle" }),
734
+ userId ? /* @__PURE__ */ jsx8(Field3, { label: "Role", hint: access?.superAdmin ? "Super-admin \u2014 managed in odla Studio." : void 0, children: /* @__PURE__ */ jsx8(
735
+ Select,
736
+ {
737
+ value: access?.role ?? "",
738
+ disabled: busy === "role" || access?.superAdmin,
739
+ options: roles.map((r) => ({ value: r, label: r })),
740
+ onChange: (e) => void setRole(e.target.value)
741
+ }
742
+ ) }) : /* @__PURE__ */ jsx8("p", { className: "muted", style: { margin: 0 }, children: "No linked account yet \u2014 role is set once they sign in." }),
743
+ applicationId ? /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 8 }, children: [
744
+ /* @__PURE__ */ jsx8(Button3, { variant: "secondary", mini: true, disabled: busy === "approve", onClick: () => void approve(), children: "Approve" }),
745
+ /* @__PURE__ */ jsx8(Button3, { variant: "danger", mini: true, disabled: busy === "refund", onClick: () => void refund(), children: "Refund" })
746
+ ] }) : null,
747
+ note ? /* @__PURE__ */ jsx8("p", { className: "muted", style: { margin: 0 }, children: note }) : null
748
+ ] });
749
+ }
750
+
751
+ // src/ui/admin-network.tsx
752
+ import { useState as useState7 } from "react";
753
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
754
+ function NetworkShareActions(props) {
755
+ const { recordId, recordType, getToken } = props;
756
+ const targets = useAdminResource(getToken, "/api/admin/network/targets");
757
+ const [busy, setBusy] = useState7(null);
758
+ const [message, setMessage] = useState7(null);
759
+ const compatible = (targets.data?.targets ?? []).filter((target) => target.types.includes(recordType));
760
+ if (targets.loading || targets.error || compatible.length === 0) return null;
761
+ const push = async (target) => {
762
+ setBusy(target.id);
763
+ setMessage(null);
764
+ try {
765
+ const result = await adminFetch(getToken, "/api/admin/network/push", {
766
+ method: "POST",
767
+ body: JSON.stringify({ recordId, targetIds: [target.id] })
768
+ });
769
+ const delivery = result.results[0];
770
+ setMessage(delivery?.ok ? `Shared with ${target.name}.` : delivery?.error ?? `Could not share with ${target.name}.`);
771
+ } catch (err) {
772
+ setMessage(err instanceof Error ? err.message : "Delivery failed.");
773
+ } finally {
774
+ setBusy(null);
775
+ }
776
+ };
777
+ return /* @__PURE__ */ jsxs9("section", { className: "panel", style: { marginTop: 14, display: "flex", flexDirection: "column", gap: 10 }, children: [
778
+ /* @__PURE__ */ jsxs9("div", { children: [
779
+ /* @__PURE__ */ jsx9("h3", { style: { margin: 0, fontSize: 16 }, children: "Share to a follower" }),
780
+ /* @__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." })
781
+ ] }),
782
+ /* @__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)) }),
783
+ message ? /* @__PURE__ */ jsx9("p", { role: "status", style: { margin: 0, fontSize: 13 }, children: message }) : null
784
+ ] });
785
+ }
786
+
787
+ // src/ui/admin-record-activity.tsx
788
+ import { useCallback as useCallback3, useEffect as useEffect5, useState as useState8 } from "react";
789
+ import { ActivityFeed } from "@odla-ai/crm/ui";
790
+ import { jsx as jsx10 } from "react/jsx-runtime";
791
+ function RecordActivityPanel(props) {
792
+ const { client, recordId } = props;
793
+ const [activities, setActivities] = useState8([]);
794
+ const [busy, setBusy] = useState8(false);
795
+ const [error, setError] = useState8(null);
796
+ const load = useCallback3(async () => {
797
+ try {
798
+ const result = await client.listActivities(recordId, { limit: 100 });
799
+ setActivities(result.activities);
800
+ setError(null);
801
+ } catch (failure) {
802
+ setError(failure instanceof Error ? failure.message : String(failure));
803
+ }
804
+ }, [client, recordId]);
805
+ useEffect5(() => void load(), [load]);
806
+ const run = async (action) => {
807
+ setBusy(true);
808
+ try {
809
+ await action();
810
+ await load();
811
+ } finally {
812
+ setBusy(false);
813
+ }
814
+ };
815
+ if (error) return /* @__PURE__ */ jsx10("p", { role: "alert", className: "muted", children: error });
816
+ return /* @__PURE__ */ jsx10(
817
+ ActivityFeed,
818
+ {
819
+ activities,
820
+ busy,
821
+ onAddNote: (body) => void run(() => client.addActivity(recordId, { kind: "note", body })),
822
+ onAddTask: (task) => void run(() => client.addActivity(recordId, { kind: "task", ...task })),
823
+ onToggleTask: (activity, done) => void run(() => client.updateTask(activity.id, { status: done ? "done" : "open" }))
824
+ }
825
+ );
826
+ }
827
+
828
+ // src/ui/admin-record-comms.tsx
829
+ import { useCallback as useCallback4, useEffect as useEffect6, useState as useState9 } from "react";
830
+ import { Button as Button4, Field as Field4, Select as Select2 } from "@odla-ai/ui/components";
831
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
832
+ function RecordCommsPanel(props) {
833
+ const { client, recordId, getToken } = props;
834
+ const config = useAdminResource(getToken, "/api/admin/group/email");
835
+ const templates = Object.entries(config.data?.emailTemplates ?? {}).filter(([, value]) => value.enabled);
836
+ const [template, setTemplate] = useState9("");
837
+ const [busy, setBusy] = useState9(false);
838
+ const [note, setNote] = useState9(null);
839
+ const send = async () => {
840
+ if (!template) return;
841
+ setBusy(true);
842
+ setNote(null);
843
+ try {
844
+ const result = await client.sendEmail(recordId, { templateId: template });
845
+ setNote(result.sent ? "Message sent." : result.reason ?? "Message was not sent.");
846
+ } catch (failure) {
847
+ setNote(failure instanceof Error ? failure.message : String(failure));
848
+ } finally {
849
+ setBusy(false);
850
+ }
851
+ };
852
+ if (config.loading) return /* @__PURE__ */ jsx11("p", { className: "muted", children: "Loading templates\u2026" });
853
+ if (config.error) return /* @__PURE__ */ jsx11("p", { role: "alert", className: "muted", children: config.error });
854
+ return /* @__PURE__ */ jsxs10("div", { style: { display: "grid", gap: 14, maxWidth: 560 }, children: [
855
+ /* @__PURE__ */ jsx11(Field4, { label: "Template", children: /* @__PURE__ */ jsx11(
856
+ Select2,
857
+ {
858
+ value: template,
859
+ onChange: (event) => setTemplate(event.currentTarget.value),
860
+ options: [
861
+ { value: "", label: "Choose a template\u2026" },
862
+ ...templates.map(([id, value]) => ({ value: id, label: value.subject || id }))
863
+ ]
864
+ }
865
+ ) }),
866
+ /* @__PURE__ */ jsx11("div", { children: /* @__PURE__ */ jsx11(Button4, { disabled: !template || busy, onClick: () => void send(), children: busy ? "Sending\u2026" : "Send" }) }),
867
+ note ? /* @__PURE__ */ jsx11("p", { role: "status", className: "muted", children: note }) : null
868
+ ] });
869
+ }
870
+ function RecordCommsHistory(props) {
871
+ const { client, recordId } = props;
872
+ const [rows, setRows] = useState9([]);
873
+ const [error, setError] = useState9(null);
874
+ const load = useCallback4(async () => {
875
+ try {
876
+ const result = await client.emailLog({ recordId, limit: 100 });
877
+ setRows(result.log);
878
+ setError(null);
879
+ } catch (failure) {
880
+ setError(failure instanceof Error ? failure.message : String(failure));
881
+ }
882
+ }, [client, recordId]);
883
+ useEffect6(() => void load(), [load]);
884
+ if (error) return /* @__PURE__ */ jsx11("p", { role: "alert", className: "muted", children: error });
885
+ if (rows.length === 0) return /* @__PURE__ */ jsx11("p", { className: "muted", children: "No messages recorded." });
886
+ return /* @__PURE__ */ jsx11("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 10 }, children: rows.map((row, index) => /* @__PURE__ */ jsxs10("li", { className: "panel", children: [
887
+ /* @__PURE__ */ jsx11("strong", { children: String(row["templateId"] ?? row["template"] ?? "Message") }),
888
+ /* @__PURE__ */ jsxs10("div", { className: "muted", children: [
889
+ row["to"] ? String(row["to"]) : "",
890
+ row["sentAt"] ? ` \xB7 ${new Date(Number(row["sentAt"])).toLocaleString()}` : ""
891
+ ] })
892
+ ] }, String(row["id"] ?? index))) });
893
+ }
894
+
895
+ // src/ui/admin-record-scheduling.tsx
896
+ import { useState as useState10 } from "react";
897
+ import { Button as Button5 } from "@odla-ai/ui/components";
898
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
899
+ function RecordSchedulingPanel(props) {
900
+ const { getToken, applicationId } = props;
901
+ const resource = useAdminResource(
902
+ getToken,
903
+ "/api/admin/meetings?all=1"
904
+ );
905
+ const [busy, setBusy] = useState10(null);
906
+ const [note, setNote] = useState10(null);
907
+ const meetings = (resource.data?.meetings ?? []).filter((meeting) => meeting.applicationId === applicationId);
908
+ const run = async (meeting, action) => {
909
+ let body;
910
+ if (action === "reschedule") {
911
+ const input = globalThis.prompt?.("New start time:");
912
+ if (!input) return;
913
+ const startAt = Date.parse(input);
914
+ if (!Number.isFinite(startAt)) {
915
+ setNote("Couldn\u2019t parse that time.");
916
+ return;
917
+ }
918
+ body = JSON.stringify({ startAt });
919
+ } else if (!globalThis.confirm?.("Cancel this call?")) {
920
+ return;
921
+ }
922
+ setBusy(meeting.id);
923
+ try {
924
+ await adminFetch(getToken, `/api/admin/meetings/${meeting.id}/${action}`, { method: "POST", body });
925
+ resource.refresh();
926
+ setNote(action === "cancel" ? "Call cancelled." : "Call rescheduled.");
927
+ } catch (failure) {
928
+ setNote(failure instanceof Error ? failure.message : String(failure));
929
+ } finally {
930
+ setBusy(null);
931
+ }
932
+ };
933
+ if (!applicationId) return /* @__PURE__ */ jsx12("p", { className: "muted", children: "No application is linked to this record." });
934
+ if (resource.loading) return /* @__PURE__ */ jsx12("p", { className: "muted", children: "Loading meetings\u2026" });
935
+ if (resource.error) return /* @__PURE__ */ jsx12("p", { role: "alert", className: "muted", children: resource.error });
936
+ if (meetings.length === 0) return /* @__PURE__ */ jsx12("p", { className: "muted", children: "No meetings recorded." });
937
+ return /* @__PURE__ */ jsxs11("div", { style: { display: "grid", gap: 12 }, children: [
938
+ meetings.map((meeting) => /* @__PURE__ */ jsxs11("section", { className: "panel", children: [
939
+ /* @__PURE__ */ jsx12("strong", { children: new Date(meeting.startAt).toLocaleString() }),
940
+ /* @__PURE__ */ jsxs11("p", { className: "muted", children: [
941
+ meeting.status,
942
+ " \xB7 ",
943
+ resource.data?.timezone
944
+ ] }),
945
+ /* @__PURE__ */ jsxs11("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [
946
+ meeting.meetUrl ? /* @__PURE__ */ jsx12("a", { href: meeting.meetUrl, target: "_blank", rel: "noreferrer", children: "Join meeting" }) : null,
947
+ meeting.htmlLink ? /* @__PURE__ */ jsx12("a", { href: meeting.htmlLink, target: "_blank", rel: "noreferrer", children: "Open calendar" }) : null,
948
+ meeting.status === "scheduled" ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
949
+ /* @__PURE__ */ jsx12(Button5, { variant: "secondary", mini: true, disabled: busy === meeting.id, onClick: () => void run(meeting, "reschedule"), children: "Reschedule" }),
950
+ /* @__PURE__ */ jsx12(Button5, { variant: "danger", mini: true, disabled: busy === meeting.id, onClick: () => void run(meeting, "cancel"), children: "Cancel" })
951
+ ] }) : null
952
+ ] })
953
+ ] }, meeting.id)),
954
+ note ? /* @__PURE__ */ jsx12("p", { role: "status", className: "muted", children: note }) : null
955
+ ] });
956
+ }
957
+
958
+ // src/ui/admin-people.tsx
959
+ import { jsx as jsx13 } from "react/jsx-runtime";
960
+ var DEFAULT_ROLES = ["provisional", "member", "admin"];
961
+ function PeopleBody(props) {
962
+ const { crm, client, type, ctx, roles, lifecycle, networkSharing, extendRecordTabs } = props;
963
+ const def = crm.type(type);
964
+ const recordId = ctx.route?.recordId ?? null;
965
+ const workspaceClient = client;
966
+ return /* @__PURE__ */ jsx13(AdminPage, { children: /* @__PURE__ */ jsx13(
967
+ CrmWorkspace,
968
+ {
969
+ crm,
970
+ client: workspaceClient,
971
+ type,
972
+ recordId,
973
+ detailTab: ctx.route?.detailTab,
974
+ defaultDetailTab: "profile",
975
+ onRecordIdChange: (id) => ctx.navigate({ recordId: id, detailTab: null }),
976
+ hrefForDetailTab: (detailTab) => ctx.href({ detailTab }),
977
+ extendRecordTabs: (base, context) => {
978
+ const record = context.detail.record;
979
+ const applicationId = typeof record.fields?.applicationId === "string" ? String(record.fields.applicationId) : null;
980
+ const byId = new Map(base.map((tab) => [tab.id, tab]));
981
+ const tabs = [];
982
+ const add = (id) => {
983
+ const tab = byId.get(id);
984
+ if (tab) tabs.push(tab);
985
+ };
986
+ add("stage");
987
+ add("profile");
988
+ if (def.facets?.email !== false) {
989
+ tabs.push({
990
+ id: "comms",
991
+ label: "Comms",
992
+ panel: /* @__PURE__ */ jsx13(RecordCommsPanel, { client, recordId: record.id, getToken: ctx.getToken })
993
+ });
994
+ tabs.push({
995
+ id: "history",
996
+ label: "Comms history",
997
+ panel: /* @__PURE__ */ jsx13(RecordCommsHistory, { client, recordId: record.id })
998
+ });
999
+ }
1000
+ if (lifecycle) {
1001
+ tabs.push({
1002
+ id: "scheduling",
1003
+ label: "Scheduling",
1004
+ panel: /* @__PURE__ */ jsx13(RecordSchedulingPanel, { getToken: ctx.getToken, applicationId })
1005
+ });
1006
+ tabs.push({ id: "billing", label: "Billing", panel: /* @__PURE__ */ jsx13(BillingCard, { record }) });
1007
+ } else {
1008
+ add("billing");
1009
+ }
1010
+ tabs.push({
1011
+ id: "notes",
1012
+ label: "Notes",
1013
+ panel: /* @__PURE__ */ jsx13(RecordActivityPanel, { client, recordId: record.id })
1014
+ });
1015
+ add("connections");
1016
+ if (lifecycle) {
1017
+ tabs.push({
1018
+ id: "access",
1019
+ label: "Access",
1020
+ panel: /* @__PURE__ */ jsx13(
1021
+ RecordActions,
1022
+ {
1023
+ getToken: ctx.getToken,
1024
+ record,
1025
+ roles,
1026
+ onChanged: () => {
1027
+ context.refreshRecord();
1028
+ context.refreshList();
1029
+ }
1030
+ }
1031
+ )
1032
+ });
1033
+ }
1034
+ if (networkSharing) {
1035
+ tabs.push({
1036
+ id: "sharing",
1037
+ label: "Sharing",
1038
+ panel: /* @__PURE__ */ jsx13(NetworkShareActions, { recordId: record.id, recordType: record.type, getToken: ctx.getToken })
1039
+ });
1040
+ }
1041
+ return extendRecordTabs?.(tabs, context) ?? tabs;
1042
+ }
1043
+ }
1044
+ ) });
1045
+ }
1046
+ function collectionSection(options) {
1047
+ const { crm, type } = options;
1048
+ let fallbackLabel = type;
1049
+ try {
1050
+ fallbackLabel = crm.type(type).labelPlural ?? crm.type(type).label ?? type;
1051
+ } catch {
1052
+ }
1053
+ const {
1054
+ id = type,
1055
+ label = fallbackLabel,
1056
+ lifecycle = type === "person",
1057
+ roles = DEFAULT_ROLES,
1058
+ networkSharing = false,
1059
+ extendRecordTabs
1060
+ } = options;
1061
+ return {
1062
+ id,
1063
+ label,
1064
+ render: (ctx) => /* @__PURE__ */ jsx13(
1065
+ PeopleBody,
1066
+ {
1067
+ crm,
1068
+ client: ctx.client,
1069
+ type,
1070
+ ctx,
1071
+ roles,
1072
+ lifecycle,
1073
+ networkSharing,
1074
+ extendRecordTabs
1075
+ }
1076
+ )
1077
+ };
1078
+ }
1079
+ function peopleSection(options) {
1080
+ const {
1081
+ crm,
1082
+ id = "people",
1083
+ label = "People",
1084
+ type = "person",
1085
+ roles,
1086
+ networkSharing,
1087
+ extendRecordTabs
1088
+ } = options;
1089
+ return collectionSection({
1090
+ crm,
1091
+ type,
1092
+ id,
1093
+ label,
1094
+ ...roles ? { roles } : {},
1095
+ ...networkSharing != null ? { networkSharing } : {},
1096
+ ...extendRecordTabs ? { extendRecordTabs } : {}
1097
+ });
1098
+ }
1099
+
1100
+ // src/ui/admin-workspaces.tsx
1101
+ import { jsx as jsx14 } from "react/jsx-runtime";
1102
+ var adminViewContext = (ctx, viewId, active) => {
1103
+ const route = {
1104
+ workspaceId: ctx.route.workspaceId,
1105
+ viewId,
1106
+ ...active && ctx.route.recordId ? { recordId: ctx.route.recordId } : {},
1107
+ ...active && ctx.route.detailTab ? { detailTab: ctx.route.detailTab } : {}
1108
+ };
1109
+ const targetFromView = (target) => {
1110
+ if (typeof target === "string") return target;
1111
+ if (target.workspaceId && target.workspaceId !== route.workspaceId) return target;
1112
+ if (target.viewId && target.viewId !== route.viewId) return target;
1113
+ return { ...route, ...target };
1114
+ };
1115
+ return {
1116
+ ...ctx,
1117
+ route,
1118
+ navigate: (target) => ctx.navigate(targetFromView(target)),
1119
+ href: (target) => ctx.href(targetFromView(target))
1120
+ };
1121
+ };
1122
+ function DashboardWorkspace({ ctx }) {
1123
+ const active = ctx.route.viewId === "billing" ? "billing" : "overview";
1124
+ const overview = dashboardSection({ id: "overview", label: "Overview" });
1125
+ const billing = billingSection();
1126
+ return /* @__PURE__ */ jsx14(
1127
+ Tabs2,
1128
+ {
1129
+ ariaLabel: "Dashboard views",
1130
+ value: active,
1131
+ items: [
1132
+ {
1133
+ value: "overview",
1134
+ label: "Overview",
1135
+ href: ctx.href({ viewId: "overview", recordId: null, detailTab: null }),
1136
+ panel: overview.render(adminViewContext(ctx, "overview", active === "overview"))
1137
+ },
1138
+ {
1139
+ value: "billing",
1140
+ label: "Billing",
1141
+ href: ctx.href({ viewId: "billing", recordId: null, detailTab: null }),
1142
+ panel: billing.render(adminViewContext(ctx, "billing", active === "billing"))
1143
+ }
1144
+ ]
1145
+ }
1146
+ );
1147
+ }
1148
+ function SettingsWorkspace({ ctx }) {
1149
+ const active = ctx.route.viewId === "email" ? "email" : "calendar";
1150
+ const calendar = availabilitySection({ id: "calendar", label: "Calendar" });
1151
+ const email = emailSection();
1152
+ return /* @__PURE__ */ jsx14(
1153
+ Tabs2,
1154
+ {
1155
+ ariaLabel: "Settings views",
1156
+ value: active,
1157
+ items: [
1158
+ {
1159
+ value: "calendar",
1160
+ label: "Calendar",
1161
+ href: ctx.href({ viewId: "calendar", recordId: null, detailTab: null }),
1162
+ panel: calendar.render(adminViewContext(ctx, "calendar", active === "calendar"))
1163
+ },
1164
+ {
1165
+ value: "email",
1166
+ label: "Email",
1167
+ href: ctx.href({ viewId: "email", recordId: null, detailTab: null }),
1168
+ panel: email.render(adminViewContext(ctx, "email", active === "email"))
1169
+ }
1170
+ ]
1171
+ }
1172
+ );
1173
+ }
1174
+ function PeopleWorkspace({ chapter, ctx }) {
1175
+ const types = Object.entries(chapter.crm.config.types);
1176
+ const fallback = types[0]?.[0] ?? "person";
1177
+ const active = types.some(([type]) => type === ctx.route.viewId) ? ctx.route.viewId : fallback;
1178
+ const networkSharing = chapter.network.targets.length > 0;
1179
+ return /* @__PURE__ */ jsx14(
1180
+ Tabs2,
1181
+ {
1182
+ ariaLabel: "CRM collections",
1183
+ value: active,
1184
+ variant: "pill",
1185
+ items: types.map(([type, def]) => {
1186
+ const section = collectionSection({
1187
+ crm: chapter.crm,
1188
+ type,
1189
+ lifecycle: chapter.mode === "chapter" && type === "person",
1190
+ roles: chapter.auth.ladder,
1191
+ networkSharing
1192
+ });
1193
+ return {
1194
+ value: type,
1195
+ label: def.labelPlural ?? def.label,
1196
+ href: ctx.href({ viewId: type, recordId: null, detailTab: null }),
1197
+ panel: section.render(adminViewContext(ctx, type, active === type))
1198
+ };
1199
+ })
1200
+ }
1201
+ );
1202
+ }
1203
+ function defaultAdminWorkspaces(chapter) {
1204
+ const people = {
1205
+ id: "people",
1206
+ label: "People",
1207
+ render: (ctx) => /* @__PURE__ */ jsx14(PeopleWorkspace, { chapter, ctx })
1208
+ };
1209
+ if (chapter.mode === "hub") return [people];
1210
+ return [
1211
+ { id: "dashboard", label: "Dashboard", render: (ctx) => /* @__PURE__ */ jsx14(DashboardWorkspace, { ctx }) },
1212
+ people,
1213
+ { id: "settings", label: "Settings", render: (ctx) => /* @__PURE__ */ jsx14(SettingsWorkspace, { ctx }) }
1214
+ ];
1215
+ }
1216
+
1217
+ // src/ui/admin.tsx
1218
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1219
+ function AdminTheme(props) {
1220
+ const brand = props.chapter?.brand;
1221
+ return /* @__PURE__ */ jsxs12(
1222
+ ThemeScope,
1223
+ {
1224
+ "data-chapter-admin": "",
1225
+ theme: brand?.theme,
1226
+ colorScheme: brand?.colorScheme ?? "light",
1227
+ children: [
1228
+ /* @__PURE__ */ jsx15(BrandStyle, { brand, selector: "[data-chapter-admin]" }),
1229
+ props.children
1230
+ ]
1231
+ }
1232
+ );
1233
+ }
1234
+ function Authed(props) {
1235
+ const {
1236
+ workspaces,
1237
+ basePath,
1238
+ brand,
1239
+ crmBasePath,
1240
+ apiBase,
1241
+ routing,
1242
+ chrome,
1243
+ renderHeader
1244
+ } = props;
1245
+ const { getToken, signOut } = useClerkAuth();
1246
+ const client = useMemo(
1247
+ () => new CrmClient({
1248
+ basePath: crmBasePath,
1249
+ headers: async () => {
1250
+ const token = await getToken();
1251
+ return token ? { authorization: `Bearer ${token}` } : {};
1252
+ }
1253
+ }),
1254
+ [getToken, crmBasePath]
1255
+ );
1256
+ const [state, setState] = useState11({ status: "checking" });
1257
+ useEffect7(() => {
1258
+ let live = true;
1259
+ void (async () => {
1260
+ try {
1261
+ const token = await getToken();
1262
+ const response = await fetch(`${apiBase}/api/me`, {
1263
+ headers: token ? { authorization: `Bearer ${token}` } : {}
1264
+ });
1265
+ const body = await response.json().catch(() => ({}));
1266
+ if (live) setState({
1267
+ status: body.authorized ? "ok" : "denied",
1268
+ email: body.email ?? null
1269
+ });
1270
+ } catch {
1271
+ if (live) setState({ status: "denied" });
1272
+ }
1273
+ })();
1274
+ return () => {
1275
+ live = false;
1276
+ };
1277
+ }, [getToken, apiBase]);
1278
+ if (state.status === "checking") return /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "Checking access\u2026" }) });
1279
+ if (state.status === "denied") {
1280
+ return /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsxs12("div", { className: "card", style: S.card, children: [
1281
+ /* @__PURE__ */ jsx15("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
1282
+ /* @__PURE__ */ jsxs12("p", { style: S.muted, children: [
1283
+ state.email ? `${state.email} isn't` : "This account isn't",
1284
+ " on the admin list."
1285
+ ] }),
1286
+ /* @__PURE__ */ jsx15("button", { className: "btn secondary", onClick: () => signOut(), children: "Sign out" })
1287
+ ] }) });
1288
+ }
1289
+ return /* @__PURE__ */ jsx15(
1290
+ AdminShell,
1291
+ {
1292
+ workspaces,
1293
+ basePath,
1294
+ brand,
1295
+ client,
1296
+ getToken,
1297
+ signOut,
1298
+ email: state.email ?? null,
1299
+ routing,
1300
+ chrome,
1301
+ renderHeader
1302
+ }
1303
+ );
1304
+ }
1305
+ var signInRedirect = (basePath, workspaces) => {
1306
+ if (typeof window === "undefined") return basePath;
1307
+ const current = new URL(window.location.href);
1308
+ const route = adminRouteFromUrl(current, basePath, workspaces.map((workspace) => workspace.id));
1309
+ return adminRouteHref(current, basePath, route, "query");
1310
+ };
1311
+ function ChapterAdmin(props) {
1312
+ const defaults = useMemo(
1313
+ () => props.chapter ? defaultAdminWorkspaces(props.chapter) : [],
1314
+ [props.chapter]
1315
+ );
1316
+ const workspaces = useMemo(() => {
1317
+ if (typeof props.workspaces === "function") return props.workspaces(defaults);
1318
+ return props.workspaces ?? props.sections ?? defaults;
1319
+ }, [props.workspaces, props.sections, defaults]);
1320
+ const basePath = props.basePath ?? "/admin";
1321
+ const brand = props.brand ?? {
1322
+ name: props.chapter?.name ?? "Admin",
1323
+ badge: props.chapter?.brand.badge,
1324
+ wordmark: props.chapter?.brand.wordmark
1325
+ };
1326
+ const crmBasePath = props.crmBasePath ?? "/api/crm";
1327
+ const apiBase = props.apiBase ?? "";
1328
+ const routing = props.routing ?? "fragment";
1329
+ const chrome = props.chrome ?? "editorial";
1330
+ const redirect = signInRedirect(basePath, workspaces);
1331
+ const [publishableKey, setPublishableKey] = useState11(void 0);
1332
+ useEffect7(() => {
1333
+ let live = true;
1334
+ void fetch(`${apiBase}/api/config`).then((response) => response.json()).then((body) => live && setPublishableKey(body.clerkPublishableKey ?? null)).catch(() => live && setPublishableKey(null));
1335
+ return () => {
1336
+ live = false;
1337
+ };
1338
+ }, [apiBase]);
1339
+ let content;
1340
+ if (publishableKey === void 0) {
1341
+ content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "Loading\u2026" }) });
1342
+ } else if (!publishableKey) {
1343
+ content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsxs12("div", { className: "card", style: S.card, children: [
1344
+ /* @__PURE__ */ jsx15("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
1345
+ /* @__PURE__ */ jsx15("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
1346
+ ] }) });
1347
+ } else if (workspaces.length === 0) {
1348
+ content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "No admin workspaces are configured." }) });
1349
+ } else {
1350
+ content = /* @__PURE__ */ jsxs12(
1351
+ ClerkGate,
1352
+ {
1353
+ publishableKey,
1354
+ appearance: clerkAppearanceFromTokens(),
1355
+ afterSignOutUrl: "/",
1356
+ children: [
1357
+ /* @__PURE__ */ jsx15(SignedOut, { children: /* @__PURE__ */ jsx15(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx15(
1358
+ SignIn,
1359
+ {
1360
+ routing: "hash",
1361
+ forceRedirectUrl: redirect,
1362
+ signUpForceRedirectUrl: redirect
1363
+ }
1364
+ ) }) }),
1365
+ /* @__PURE__ */ jsx15(SignedIn, { children: /* @__PURE__ */ jsx15(
1366
+ Authed,
1367
+ {
1368
+ workspaces,
1369
+ basePath,
1370
+ brand,
1371
+ crmBasePath,
1372
+ apiBase,
1373
+ routing,
1374
+ chrome,
1375
+ renderHeader: props.renderHeader
1376
+ }
1377
+ ) })
1378
+ ]
1379
+ }
1380
+ );
1381
+ }
1382
+ return /* @__PURE__ */ jsx15(AdminTheme, { chapter: props.chapter, children: content });
1383
+ }
1384
+
1385
+ // src/ui/admin-defaults.ts
1386
+ function defaultAdminSections(chapter) {
1387
+ return defaultAdminWorkspaces(chapter);
1388
+ }
1389
+
1390
+ // src/ui/admin-meetings.tsx
1391
+ import { useState as useState12 } from "react";
1392
+ import { Button as Button6 } from "@odla-ai/ui/components";
1393
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1394
+ var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
1395
+ function MeetingsBody({ ctx }) {
1396
+ const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
1397
+ const [busy, setBusy] = useState12(null);
1398
+ const [note, setNote] = useState12(null);
1399
+ if (loading) return /* @__PURE__ */ jsx16(AdminNote, { children: "Loading\u2026" });
1400
+ if (error || !data) return /* @__PURE__ */ jsxs13(AdminNote, { children: [
1401
+ "Couldn\u2019t load meetings",
1402
+ error ? `: ${error}` : "",
1403
+ "."
1404
+ ] });
1405
+ const run = async (id, fn) => {
1406
+ setBusy(id);
1407
+ setNote(null);
1408
+ try {
1409
+ await fn();
1410
+ refresh();
1411
+ } catch (e) {
1412
+ setNote(e instanceof Error ? e.message : String(e));
1413
+ } finally {
1414
+ setBusy(null);
1415
+ }
1416
+ };
1417
+ const cancel = (id) => {
1418
+ if (!globalThis.confirm?.("Cancel this call? Google notifies the attendee.")) return;
1419
+ void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/cancel`, { method: "POST" }));
1420
+ };
1421
+ const reschedule = (id) => {
1422
+ const input = globalThis.prompt?.("New start time (e.g. 2026-08-01 14:00):");
1423
+ if (!input) return;
1424
+ const startAt = Date.parse(input);
1425
+ if (!Number.isFinite(startAt)) {
1426
+ setNote("Couldn\u2019t parse that time.");
1427
+ return;
1428
+ }
1429
+ void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
1430
+ };
1431
+ return /* @__PURE__ */ jsx16(AdminPage, { children: /* @__PURE__ */ jsx16(Panel, { title: /* @__PURE__ */ jsxs13(Fragment4, { children: [
1432
+ "Agenda ",
1433
+ /* @__PURE__ */ jsxs13("span", { className: "muted", style: { fontWeight: 400 }, children: [
1434
+ "\xB7 ",
1435
+ data.timezone
1436
+ ] })
1437
+ ] }), actions: note ? /* @__PURE__ */ jsx16("span", { className: "muted", children: note }) : void 0, children: data.meetings.length === 0 ? /* @__PURE__ */ jsx16("p", { className: "muted", style: { margin: 0 }, children: "No meetings." }) : /* @__PURE__ */ jsx16("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 12 }, children: data.meetings.map((m) => /* @__PURE__ */ jsxs13("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", opacity: m.status === "cancelled" ? 0.55 : 1 }, children: [
1438
+ /* @__PURE__ */ jsx16("strong", { style: { minWidth: 170 }, children: when2(m.startAt) }),
1439
+ /* @__PURE__ */ jsx16("span", { style: { flex: 1 }, children: m.applicant ? `${m.applicant.name} \xB7 ${m.applicant.email}` : "(unknown)" }),
1440
+ /* @__PURE__ */ jsx16("span", { className: "badge", children: m.status }),
1441
+ m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs13("span", { className: "badge", children: [
1442
+ "drift: ",
1443
+ m.drift
1444
+ ] }) : null,
1445
+ m.meetUrl ? /* @__PURE__ */ jsx16("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null,
1446
+ m.status === "scheduled" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
1447
+ /* @__PURE__ */ jsx16(Button6, { variant: "secondary", mini: true, disabled: busy === m.id, onClick: () => reschedule(m.id), children: "Reschedule" }),
1448
+ /* @__PURE__ */ jsx16(Button6, { variant: "danger", mini: true, disabled: busy === m.id, onClick: () => cancel(m.id), children: "Cancel" })
1449
+ ] }) : null
1450
+ ] }, m.id)) }) }) });
1451
+ }
1452
+ function meetingsSection(options = {}) {
1453
+ const { id = "meetings", label = "Meetings" } = options;
1454
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx16(MeetingsBody, { ctx }) };
1455
+ }
1456
+
1457
+ export {
1458
+ AdminPage,
1459
+ Panel,
1460
+ ThemeWarning,
1461
+ AdminNote,
1462
+ adminRouteFromUrl,
1463
+ adminRouteHref,
1464
+ adminSectionFromUrl,
1465
+ adminSectionHref,
1466
+ adminFetch,
1467
+ useAdminResource,
1468
+ availabilitySection,
1469
+ billingSection,
1470
+ dashboardSection,
1471
+ emailSection,
1472
+ RecordActions,
1473
+ NetworkShareActions,
1474
+ collectionSection,
1475
+ peopleSection,
1476
+ defaultAdminWorkspaces,
1477
+ ChapterAdmin,
1478
+ defaultAdminSections,
1479
+ meetingsSection
1480
+ };
1481
+ //# sourceMappingURL=chunk-AOTWFOH4.js.map