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