@odla-ai/chapter 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy } from '../../copy-context-pDogTR4x.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-pDogTR4x.js';
4
+ import { a as Chapter, b as ChapterCopy } from '../../copy-context-Dp5hvj5W.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dp5hvj5W.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -24,11 +24,11 @@ import {
24
24
  meetingsSection,
25
25
  peopleSection,
26
26
  useAdminResource
27
- } from "../../chunk-VCC4S6JU.js";
27
+ } from "../../chunk-ZPAJL7KG.js";
28
28
  import {
29
29
  ChapterCopyProvider,
30
30
  useChapterCopy
31
- } from "../../chunk-NTZSXAFT.js";
31
+ } from "../../chunk-WGQO4WDJ.js";
32
32
  export {
33
33
  AdminNote,
34
34
  AdminPage,
@@ -1,5 +1,5 @@
1
1
  export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, MeetingsSectionOptions, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-pDogTR4x.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-Dp5hvj5W.js';
3
3
  export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
package/dist/ui/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  loadJoinResume,
14
14
  timeLabel,
15
15
  tzShort
16
- } from "../chunk-OYXNI6VR.js";
16
+ } from "../chunk-GMGTJIY3.js";
17
17
  import {
18
18
  AdminNote,
19
19
  AdminPage,
@@ -40,12 +40,12 @@ import {
40
40
  meetingsSection,
41
41
  peopleSection,
42
42
  useAdminResource
43
- } from "../chunk-VCC4S6JU.js";
43
+ } from "../chunk-ZPAJL7KG.js";
44
44
  import {
45
45
  BrandStyle,
46
46
  ChapterCopyProvider,
47
47
  useChapterCopy
48
- } from "../chunk-NTZSXAFT.js";
48
+ } from "../chunk-WGQO4WDJ.js";
49
49
  export {
50
50
  AdminNote,
51
51
  AdminPage,
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-pDogTR4x.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-pDogTR4x.js';
3
+ import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-Dp5hvj5W.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dp5hvj5W.js';
5
5
  import '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -13,12 +13,12 @@ import {
13
13
  loadJoinResume,
14
14
  timeLabel,
15
15
  tzShort
16
- } from "../../chunk-OYXNI6VR.js";
16
+ } from "../../chunk-GMGTJIY3.js";
17
17
  import {
18
18
  BrandStyle,
19
19
  ChapterCopyProvider,
20
20
  useChapterCopy
21
- } from "../../chunk-NTZSXAFT.js";
21
+ } from "../../chunk-WGQO4WDJ.js";
22
22
  export {
23
23
  BrandStyle,
24
24
  ChapterCopyProvider,
@@ -948,6 +948,93 @@ var handleMember = async (req, url, env, ctx) => {
948
948
  return null;
949
949
  };
950
950
 
951
+ // src/crm-sync.ts
952
+ var import_crm3 = require("@odla-ai/crm");
953
+ var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
954
+ var crmDeps = (db, ctx) => ({
955
+ crm: ctx.chapter.crm,
956
+ db,
957
+ now: () => Date.now(),
958
+ newId: () => crypto.randomUUID(),
959
+ chapter: ctx.chapter
960
+ });
961
+ function personInputFromApp(chapter, app) {
962
+ const input = sharedPersonInput({
963
+ email: str(app.email),
964
+ firstName: str(app.firstName) || void 0,
965
+ lastName: str(app.lastName) || void 0,
966
+ phone: str(app.phone) || void 0,
967
+ linkedin: str(app.linkedin) || void 0,
968
+ hubRecordId: str(app.id)
969
+ });
970
+ for (const f of chapter.application.crmFields) {
971
+ if (app[f] !== void 0) input[f] = app[f];
972
+ }
973
+ if (app.id !== void 0) input.applicationId = str(app.id);
974
+ return input;
975
+ }
976
+ function billingColumns(app) {
977
+ const status = str(app.status);
978
+ const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
979
+ const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
980
+ const cols = { billingStatus };
981
+ if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
982
+ if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
983
+ if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
984
+ return cols;
985
+ }
986
+ async function syncApplicationToCrm(deps, opts) {
987
+ const emailKey = str(opts.app.email).toLowerCase();
988
+ if (!emailKey) return null;
989
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
990
+ const input = personInputFromApp(deps.chapter, opts.app);
991
+ const { crm_record } = await deps.db.query({
992
+ crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
993
+ });
994
+ const existing = crm_record?.[0] ?? null;
995
+ const stage = opts.stage || void 0;
996
+ let recordId;
997
+ if (existing && typeof existing.id === "string") {
998
+ recordId = existing.id;
999
+ await (0, import_crm3.updateRecord)(recordDeps, { id: recordId, input });
1000
+ } else {
1001
+ const created = await (0, import_crm3.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
1002
+ recordId = created.id;
1003
+ }
1004
+ if (existing && stage && existing.stage !== stage) {
1005
+ await (0, import_crm3.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1006
+ }
1007
+ await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1008
+ await (0, import_crm3.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1009
+ return recordId;
1010
+ }
1011
+ async function backfillCrm(deps) {
1012
+ const [appsRes, usersRes] = await Promise.all([
1013
+ deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1014
+ deps.db.query({ $users: { $: { limit: 1e3 } } })
1015
+ ]);
1016
+ const seen = /* @__PURE__ */ new Set();
1017
+ let synced = 0;
1018
+ const errors = [];
1019
+ const run = async (app, stage) => {
1020
+ const key = str(app.email).toLowerCase();
1021
+ if (!key || seen.has(key)) return;
1022
+ seen.add(key);
1023
+ try {
1024
+ await syncApplicationToCrm(deps, { app, stage });
1025
+ synced += 1;
1026
+ } catch (err) {
1027
+ errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1028
+ }
1029
+ };
1030
+ for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1031
+ for (const u of usersRes.$users ?? []) {
1032
+ if (u.deleted === true) continue;
1033
+ await run({ email: u.email, firstName: str(u.name) });
1034
+ }
1035
+ return { synced, errors };
1036
+ }
1037
+
951
1038
  // src/worker-routes-schedule.ts
952
1039
  var import_calendar = require("@odla-ai/calendar");
953
1040
  function errCode(err) {
@@ -1051,8 +1138,11 @@ async function bookSlot(req, env, ctx) {
1051
1138
  if (code === "calendar_slot_unavailable") return json({ error: "slot no longer available", code }, 409);
1052
1139
  return json({ error: "booking failed", code }, 502);
1053
1140
  }
1054
- const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
1141
+ const bookingAttrs = applicationBookingUpdate(status, startAt, htmlLink);
1142
+ const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
1055
1143
  await db.transact([meetingOp, appOp]);
1144
+ const booked = { ...app, ...bookingAttrs };
1145
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
1056
1146
  if (typeof app.email === "string" && app.email) {
1057
1147
  await sendTemplated(
1058
1148
  { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
@@ -1402,6 +1492,8 @@ async function ingestWebhook(req, env, ctx) {
1402
1492
  const patch = webhookPatch(event, String(app.status ?? ""));
1403
1493
  if (Object.keys(patch).length) {
1404
1494
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
1495
+ const next = { ...app, ...patch };
1496
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: next, stage: String(next.status ?? "") }).catch(() => void 0);
1405
1497
  }
1406
1498
  if (event.kind === "first_payment") {
1407
1499
  await notifyPaymentConfirmed(db, env, eventId, app);
@@ -1593,86 +1685,6 @@ async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
1593
1685
  return res.ok;
1594
1686
  }
1595
1687
 
1596
- // src/crm-sync.ts
1597
- var import_crm3 = require("@odla-ai/crm");
1598
- var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1599
- function personInputFromApp(chapter, app) {
1600
- const input = sharedPersonInput({
1601
- email: str(app.email),
1602
- firstName: str(app.firstName) || void 0,
1603
- lastName: str(app.lastName) || void 0,
1604
- phone: str(app.phone) || void 0,
1605
- linkedin: str(app.linkedin) || void 0,
1606
- hubRecordId: str(app.id)
1607
- });
1608
- for (const f of chapter.application.crmFields) {
1609
- if (app[f] !== void 0) input[f] = app[f];
1610
- }
1611
- if (app.id !== void 0) input.applicationId = str(app.id);
1612
- return input;
1613
- }
1614
- function billingColumns(app) {
1615
- const status = str(app.status);
1616
- const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
1617
- const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
1618
- const cols = { billingStatus };
1619
- if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
1620
- if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
1621
- if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
1622
- return cols;
1623
- }
1624
- async function syncApplicationToCrm(deps, opts) {
1625
- const emailKey = str(opts.app.email).toLowerCase();
1626
- if (!emailKey) return null;
1627
- const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1628
- const input = personInputFromApp(deps.chapter, opts.app);
1629
- const { crm_record } = await deps.db.query({
1630
- crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
1631
- });
1632
- const existing = crm_record?.[0] ?? null;
1633
- const stage = opts.stage || void 0;
1634
- let recordId;
1635
- if (existing && typeof existing.id === "string") {
1636
- recordId = existing.id;
1637
- await (0, import_crm3.updateRecord)(crmDeps3, { id: recordId, input });
1638
- } else {
1639
- const created = await (0, import_crm3.createRecord)(crmDeps3, { type: "person", input, ...stage ? { stage } : {} });
1640
- recordId = created.id;
1641
- }
1642
- if (existing && stage && existing.stage !== stage) {
1643
- await (0, import_crm3.setStage)(crmDeps3, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1644
- }
1645
- await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1646
- await (0, import_crm3.linkIdentity)(crmDeps3, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1647
- return recordId;
1648
- }
1649
- async function backfillCrm(deps) {
1650
- const [appsRes, usersRes] = await Promise.all([
1651
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1652
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1653
- ]);
1654
- const seen = /* @__PURE__ */ new Set();
1655
- let synced = 0;
1656
- const errors = [];
1657
- const run = async (app, stage) => {
1658
- const key = str(app.email).toLowerCase();
1659
- if (!key || seen.has(key)) return;
1660
- seen.add(key);
1661
- try {
1662
- await syncApplicationToCrm(deps, { app, stage });
1663
- synced += 1;
1664
- } catch (err) {
1665
- errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1666
- }
1667
- };
1668
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1669
- for (const u of usersRes.$users ?? []) {
1670
- if (u.deleted === true) continue;
1671
- await run({ email: u.email, firstName: str(u.name) });
1672
- }
1673
- return { synced, errors };
1674
- }
1675
-
1676
1688
  // src/worker-routes-admin-people.ts
1677
1689
  async function adminGate(req, env, ctx) {
1678
1690
  const rawDb = ctx.makeDb(env);
@@ -1681,7 +1693,7 @@ async function adminGate(req, env, ctx) {
1681
1693
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
1682
1694
  return { db: rawDb, actor: { userId: u.userId, email: u.email ?? void 0 } };
1683
1695
  }
1684
- var crmDeps = (db, ctx) => ({
1696
+ var crmDeps2 = (db, ctx) => ({
1685
1697
  crm: ctx.chapter.crm,
1686
1698
  db,
1687
1699
  now: () => Date.now(),
@@ -1692,7 +1704,7 @@ var handleAdminCrmSync = async (req, url, env, ctx) => {
1692
1704
  if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
1693
1705
  const gate5 = await adminGate(req, env, ctx);
1694
1706
  if (gate5 instanceof Response) return gate5;
1695
- const result = await backfillCrm(crmDeps(gate5.db, ctx));
1707
+ const result = await backfillCrm(crmDeps2(gate5.db, ctx));
1696
1708
  return json({ ok: true, ...result });
1697
1709
  };
1698
1710
  var handleAdminPeople = async (req, url, env, ctx) => {
@@ -1972,7 +1984,6 @@ async function gate2(req, env, ctx) {
1972
1984
  return rawDb;
1973
1985
  }
1974
1986
  var calFor = (env) => (0, import_calendar3.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
1975
- var crmDeps2 = (db, ctx) => ({ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID(), chapter: ctx.chapter });
1976
1987
  var readJson = async (req) => {
1977
1988
  try {
1978
1989
  return await req.json();
@@ -2058,7 +2069,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
2058
2069
  if (!canApprove(String(app.status), ctx.chapter.pipeline)) return json({ error: `cannot approve from status "${String(app.status)}"` }, 409);
2059
2070
  const target = "approved";
2060
2071
  await db.transact([{ t: "update", ns: "applications", id, attrs: { status: target } }]);
2061
- await syncApplicationToCrm(crmDeps2(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
2072
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
2062
2073
  const { promoteTo, send } = ctx.chapter.operations.onApprove;
2063
2074
  let rolePromoted = false;
2064
2075
  const sk = await getVaultSecret(db, "clerk_secret_key");
@@ -2139,7 +2150,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
2139
2150
  if (Object.keys(attrs).length === 0) return json({ error: "nothing to update" }, 400);
2140
2151
  await db.transact([{ t: "update", ns: "applications", id, attrs }]);
2141
2152
  if (attrs.status !== void 0) {
2142
- await syncApplicationToCrm(crmDeps2(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
2153
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
2143
2154
  }
2144
2155
  return json({ ok: true });
2145
2156
  };