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