@odla-ai/chapter 0.19.0 → 0.20.1
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 +204 -21
- package/dist/{chunk-GWY2ZTN7.js → chunk-LME2IHY5.js} +2 -50
- package/dist/chunk-LME2IHY5.js.map +1 -0
- package/dist/chunk-MKXHZMAP.js +51 -0
- package/dist/chunk-MKXHZMAP.js.map +1 -0
- package/dist/{chunk-D3QMK3ED.js → chunk-UIWFEESI.js} +611 -470
- package/dist/chunk-UIWFEESI.js.map +1 -0
- package/dist/index.cjs +176 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -9
- package/dist/index.d.ts +83 -9
- package/dist/index.js +176 -11
- package/dist/index.js.map +1 -1
- package/dist/types-D2fGiNG8.d.ts +366 -0
- package/dist/ui/admin/index.d.ts +37 -3
- package/dist/ui/admin/index.js +10 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +13 -3
- package/dist/ui/member/index.d.ts +2 -25
- package/dist/ui/member/index.js +4 -2
- package/dist/worker/index.cjs +225 -24
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +45 -0
- package/dist/worker/index.d.ts +45 -0
- package/dist/worker/index.js +225 -24
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-D3QMK3ED.js.map +0 -1
- package/dist/chunk-GWY2ZTN7.js.map +0 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrandStyle
|
|
3
|
+
} from "./chunk-MKXHZMAP.js";
|
|
4
|
+
|
|
1
5
|
// src/ui/admin.tsx
|
|
2
|
-
import { useEffect as
|
|
6
|
+
import { useEffect as useEffect5, useMemo, useState as useState10 } from "react";
|
|
3
7
|
import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
|
|
4
8
|
import { CrmClient } from "@odla-ai/crm";
|
|
5
9
|
|
|
@@ -68,7 +72,38 @@ function AdminNote({ children }) {
|
|
|
68
72
|
// src/ui/chrome.tsx
|
|
69
73
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
70
74
|
var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
|
|
71
|
-
var brandDisplay = (brand) =>
|
|
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
|
+
}
|
|
72
107
|
var S = {
|
|
73
108
|
gate: {
|
|
74
109
|
minHeight: "100vh",
|
|
@@ -134,21 +169,18 @@ function Gate(props) {
|
|
|
134
169
|
] }) });
|
|
135
170
|
}
|
|
136
171
|
function AdminShell(props) {
|
|
137
|
-
const { sections, basePath, brand, client, getToken, signOut, email } = props;
|
|
172
|
+
const { sections, basePath, brand, client, getToken, signOut, email, routing = "query" } = props;
|
|
138
173
|
const sectionFromPath = useCallback(() => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const rest = window.location.pathname.slice(basePath.length).replace(/^\//, "");
|
|
142
|
-
const id = rest.split("/")[0] ?? "";
|
|
143
|
-
return sections.some((s) => s.id === id) ? id : fallback;
|
|
174
|
+
if (typeof window === "undefined") return sections[0]?.id ?? "";
|
|
175
|
+
return adminSectionFromUrl(new URL(window.location.href), basePath, sections.map((section2) => section2.id));
|
|
144
176
|
}, [sections, basePath]);
|
|
145
177
|
const [section, setSection] = useState2(sectionFromPath);
|
|
146
178
|
const go = useCallback(
|
|
147
179
|
(id) => {
|
|
148
|
-
window.history.pushState(null, "",
|
|
180
|
+
window.history.pushState(null, "", adminSectionHref(new URL(window.location.href), basePath, id, routing));
|
|
149
181
|
setSection(id);
|
|
150
182
|
},
|
|
151
|
-
[basePath]
|
|
183
|
+
[basePath, routing]
|
|
152
184
|
);
|
|
153
185
|
useEffect2(() => {
|
|
154
186
|
const onPop = () => setSection(sectionFromPath());
|
|
@@ -179,113 +211,12 @@ function AdminShell(props) {
|
|
|
179
211
|
] });
|
|
180
212
|
}
|
|
181
213
|
|
|
182
|
-
// src/ui/admin.tsx
|
|
183
|
-
import {
|
|
184
|
-
|
|
185
|
-
const { sections, basePath, brand, crmBasePath, apiBase } = props;
|
|
186
|
-
const { getToken, signOut } = useClerkAuth();
|
|
187
|
-
const client = useMemo(
|
|
188
|
-
() => new CrmClient({
|
|
189
|
-
basePath: crmBasePath,
|
|
190
|
-
headers: async () => {
|
|
191
|
-
const t = await getToken();
|
|
192
|
-
return t ? { authorization: `Bearer ${t}` } : {};
|
|
193
|
-
}
|
|
194
|
-
}),
|
|
195
|
-
[getToken, crmBasePath]
|
|
196
|
-
);
|
|
197
|
-
const [state, setState] = useState3({
|
|
198
|
-
status: "checking"
|
|
199
|
-
});
|
|
200
|
-
useEffect3(() => {
|
|
201
|
-
let live = true;
|
|
202
|
-
void (async () => {
|
|
203
|
-
try {
|
|
204
|
-
const t = await getToken();
|
|
205
|
-
const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
|
|
206
|
-
const body = await res.json().catch(() => ({}));
|
|
207
|
-
if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
|
|
208
|
-
} catch {
|
|
209
|
-
if (live) setState({ status: "denied" });
|
|
210
|
-
}
|
|
211
|
-
})();
|
|
212
|
-
return () => {
|
|
213
|
-
live = false;
|
|
214
|
-
};
|
|
215
|
-
}, [getToken, apiBase]);
|
|
216
|
-
if (state.status === "checking") {
|
|
217
|
-
return /* @__PURE__ */ jsx3(Gate, { brand, children: /* @__PURE__ */ jsx3("p", { style: S.muted, children: "Checking access\u2026" }) });
|
|
218
|
-
}
|
|
219
|
-
if (state.status === "denied") {
|
|
220
|
-
return /* @__PURE__ */ jsx3(Gate, { brand, children: /* @__PURE__ */ jsxs3("div", { className: "card", style: S.card, children: [
|
|
221
|
-
/* @__PURE__ */ jsx3("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
|
|
222
|
-
/* @__PURE__ */ jsxs3("p", { style: S.muted, children: [
|
|
223
|
-
state.email ? `${state.email} isn't` : "This account isn't",
|
|
224
|
-
" on the admin list. Ask an existing admin to add you in odla Studio."
|
|
225
|
-
] }),
|
|
226
|
-
/* @__PURE__ */ jsx3("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
|
|
227
|
-
] }) });
|
|
228
|
-
}
|
|
229
|
-
return /* @__PURE__ */ jsx3(
|
|
230
|
-
AdminShell,
|
|
231
|
-
{
|
|
232
|
-
sections,
|
|
233
|
-
basePath,
|
|
234
|
-
brand,
|
|
235
|
-
client,
|
|
236
|
-
getToken,
|
|
237
|
-
signOut,
|
|
238
|
-
email: state.email ?? null
|
|
239
|
-
}
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
function ChapterAdmin(props) {
|
|
243
|
-
const sections = props.sections;
|
|
244
|
-
const basePath = props.basePath ?? "/admin";
|
|
245
|
-
const brand = props.brand ?? { name: "Admin" };
|
|
246
|
-
const crmBasePath = props.crmBasePath ?? "/api/crm";
|
|
247
|
-
const apiBase = props.apiBase ?? "";
|
|
248
|
-
const [pk, setPk] = useState3(void 0);
|
|
249
|
-
useEffect3(() => {
|
|
250
|
-
let live = true;
|
|
251
|
-
void (async () => {
|
|
252
|
-
try {
|
|
253
|
-
const res = await fetch(`${apiBase}/api/config`);
|
|
254
|
-
const body = await res.json();
|
|
255
|
-
if (live) setPk(body.clerkPublishableKey ?? null);
|
|
256
|
-
} catch {
|
|
257
|
-
if (live) setPk(null);
|
|
258
|
-
}
|
|
259
|
-
})();
|
|
260
|
-
return () => {
|
|
261
|
-
live = false;
|
|
262
|
-
};
|
|
263
|
-
}, [apiBase]);
|
|
264
|
-
if (pk === void 0) {
|
|
265
|
-
return /* @__PURE__ */ jsx3(Gate, { brand, children: /* @__PURE__ */ jsx3("p", { style: S.muted, children: "Loading\u2026" }) });
|
|
266
|
-
}
|
|
267
|
-
if (!pk) {
|
|
268
|
-
return /* @__PURE__ */ jsx3(Gate, { brand, children: /* @__PURE__ */ jsxs3("div", { className: "card", style: S.card, children: [
|
|
269
|
-
/* @__PURE__ */ jsx3("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
|
|
270
|
-
/* @__PURE__ */ jsx3("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
|
|
271
|
-
] }) });
|
|
272
|
-
}
|
|
273
|
-
return /* @__PURE__ */ jsxs3(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
|
|
274
|
-
/* @__PURE__ */ jsx3(SignedOut, { children: /* @__PURE__ */ jsx3(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx3(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
|
|
275
|
-
/* @__PURE__ */ jsx3(SignedIn, { children: /* @__PURE__ */ jsx3(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
|
|
276
|
-
] });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// src/ui/admin-people.tsx
|
|
280
|
-
import { useState as useState6 } from "react";
|
|
281
|
-
import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
|
|
282
|
-
|
|
283
|
-
// src/ui/admin-record-actions.tsx
|
|
284
|
-
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
285
|
-
import { Button, Field, Select } from "@odla-ai/ui/components";
|
|
214
|
+
// src/ui/admin-availability.tsx
|
|
215
|
+
import { useState as useState4 } from "react";
|
|
216
|
+
import { Button, Checkbox, Field, Input } from "@odla-ai/ui/components";
|
|
286
217
|
|
|
287
218
|
// src/ui/admin-api.ts
|
|
288
|
-
import { useCallback as useCallback2, useEffect as
|
|
219
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
|
|
289
220
|
async function adminFetch(getToken, path, init) {
|
|
290
221
|
const token = await getToken();
|
|
291
222
|
const headers = new Headers(init?.headers);
|
|
@@ -297,11 +228,11 @@ async function adminFetch(getToken, path, init) {
|
|
|
297
228
|
return body;
|
|
298
229
|
}
|
|
299
230
|
function useAdminResource(getToken, path) {
|
|
300
|
-
const [data, setData] =
|
|
301
|
-
const [loading, setLoading] =
|
|
302
|
-
const [error, setError] =
|
|
303
|
-
const [nonce, setNonce] =
|
|
304
|
-
|
|
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(() => {
|
|
305
236
|
let live = true;
|
|
306
237
|
setLoading(true);
|
|
307
238
|
setError(null);
|
|
@@ -314,271 +245,193 @@ function useAdminResource(getToken, path) {
|
|
|
314
245
|
return { data, loading, error, refresh };
|
|
315
246
|
}
|
|
316
247
|
|
|
317
|
-
// src/ui/admin-
|
|
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";
|
|
318
311
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
419
|
const [busy, setBusy] = useState5(null);
|
|
324
420
|
const [note, setNote] = useState5(null);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}, [getToken, userId]);
|
|
336
|
-
const act = async (key, run, ok) => {
|
|
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) => {
|
|
337
431
|
setBusy(key);
|
|
338
432
|
setNote(null);
|
|
339
433
|
try {
|
|
340
|
-
await
|
|
341
|
-
setNote(ok);
|
|
342
|
-
onChanged();
|
|
343
|
-
} catch (e) {
|
|
344
|
-
setNote(e instanceof Error ? e.message : String(e));
|
|
345
|
-
} finally {
|
|
346
|
-
setBusy(null);
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
const setRole = (role) => act("role", () => adminFetch(getToken, "/api/admin/people/role", { method: "POST", body: JSON.stringify({ userId, role }) }), `Role set to ${role}.`);
|
|
350
|
-
const approve = () => act("approve", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/approve`, { method: "POST" }), "Approved.");
|
|
351
|
-
const refund = () => act("refund", () => adminFetch(getToken, `/api/admin/applications/${applicationId}/refund`, { method: "POST" }), "Refund issued.");
|
|
352
|
-
if (!userId && !applicationId) return null;
|
|
353
|
-
return /* @__PURE__ */ jsxs4("section", { className: "panel", style: { display: "flex", flexDirection: "column", gap: 14, marginTop: 16 }, children: [
|
|
354
|
-
/* @__PURE__ */ jsx4("h3", { style: { margin: 0, fontSize: 16 }, children: "Access & lifecycle" }),
|
|
355
|
-
userId ? /* @__PURE__ */ jsx4(Field, { label: "Role", hint: access?.superAdmin ? "Super-admin \u2014 managed in odla Studio." : void 0, children: /* @__PURE__ */ jsx4(
|
|
356
|
-
Select,
|
|
357
|
-
{
|
|
358
|
-
value: access?.role ?? "",
|
|
359
|
-
disabled: busy === "role" || access?.superAdmin,
|
|
360
|
-
options: roles.map((r) => ({ value: r, label: r })),
|
|
361
|
-
onChange: (e) => void setRole(e.target.value)
|
|
362
|
-
}
|
|
363
|
-
) }) : /* @__PURE__ */ jsx4("p", { className: "muted", style: { margin: 0 }, children: "No linked account yet \u2014 role is set once they sign in." }),
|
|
364
|
-
applicationId ? /* @__PURE__ */ jsxs4("div", { style: { display: "flex", gap: 8 }, children: [
|
|
365
|
-
/* @__PURE__ */ jsx4(Button, { variant: "secondary", mini: true, disabled: busy === "approve", onClick: () => void approve(), children: "Approve" }),
|
|
366
|
-
/* @__PURE__ */ jsx4(Button, { variant: "danger", mini: true, disabled: busy === "refund", onClick: () => void refund(), children: "Refund" })
|
|
367
|
-
] }) : null,
|
|
368
|
-
note ? /* @__PURE__ */ jsx4("p", { className: "muted", style: { margin: 0 }, children: note }) : null
|
|
369
|
-
] });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// src/ui/admin-people.tsx
|
|
373
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
374
|
-
var DEFAULT_ROLES = ["provisional", "member", "admin"];
|
|
375
|
-
function PeopleBody(props) {
|
|
376
|
-
const { crm, client, type, getToken, roles, lifecycle } = props;
|
|
377
|
-
const hookClient = client;
|
|
378
|
-
const query = useCrmQuery(hookClient, type);
|
|
379
|
-
const [openId, setOpenId] = useState6(null);
|
|
380
|
-
const record = useCrmRecord(hookClient, openId);
|
|
381
|
-
const [saving, setSaving] = useState6(false);
|
|
382
|
-
const saveFields = async (input) => {
|
|
383
|
-
if (!openId) return;
|
|
384
|
-
setSaving(true);
|
|
385
|
-
try {
|
|
386
|
-
await client.updateRecord(openId, { input });
|
|
387
|
-
record.refresh();
|
|
388
|
-
query.refresh();
|
|
389
|
-
} finally {
|
|
390
|
-
setSaving(false);
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
const moveStage = async (to) => {
|
|
394
|
-
if (!openId) return;
|
|
395
|
-
await client.setStage(openId, to);
|
|
396
|
-
record.refresh();
|
|
397
|
-
query.refresh();
|
|
398
|
-
};
|
|
399
|
-
const addTag = async (tag) => {
|
|
400
|
-
if (openId) {
|
|
401
|
-
await client.addTag(openId, tag);
|
|
402
|
-
record.refresh();
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
const removeTag = async (tag) => {
|
|
406
|
-
if (openId) {
|
|
407
|
-
await client.removeTag(openId, tag);
|
|
408
|
-
record.refresh();
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
return /* @__PURE__ */ jsxs5(AdminPage, { children: [
|
|
412
|
-
/* @__PURE__ */ jsx5(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
|
|
413
|
-
record.detail ? /* @__PURE__ */ jsxs5("div", { children: [
|
|
414
|
-
/* @__PURE__ */ jsx5(
|
|
415
|
-
RecordPanel,
|
|
416
|
-
{
|
|
417
|
-
crm,
|
|
418
|
-
detail: record.detail,
|
|
419
|
-
onSaveFields: saveFields,
|
|
420
|
-
onMoveStage: moveStage,
|
|
421
|
-
onAddTag: addTag,
|
|
422
|
-
onRemoveTag: removeTag,
|
|
423
|
-
saving
|
|
424
|
-
}
|
|
425
|
-
),
|
|
426
|
-
lifecycle ? /* @__PURE__ */ jsx5(
|
|
427
|
-
RecordActions,
|
|
428
|
-
{
|
|
429
|
-
getToken,
|
|
430
|
-
record: record.detail.record,
|
|
431
|
-
roles,
|
|
432
|
-
onChanged: () => {
|
|
433
|
-
record.refresh();
|
|
434
|
-
query.refresh();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
) : null
|
|
438
|
-
] }) : null
|
|
439
|
-
] });
|
|
440
|
-
}
|
|
441
|
-
function collectionSection(options) {
|
|
442
|
-
const { crm, type } = options;
|
|
443
|
-
let fallbackLabel = type;
|
|
444
|
-
try {
|
|
445
|
-
fallbackLabel = crm.type(type).labelPlural ?? crm.type(type).label ?? type;
|
|
446
|
-
} catch {
|
|
447
|
-
}
|
|
448
|
-
const { id = type, label = fallbackLabel, lifecycle = type === "person", roles = DEFAULT_ROLES } = options;
|
|
449
|
-
return { id, label, render: (ctx) => /* @__PURE__ */ jsx5(PeopleBody, { crm, client: ctx.client, type, getToken: ctx.getToken, roles, lifecycle }) };
|
|
450
|
-
}
|
|
451
|
-
function peopleSection(options) {
|
|
452
|
-
const { crm, id = "people", label = "People", type = "person", roles } = options;
|
|
453
|
-
return collectionSection({ crm, type, id, label, ...roles ? { roles } : {} });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// src/ui/admin-dashboard.tsx
|
|
457
|
-
import { StatBand } from "@odla-ai/ui/components";
|
|
458
|
-
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
459
|
-
var money = (cents) => `$${Math.round(cents / 100).toLocaleString()}`;
|
|
460
|
-
var when = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
|
|
461
|
-
function DashboardBody({ ctx }) {
|
|
462
|
-
const { data, loading, error } = useAdminResource(ctx.getToken, "/api/admin/dashboard");
|
|
463
|
-
if (loading) return /* @__PURE__ */ jsx6(AdminNote, { children: "Loading\u2026" });
|
|
464
|
-
if (error || !data) return /* @__PURE__ */ jsxs6(AdminNote, { children: [
|
|
465
|
-
"Couldn\u2019t load the dashboard",
|
|
466
|
-
error ? `: ${error}` : "",
|
|
467
|
-
"."
|
|
468
|
-
] });
|
|
469
|
-
const kpis = [
|
|
470
|
-
{ value: data.applications.total, label: "Applications", description: `+${data.applications.last7} in the last 7 days` },
|
|
471
|
-
{ value: data.calls.upcoming, label: "Upcoming calls", description: data.calls.needsAttention ? `${data.calls.needsAttention} need attention` : "all on track" }
|
|
472
|
-
];
|
|
473
|
-
if (data.revenue.billingReady) {
|
|
474
|
-
kpis.push({ value: data.revenue.activeCount ?? 0, label: "Active members", description: "paid subscriptions" });
|
|
475
|
-
kpis.push({ value: money(data.revenue.annualRunRateCents ?? 0), label: "Annual run rate", description: "from active subscriptions" });
|
|
476
|
-
}
|
|
477
|
-
const stages = Object.entries(data.pipeline).map(([stage, count]) => ({
|
|
478
|
-
value: count,
|
|
479
|
-
label: stage,
|
|
480
|
-
description: data.pipelineDelta[stage] ? `+${data.pipelineDelta[stage]} this week` : void 0
|
|
481
|
-
}));
|
|
482
|
-
return /* @__PURE__ */ jsxs6(AdminPage, { children: [
|
|
483
|
-
/* @__PURE__ */ jsx6(StatBand, { stats: kpis }),
|
|
484
|
-
/* @__PURE__ */ jsx6(Panel, { title: "Pipeline", children: /* @__PURE__ */ jsx6(StatBand, { stats: stages }) }),
|
|
485
|
-
/* @__PURE__ */ jsx6(Panel, { title: /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
486
|
-
"Upcoming calls ",
|
|
487
|
-
/* @__PURE__ */ jsxs6("span", { className: "muted", style: { fontWeight: 400 }, children: [
|
|
488
|
-
"\xB7 ",
|
|
489
|
-
data.timezone
|
|
490
|
-
] })
|
|
491
|
-
] }), children: data.agenda.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "muted", style: { margin: 0 }, children: "No calls scheduled." }) : /* @__PURE__ */ jsx6("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 10 }, children: data.agenda.map((m) => /* @__PURE__ */ jsxs6("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
|
|
492
|
-
/* @__PURE__ */ jsx6("strong", { style: { minWidth: 170 }, children: when(m.startAt) }),
|
|
493
|
-
/* @__PURE__ */ jsxs6("span", { children: [
|
|
494
|
-
m.name,
|
|
495
|
-
m.email ? ` \xB7 ${m.email}` : ""
|
|
496
|
-
] }),
|
|
497
|
-
m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs6("span", { className: "badge", children: [
|
|
498
|
-
"drift: ",
|
|
499
|
-
m.drift
|
|
500
|
-
] }) : null,
|
|
501
|
-
m.meetUrl ? /* @__PURE__ */ jsx6("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null
|
|
502
|
-
] }, m.id)) }) })
|
|
503
|
-
] });
|
|
504
|
-
}
|
|
505
|
-
function dashboardSection(options = {}) {
|
|
506
|
-
const { id = "dashboard", label = "Dashboard" } = options;
|
|
507
|
-
return { id, label, render: (ctx) => /* @__PURE__ */ jsx6(DashboardBody, { ctx }) };
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// src/ui/admin-billing.tsx
|
|
511
|
-
import { DataTable, StatBand as StatBand2 } from "@odla-ai/ui/components";
|
|
512
|
-
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
513
|
-
var money2 = (cents) => `$${(cents / 100).toLocaleString()}`;
|
|
514
|
-
var date = (t) => t ? new Date(t).toLocaleDateString([], { dateStyle: "medium" }) : "\u2014";
|
|
515
|
-
var COLUMNS = [
|
|
516
|
-
{ key: "name", header: "Name", cell: (r) => r.name, sortAs: "string" },
|
|
517
|
-
{ key: "email", header: "Email", cell: (r) => r.email },
|
|
518
|
-
{ key: "applicationStatus", header: "Application", cell: (r) => r.applicationStatus },
|
|
519
|
-
{ key: "subscriptionStatus", header: "Subscription", cell: (r) => r.subscriptionStatus ? `${r.subscriptionStatus}${r.cancelAtPeriodEnd ? " \xB7 cancelling" : ""}` : "\u2014" },
|
|
520
|
-
{ key: "amountCents", header: "Amount", cell: (r) => `${money2(r.amountCents)}/${r.interval === "month" ? "mo" : "yr"}`, sortAs: "number", sortValue: (r) => r.amountCents },
|
|
521
|
-
{ key: "renewalAt", header: "Renews", cell: (r) => date(r.renewalAt), sortAs: "number", sortValue: (r) => r.renewalAt ?? 0 }
|
|
522
|
-
];
|
|
523
|
-
function BillingBody({ ctx }) {
|
|
524
|
-
const { data, loading, error } = useAdminResource(ctx.getToken, "/api/admin/billing");
|
|
525
|
-
if (loading) return /* @__PURE__ */ jsx7(AdminNote, { children: "Loading\u2026" });
|
|
526
|
-
if (error || !data) return /* @__PURE__ */ jsxs7(AdminNote, { children: [
|
|
527
|
-
"Couldn\u2019t load billing",
|
|
528
|
-
error ? `: ${error}` : "",
|
|
529
|
-
"."
|
|
530
|
-
] });
|
|
531
|
-
if (!data.billingReady) return /* @__PURE__ */ jsx7(AdminNote, { children: "Billing isn\u2019t configured \u2014 no Stripe key is vaulted for this group." });
|
|
532
|
-
const s = data.summary;
|
|
533
|
-
const stats = s ? [
|
|
534
|
-
{ value: s.activeCount, label: "Active" },
|
|
535
|
-
{ value: money2(s.annualizedCents), label: "Annualized" },
|
|
536
|
-
{ value: s.renewingSoonCount, label: "Renewing \u226460d" },
|
|
537
|
-
{ value: s.pastDueCount, label: "Past due" }
|
|
538
|
-
] : [];
|
|
539
|
-
return /* @__PURE__ */ jsxs7(AdminPage, { children: [
|
|
540
|
-
s ? /* @__PURE__ */ jsx7(StatBand2, { stats }) : null,
|
|
541
|
-
/* @__PURE__ */ jsxs7(Panel, { title: "Subscriptions", actions: data.testMode ? /* @__PURE__ */ jsx7("span", { className: "badge", children: "Stripe test mode" }) : void 0, children: [
|
|
542
|
-
data.truncated ? /* @__PURE__ */ jsx7("p", { className: "badge", children: "Showing the first 100 subscriptions \u2014 the list is truncated." }) : null,
|
|
543
|
-
/* @__PURE__ */ jsx7(DataTable, { rows: data.rows, columns: COLUMNS, rowKey: (r) => r.id })
|
|
544
|
-
] })
|
|
545
|
-
] });
|
|
546
|
-
}
|
|
547
|
-
function billingSection(options = {}) {
|
|
548
|
-
const { id = "billing", label = "Billing" } = options;
|
|
549
|
-
return { id, label, render: (ctx) => /* @__PURE__ */ jsx7(BillingBody, { ctx }) };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/ui/admin-email.tsx
|
|
553
|
-
import { useState as useState7 } from "react";
|
|
554
|
-
import { Button as Button2, Checkbox, DataTable as DataTable2, Field as Field2, Input, Textarea } from "@odla-ai/ui/components";
|
|
555
|
-
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
556
|
-
var LOG_COLUMNS = [
|
|
557
|
-
{ key: "sentAt", header: "Sent", cell: (r) => new Date(r.sentAt).toLocaleString(), sortAs: "number", sortValue: (r) => r.sentAt },
|
|
558
|
-
{ key: "template", header: "Template", cell: (r) => r.template },
|
|
559
|
-
{ key: "to", header: "To", cell: (r) => r.to },
|
|
560
|
-
{ key: "status", header: "Status", cell: (r) => r.error ? /* @__PURE__ */ jsx8("span", { className: "badge", children: "failed" }) : r.redirected ? /* @__PURE__ */ jsx8("span", { className: "badge", children: "redirected" }) : "delivered" }
|
|
561
|
-
];
|
|
562
|
-
function EmailBody({ ctx }) {
|
|
563
|
-
const cfg = useAdminResource(ctx.getToken, "/api/admin/group/email");
|
|
564
|
-
const log = useAdminResource(ctx.getToken, "/api/admin/email/log");
|
|
565
|
-
const [draft, setDraft] = useState7(null);
|
|
566
|
-
const [busy, setBusy] = useState7(null);
|
|
567
|
-
const [note, setNote] = useState7(null);
|
|
568
|
-
const data = draft ?? cfg.data;
|
|
569
|
-
if (cfg.loading) return /* @__PURE__ */ jsx8(AdminNote, { children: "Loading\u2026" });
|
|
570
|
-
if (cfg.error || !data) return /* @__PURE__ */ jsxs8(AdminNote, { children: [
|
|
571
|
-
"Couldn\u2019t load email config",
|
|
572
|
-
cfg.error ? `: ${cfg.error}` : "",
|
|
573
|
-
"."
|
|
574
|
-
] });
|
|
575
|
-
const edit = (patch) => setDraft({ ...data, ...patch });
|
|
576
|
-
const editTemplate = (key, patch) => edit({ emailTemplates: { ...data.emailTemplates, [key]: { ...data.emailTemplates[key], ...patch } } });
|
|
577
|
-
const run = async (key, fn) => {
|
|
578
|
-
setBusy(key);
|
|
579
|
-
setNote(null);
|
|
580
|
-
try {
|
|
581
|
-
await fn();
|
|
434
|
+
await fn();
|
|
582
435
|
} catch (e) {
|
|
583
436
|
setNote(e instanceof Error ? e.message : String(e));
|
|
584
437
|
} finally {
|
|
@@ -596,52 +449,52 @@ function EmailBody({ ctx }) {
|
|
|
596
449
|
setNote(r.ok ? `Sent ${template} to ${r.to}${r.redirected ? " (dev-redirected)" : ""}.` : "Test send did not deliver.");
|
|
597
450
|
log.refresh();
|
|
598
451
|
});
|
|
599
|
-
return /* @__PURE__ */
|
|
600
|
-
/* @__PURE__ */
|
|
452
|
+
return /* @__PURE__ */ jsxs6(AdminPage, { children: [
|
|
453
|
+
/* @__PURE__ */ jsxs6(
|
|
601
454
|
Panel,
|
|
602
455
|
{
|
|
603
456
|
title: "Delivery",
|
|
604
|
-
actions: /* @__PURE__ */
|
|
457
|
+
actions: /* @__PURE__ */ jsxs6("span", { className: "muted", children: [
|
|
605
458
|
data.envName,
|
|
606
459
|
" \xB7 ",
|
|
607
460
|
data.transport,
|
|
608
461
|
data.fromEmail ? ` \xB7 ${data.fromEmail}` : ""
|
|
609
462
|
] }),
|
|
610
463
|
children: [
|
|
611
|
-
/* @__PURE__ */
|
|
612
|
-
/* @__PURE__ */
|
|
613
|
-
/* @__PURE__ */
|
|
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 }) }) })
|
|
614
467
|
]
|
|
615
468
|
}
|
|
616
469
|
),
|
|
617
|
-
Object.entries(data.emailTemplates).map(([key, t]) => /* @__PURE__ */
|
|
618
|
-
/* @__PURE__ */
|
|
619
|
-
/* @__PURE__ */
|
|
620
|
-
/* @__PURE__ */
|
|
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 }) }) })
|
|
621
474
|
] }, key)),
|
|
622
|
-
/* @__PURE__ */
|
|
623
|
-
/* @__PURE__ */
|
|
624
|
-
note ? /* @__PURE__ */
|
|
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
|
|
625
478
|
] }),
|
|
626
|
-
/* @__PURE__ */
|
|
479
|
+
/* @__PURE__ */ jsx6(Panel, { title: "Send log", children: /* @__PURE__ */ jsx6(DataTable2, { rows: log.data?.sends ?? [], columns: LOG_COLUMNS, rowKey: (r) => r.id }) })
|
|
627
480
|
] });
|
|
628
481
|
}
|
|
629
482
|
function emailSection(options = {}) {
|
|
630
483
|
const { id = "email", label = "Email" } = options;
|
|
631
|
-
return { id, label, render: (ctx) => /* @__PURE__ */
|
|
484
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx6(EmailBody, { ctx }) };
|
|
632
485
|
}
|
|
633
486
|
|
|
634
487
|
// src/ui/admin-meetings.tsx
|
|
635
|
-
import { useState as
|
|
488
|
+
import { useState as useState6 } from "react";
|
|
636
489
|
import { Button as Button3 } from "@odla-ai/ui/components";
|
|
637
|
-
import { Fragment as Fragment3, jsx as
|
|
490
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
638
491
|
var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
|
|
639
492
|
function MeetingsBody({ ctx }) {
|
|
640
493
|
const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
|
|
641
|
-
const [busy, setBusy] =
|
|
642
|
-
const [note, setNote] =
|
|
643
|
-
if (loading) return /* @__PURE__ */
|
|
644
|
-
if (error || !data) return /* @__PURE__ */
|
|
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: [
|
|
645
498
|
"Couldn\u2019t load meetings",
|
|
646
499
|
error ? `: ${error}` : "",
|
|
647
500
|
"."
|
|
@@ -672,93 +525,377 @@ function MeetingsBody({ ctx }) {
|
|
|
672
525
|
}
|
|
673
526
|
void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
|
|
674
527
|
};
|
|
675
|
-
return /* @__PURE__ */
|
|
528
|
+
return /* @__PURE__ */ jsx7(AdminPage, { children: /* @__PURE__ */ jsx7(Panel, { title: /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
676
529
|
"Agenda ",
|
|
677
|
-
/* @__PURE__ */
|
|
530
|
+
/* @__PURE__ */ jsxs7("span", { className: "muted", style: { fontWeight: 400 }, children: [
|
|
678
531
|
"\xB7 ",
|
|
679
532
|
data.timezone
|
|
680
533
|
] })
|
|
681
|
-
] }), actions: note ? /* @__PURE__ */
|
|
682
|
-
/* @__PURE__ */
|
|
683
|
-
/* @__PURE__ */
|
|
684
|
-
/* @__PURE__ */
|
|
685
|
-
m.drift && m.drift !== "none" ? /* @__PURE__ */
|
|
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: [
|
|
686
539
|
"drift: ",
|
|
687
540
|
m.drift
|
|
688
541
|
] }) : null,
|
|
689
|
-
m.meetUrl ? /* @__PURE__ */
|
|
690
|
-
m.status === "scheduled" ? /* @__PURE__ */
|
|
691
|
-
/* @__PURE__ */
|
|
692
|
-
/* @__PURE__ */
|
|
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" })
|
|
693
546
|
] }) : null
|
|
694
547
|
] }, m.id)) }) }) });
|
|
695
548
|
}
|
|
696
549
|
function meetingsSection(options = {}) {
|
|
697
550
|
const { id = "meetings", label = "Meetings" } = options;
|
|
698
|
-
return { id, label, render: (ctx) => /* @__PURE__ */
|
|
551
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx7(MeetingsBody, { ctx }) };
|
|
699
552
|
}
|
|
700
553
|
|
|
701
|
-
// src/ui/admin-
|
|
554
|
+
// src/ui/admin-people.tsx
|
|
702
555
|
import { useState as useState9 } from "react";
|
|
703
|
-
import {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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);
|
|
724
581
|
setNote(null);
|
|
725
|
-
setErrors({});
|
|
726
|
-
const token = await ctx.getToken();
|
|
727
582
|
try {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
setErrors(body.errors ?? {});
|
|
732
|
-
setNote(body.error ?? `save failed (${r.status})`);
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
setDraft(null);
|
|
736
|
-
refresh();
|
|
737
|
-
setNote("Saved.");
|
|
583
|
+
await run();
|
|
584
|
+
setNote(ok);
|
|
585
|
+
onChanged();
|
|
738
586
|
} catch (e) {
|
|
739
587
|
setNote(e instanceof Error ? e.message : String(e));
|
|
740
588
|
} finally {
|
|
741
|
-
setBusy(
|
|
589
|
+
setBusy(null);
|
|
742
590
|
}
|
|
743
591
|
};
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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." })
|
|
754
645
|
] }),
|
|
755
|
-
/* @__PURE__ */
|
|
756
|
-
/* @__PURE__ */
|
|
757
|
-
] })
|
|
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
|
+
] });
|
|
758
649
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
] });
|
|
762
899
|
}
|
|
763
900
|
|
|
764
901
|
export {
|
|
@@ -766,16 +903,20 @@ export {
|
|
|
766
903
|
Panel,
|
|
767
904
|
ThemeWarning,
|
|
768
905
|
AdminNote,
|
|
769
|
-
|
|
906
|
+
adminSectionFromUrl,
|
|
907
|
+
adminSectionHref,
|
|
770
908
|
adminFetch,
|
|
771
909
|
useAdminResource,
|
|
772
|
-
|
|
773
|
-
collectionSection,
|
|
774
|
-
peopleSection,
|
|
775
|
-
dashboardSection,
|
|
910
|
+
availabilitySection,
|
|
776
911
|
billingSection,
|
|
912
|
+
dashboardSection,
|
|
777
913
|
emailSection,
|
|
778
914
|
meetingsSection,
|
|
779
|
-
|
|
915
|
+
RecordActions,
|
|
916
|
+
NetworkShareActions,
|
|
917
|
+
collectionSection,
|
|
918
|
+
peopleSection,
|
|
919
|
+
defaultAdminSections,
|
|
920
|
+
ChapterAdmin
|
|
780
921
|
};
|
|
781
|
-
//# sourceMappingURL=chunk-
|
|
922
|
+
//# sourceMappingURL=chunk-UIWFEESI.js.map
|