@fonderie/vue-admin-screens 1.2.0 → 1.3.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/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  ModulesScreen: () => ModulesScreen,
32
32
  OperatorsScreen: () => OperatorsScreen,
33
33
  RoutesScreen: () => RoutesScreen,
34
+ SubscriberBilling: () => SubscriberBilling,
34
35
  SubscriberScreen: () => SubscriberScreen,
35
36
  TokensScreen: () => TokensScreen,
36
37
  UsersScreen: () => UsersScreen
@@ -40,7 +41,7 @@ module.exports = __toCommonJS(index_exports);
40
41
  // src/screens/AdminShell.ts
41
42
  var import_vue_config_admin_screens = require("@fonderie/vue-config-admin-screens");
42
43
  var import_vue_courier_admin_screens = require("@fonderie/vue-courier-admin-screens");
43
- var import_vue16 = require("vue");
44
+ var import_vue18 = require("vue");
44
45
 
45
46
  // src/styles.ts
46
47
  var t = {
@@ -596,6 +597,12 @@ function empty(title, body, iconName = "check") {
596
597
  body ? (0, import_vue.h)("div", body) : null
597
598
  ]);
598
599
  }
600
+ function refreshButton(onClick, busy, label = "Refresh") {
601
+ return (0, import_vue.h)("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
602
+ icon("refresh", 14),
603
+ busy ? "Working\u2026" : label
604
+ ]);
605
+ }
599
606
  function methodChip(method) {
600
607
  const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
601
608
  const color = toneColor[tone];
@@ -647,7 +654,7 @@ function table(headers, rows) {
647
654
  ]);
648
655
  }
649
656
  var td = (children, extra = {}) => (0, import_vue2.h)("td", { style: { ...styles.td, ...extra } }, children);
650
- var refreshButton = (label, disabled, onClick) => (0, import_vue2.h)("button", { type: "button", style: styles.button, disabled, onClick }, [
657
+ var refreshButton2 = (label, disabled, onClick) => (0, import_vue2.h)("button", { type: "button", style: styles.button, disabled, onClick }, [
651
658
  icon("refresh", 14),
652
659
  disabled ? "Working\u2026" : label
653
660
  ]);
@@ -673,7 +680,7 @@ var AdminLogScreen = (0, import_vue3.defineComponent)({
673
680
  pageHeader(
674
681
  "Admin log",
675
682
  "Every request made through this surface, newest first \u2014 refused ones included.",
676
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
683
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
677
684
  ),
678
685
  error.value ? (0, import_vue3.h)(
679
686
  "p",
@@ -814,7 +821,7 @@ var AttentionScreen = (0, import_vue4.defineComponent)({
814
821
  )
815
822
  ];
816
823
  },
817
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
824
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
818
825
  {
819
826
  lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
820
827
  }
@@ -994,7 +1001,7 @@ var DoctorScreen = (0, import_vue6.defineComponent)({
994
1001
  report.value.ok ? "ok" : "bad",
995
1002
  report.value.ok ? "all checks pass" : "a check failed"
996
1003
  ) : null,
997
- refreshButton("Run again", isLoading.value, () => void refresh())
1004
+ refreshButton2("Run again", isLoading.value, () => void refresh())
998
1005
  ],
999
1006
  {
1000
1007
  loadingText: "Running the checks\u2026",
@@ -1197,7 +1204,7 @@ var MigrationsScreen = (0, import_vue9.defineComponent)({
1197
1204
  ...behind.map((m) => section(m, r.everApplied))
1198
1205
  ].filter(Boolean);
1199
1206
  },
1200
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
1207
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
1201
1208
  {
1202
1209
  lead: "Schema changes each module ships, and whether this database has them.",
1203
1210
  errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
@@ -1706,29 +1713,234 @@ var TokensScreen = (0, import_vue11.defineComponent)({
1706
1713
 
1707
1714
  // src/screens/UsersScreen.ts
1708
1715
  var import_client = require("@fonderie/client");
1709
- var import_vue_admin10 = require("@fonderie/vue-admin");
1716
+ var import_vue_admin11 = require("@fonderie/vue-admin");
1717
+ var import_vue14 = require("vue");
1718
+
1719
+ // src/billing.ts
1710
1720
  var import_vue12 = require("vue");
1711
- var UsersScreen = (0, import_vue12.defineComponent)({
1721
+ function periodEnd(s) {
1722
+ if (!s.currentPeriodEnd) return null;
1723
+ const date = new Date(s.currentPeriodEnd).toLocaleDateString();
1724
+ if (s.status === "canceled")
1725
+ return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
1726
+ if (s.cancelAtPeriodEnd) return { label: "Ends", date };
1727
+ return { label: "Renews", date };
1728
+ }
1729
+ var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
1730
+ function useSubscriptionIndex(client) {
1731
+ const index = (0, import_vue12.ref)(null);
1732
+ if (!client) return index;
1733
+ void (async () => {
1734
+ const map = /* @__PURE__ */ new Map();
1735
+ let cursor;
1736
+ for (let page2 = 0; page2 < 20; page2++) {
1737
+ const { result } = await client.listSubscriptions({
1738
+ limit: 100,
1739
+ ...cursor ? { cursor } : {}
1740
+ });
1741
+ for (const s of result.subscriptions) {
1742
+ const key = `${s.subscriberType}/${s.subscriberId}`;
1743
+ if (!map.has(key)) map.set(key, s);
1744
+ }
1745
+ if (!result.nextCursor) break;
1746
+ cursor = result.nextCursor;
1747
+ }
1748
+ index.value = map;
1749
+ })().catch(() => {
1750
+ index.value = /* @__PURE__ */ new Map();
1751
+ });
1752
+ return index;
1753
+ }
1754
+
1755
+ // src/screens/SubscriberBilling.ts
1756
+ var import_vue_admin10 = require("@fonderie/vue-admin");
1757
+ var import_vue13 = require("vue");
1758
+ var SubscriberBilling = (0, import_vue13.defineComponent)({
1759
+ name: "FonderieSubscriberBilling",
1760
+ props: {
1761
+ client: { type: Object, required: true },
1762
+ subscriberType: { type: String, required: true },
1763
+ subscriberId: { type: String, required: true }
1764
+ },
1765
+ setup(props) {
1766
+ const subscriber = (0, import_vue13.computed)(() => ({ type: props.subscriberType, id: props.subscriberId }));
1767
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin10.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
1768
+ const amount = (0, import_vue13.ref)("");
1769
+ const note = (0, import_vue13.ref)("");
1770
+ const granted = (0, import_vue13.ref)(null);
1771
+ return () => {
1772
+ const s = subscription.value;
1773
+ const w = wallet.value;
1774
+ if (isLoading.value && !w && !s) return (0, import_vue13.h)("p", { style: styles.status }, "Loading billing\u2026");
1775
+ const end = s ? periodEnd(s) : null;
1776
+ return (0, import_vue13.h)("div", [
1777
+ error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1778
+ (0, import_vue13.h)("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
1779
+ (0, import_vue13.h)("div", { style: styles.card }, [
1780
+ (0, import_vue13.h)("div", { style: styles.statLabel }, "Plan"),
1781
+ (0, import_vue13.h)(
1782
+ "div",
1783
+ {
1784
+ style: {
1785
+ display: "flex",
1786
+ alignItems: "center",
1787
+ gap: "8px",
1788
+ marginTop: "6px",
1789
+ flexWrap: "wrap"
1790
+ }
1791
+ },
1792
+ [
1793
+ (0, import_vue13.h)("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
1794
+ ...s ? [
1795
+ (0, import_vue13.h)("span", { style: styles.muted }, `\xB7 ${s.interval}`),
1796
+ pill(statusTone(s.status), s.status)
1797
+ ] : [(0, import_vue13.h)("span", { style: styles.muted }, "no subscription")]
1798
+ ]
1799
+ ),
1800
+ end ? (0, import_vue13.h)("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
1801
+ s?.providerSubscriptionId ? (0, import_vue13.h)(
1802
+ "div",
1803
+ { style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" } },
1804
+ s.providerSubscriptionId
1805
+ ) : null
1806
+ ]),
1807
+ (0, import_vue13.h)("div", { style: styles.card }, [
1808
+ (0, import_vue13.h)("div", { style: styles.statLabel }, "Credits"),
1809
+ ...w ? [
1810
+ (0, import_vue13.h)("div", { style: styles.statValue }, [
1811
+ `${w.balance} `,
1812
+ (0, import_vue13.h)("span", { style: { fontSize: "14px", fontWeight: 500 } }, w.currency)
1813
+ ]),
1814
+ (0, import_vue13.h)(
1815
+ "div",
1816
+ { style: styles.statHint },
1817
+ `${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
1818
+ )
1819
+ ] : [
1820
+ (0, import_vue13.h)(
1821
+ "div",
1822
+ { style: styles.statHint },
1823
+ "No wallet \u2014 billing has no wallet configuration."
1824
+ )
1825
+ ]
1826
+ ])
1827
+ ]),
1828
+ w ? (0, import_vue13.h)(
1829
+ "form",
1830
+ {
1831
+ style: {
1832
+ ...styles.card,
1833
+ display: "flex",
1834
+ gap: "8px",
1835
+ alignItems: "center",
1836
+ flexWrap: "wrap",
1837
+ marginBottom: "16px"
1838
+ },
1839
+ onSubmit: (e) => {
1840
+ e.preventDefault();
1841
+ const key = `admin-grant-${props.subscriberType}-${props.subscriberId}-${Date.now()}`;
1842
+ const a = amount.value.trim();
1843
+ void grant({
1844
+ amount: a,
1845
+ ...note.value ? { description: note.value } : {},
1846
+ idempotencyKey: key
1847
+ }).then(() => {
1848
+ granted.value = `Granted ${a} ${w.currency}.`;
1849
+ amount.value = "";
1850
+ note.value = "";
1851
+ }).catch(() => {
1852
+ });
1853
+ }
1854
+ },
1855
+ [
1856
+ (0, import_vue13.h)("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
1857
+ (0, import_vue13.h)("input", {
1858
+ value: amount.value,
1859
+ onInput: (e) => amount.value = e.target.value,
1860
+ placeholder: `amount (${w.currency})`,
1861
+ inputmode: "numeric",
1862
+ style: { ...styles.input, width: "150px" },
1863
+ "aria-label": "Amount"
1864
+ }),
1865
+ (0, import_vue13.h)("input", {
1866
+ value: note.value,
1867
+ onInput: (e) => note.value = e.target.value,
1868
+ placeholder: "reason (optional)",
1869
+ style: { ...styles.input, flex: 1, minWidth: "180px" },
1870
+ "aria-label": "Reason"
1871
+ }),
1872
+ (0, import_vue13.h)(
1873
+ "button",
1874
+ {
1875
+ type: "submit",
1876
+ style: styles.buttonPrimary,
1877
+ disabled: !/^\d+$/.test(amount.value.trim())
1878
+ },
1879
+ [icon("plus", 14), "Grant"]
1880
+ ),
1881
+ granted.value ? (0, import_vue13.h)("span", { style: styles.ok }, granted.value) : null
1882
+ ]
1883
+ ) : null,
1884
+ w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : (0, import_vue13.h)("div", [
1885
+ table(
1886
+ ["When", "Kind", "Amount", "Note"],
1887
+ ledger.value.map(
1888
+ (t2) => (0, import_vue13.h)("tr", { key: t2.id }, [
1889
+ td(new Date(t2.createdAt).toLocaleString(), {
1890
+ ...styles.muted,
1891
+ whiteSpace: "nowrap"
1892
+ }),
1893
+ td(t2.type),
1894
+ td(t2.amount, styles.mono),
1895
+ td(t2.description ?? "", styles.muted)
1896
+ ])
1897
+ )
1898
+ ),
1899
+ hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
1900
+ ]) : null
1901
+ ]);
1902
+ };
1903
+ }
1904
+ });
1905
+
1906
+ // src/screens/UsersScreen.ts
1907
+ var UsersScreen = (0, import_vue14.defineComponent)({
1712
1908
  name: "FonderieUsersScreen",
1713
1909
  props: {
1714
1910
  client: { type: Object, required: true },
1715
- pageSize: { type: Number, default: 50 }
1911
+ pageSize: { type: Number, default: 50 },
1912
+ // Given ⇒ a Plan column, and each user's plan, credits and grant form on
1913
+ // their page. Everyone has a wallet; no subscription means the free tier.
1914
+ billingClient: { type: Object, default: void 0 },
1915
+ // Open this user on arrival (e.g. from the Subscriptions list).
1916
+ openUserId: { type: String, default: void 0 }
1716
1917
  },
1717
1918
  setup(props) {
1718
- const input = (0, import_vue12.ref)("");
1719
- const email = (0, import_vue12.ref)("");
1720
- const selectedId = (0, import_vue12.ref)("");
1721
- const list = (0, import_vue_admin10.useAdminUsers)(props.client, { limit: props.pageSize });
1722
- const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin10.useAdminUser)(
1919
+ const input = (0, import_vue14.ref)("");
1920
+ const email = (0, import_vue14.ref)("");
1921
+ const selectedId = (0, import_vue14.ref)(props.openUserId ?? "");
1922
+ const plans = useSubscriptionIndex(props.billingClient);
1923
+ const list = (0, import_vue_admin11.useAdminUsers)(props.client, { limit: props.pageSize });
1924
+ const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin11.useAdminUser)(
1723
1925
  props.client,
1724
1926
  { email, id: selectedId }
1725
1927
  );
1726
- const userId = (0, import_vue12.computed)(() => user.value?.id ?? null);
1727
- const sessions = (0, import_vue_admin10.useAdminUserSessions)(props.client, userId);
1728
- const history = (0, import_vue_admin10.useAdminLoginHistory)(props.client, userId, { limit: 20 });
1729
- const showList = (0, import_vue12.computed)(() => !email.value && !selectedId.value);
1928
+ const userId = (0, import_vue14.computed)(() => user.value?.id ?? null);
1929
+ const sessions = (0, import_vue_admin11.useAdminUserSessions)(props.client, userId);
1930
+ const history = (0, import_vue_admin11.useAdminLoginHistory)(props.client, userId, { limit: 20 });
1931
+ const showList = (0, import_vue14.computed)(() => !email.value && !selectedId.value);
1730
1932
  const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1731
- const row = (k, v) => (0, import_vue12.h)("tr", [td(k), td(v)]);
1933
+ const row = (k, v) => (0, import_vue14.h)("tr", [td(k), td(v)]);
1934
+ const planCell = (id) => {
1935
+ if (!plans.value) return (0, import_vue14.h)("span", { style: styles.muted }, "\u2026");
1936
+ const sub = plans.value.get(`user/${id}`);
1937
+ if (!sub || sub.status === "canceled") return (0, import_vue14.h)("span", { style: styles.muted }, "free");
1938
+ return [
1939
+ (0, import_vue14.h)("strong", sub.plan),
1940
+ " ",
1941
+ sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
1942
+ ];
1943
+ };
1732
1944
  const clear = () => {
1733
1945
  email.value = "";
1734
1946
  selectedId.value = "";
@@ -1736,12 +1948,12 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1736
1948
  };
1737
1949
  return () => {
1738
1950
  const u = user.value;
1739
- return (0, import_vue12.h)("div", { style: styles.container }, [
1951
+ return (0, import_vue14.h)("div", { style: styles.container }, [
1740
1952
  pageHeader(
1741
1953
  "Users",
1742
1954
  "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1743
1955
  ),
1744
- (0, import_vue12.h)(
1956
+ (0, import_vue14.h)(
1745
1957
  "form",
1746
1958
  {
1747
1959
  style: styles.toolbar,
@@ -1752,7 +1964,7 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1752
1964
  }
1753
1965
  },
1754
1966
  [
1755
- (0, import_vue12.h)("input", {
1967
+ (0, import_vue14.h)("input", {
1756
1968
  type: "email",
1757
1969
  value: input.value,
1758
1970
  onInput: (e) => input.value = e.target.value,
@@ -1760,22 +1972,22 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1760
1972
  style: { ...styles.input, minWidth: "280px" },
1761
1973
  "aria-label": "Email"
1762
1974
  }),
1763
- (0, import_vue12.h)(
1975
+ (0, import_vue14.h)(
1764
1976
  "button",
1765
1977
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1766
1978
  [icon("search", 14), "Look up"]
1767
1979
  ),
1768
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1980
+ showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1769
1981
  ]
1770
1982
  ),
1771
- showList.value ? (0, import_vue12.h)("div", [
1772
- list.error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1983
+ showList.value ? (0, import_vue14.h)("div", [
1984
+ list.error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1773
1985
  list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
1774
- ["Email", "Name", "Created", "Status"],
1986
+ props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
1775
1987
  list.users.value.map(
1776
- (u2) => (0, import_vue12.h)("tr", { key: u2.id }, [
1988
+ (u2) => (0, import_vue14.h)("tr", { key: u2.id }, [
1777
1989
  td(
1778
- (0, import_vue12.h)(
1990
+ (0, import_vue14.h)(
1779
1991
  "button",
1780
1992
  {
1781
1993
  type: "button",
@@ -1786,8 +1998,9 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1786
1998
  )
1787
1999
  ),
1788
2000
  td(
1789
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue12.h)("span", { style: styles.muted }, "\u2014")
2001
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue14.h)("span", { style: styles.muted }, "\u2014")
1790
2002
  ),
2003
+ props.billingClient ? td(planCell(u2.id)) : null,
1791
2004
  td(new Date(u2.createdAt).toLocaleDateString()),
1792
2005
  td(
1793
2006
  u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
@@ -1795,24 +2008,24 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1795
2008
  ])
1796
2009
  )
1797
2010
  ),
1798
- list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.status }, "Loading\u2026") : null,
2011
+ list.isLoading.value ? (0, import_vue14.h)("p", { style: styles.status }, "Loading\u2026") : null,
1799
2012
  list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
1800
2013
  ]) : null,
1801
- error.value ? (0, import_vue12.h)(
2014
+ error.value ? (0, import_vue14.h)(
1802
2015
  "p",
1803
2016
  { style: styles.error, role: "alert" },
1804
2017
  error.value.status === 404 ? "No user with that email." : error.value.explanation
1805
2018
  ) : null,
1806
2019
  u ? [
1807
- (0, import_vue12.h)("h2", { style: styles.subtitle }, [
2020
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, [
1808
2021
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
1809
2022
  " ",
1810
2023
  u.suspended ? pill("warn", "suspended") : null,
1811
2024
  u.deletedAt ? pill("neutral", "deleted") : null
1812
2025
  ]),
1813
- (0, import_vue12.h)("table", { style: styles.table }, [
1814
- (0, import_vue12.h)("tbody", [
1815
- row("id", (0, import_vue12.h)("span", { style: styles.mono }, u.id)),
2026
+ (0, import_vue14.h)("table", { style: styles.table }, [
2027
+ (0, import_vue14.h)("tbody", [
2028
+ row("id", (0, import_vue14.h)("span", { style: styles.mono }, u.id)),
1816
2029
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
1817
2030
  row("MFA", yesNo(u.mfaEnabled)),
1818
2031
  row(
@@ -1821,12 +2034,12 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1821
2034
  ),
1822
2035
  row(
1823
2036
  "last login",
1824
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue12.h)("span", { style: styles.muted }, "never")
2037
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue14.h)("span", { style: styles.muted }, "never")
1825
2038
  ),
1826
2039
  row("created", new Date(u.createdAt).toLocaleString())
1827
2040
  ])
1828
2041
  ]),
1829
- (0, import_vue12.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2042
+ (0, import_vue14.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1830
2043
  u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1831
2044
  "Suspend",
1832
2045
  isLoading.value,
@@ -1839,19 +2052,26 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1839
2052
  () => void revokeSessions().then(() => sessions.refresh())
1840
2053
  )
1841
2054
  ]),
1842
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Live sessions"),
1843
- sessions.sessions.value.length === 0 ? (0, import_vue12.h)("p", { style: styles.muted }, "None.") : (0, import_vue12.h)(
2055
+ props.billingClient ? (0, import_vue14.h)("h2", { style: styles.subtitle }, "Plan & credits") : null,
2056
+ props.billingClient ? (0, import_vue14.h)(SubscriberBilling, {
2057
+ key: u.id,
2058
+ client: props.billingClient,
2059
+ subscriberType: "user",
2060
+ subscriberId: u.id
2061
+ }) : null,
2062
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Live sessions"),
2063
+ sessions.sessions.value.length === 0 ? (0, import_vue14.h)("p", { style: styles.muted }, "None.") : (0, import_vue14.h)(
1844
2064
  "ul",
1845
2065
  { style: styles.list },
1846
2066
  sessions.sessions.value.map(
1847
- (s) => (0, import_vue12.h)("li", { key: s.id, style: styles.row }, [
1848
- (0, import_vue12.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
2067
+ (s) => (0, import_vue14.h)("li", { key: s.id, style: styles.row }, [
2068
+ (0, import_vue14.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1849
2069
  " ",
1850
- (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
2070
+ (0, import_vue14.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
1851
2071
  " ",
1852
- (0, import_vue12.h)("span", { style: styles.muted }, s.userAgent ?? ""),
2072
+ (0, import_vue14.h)("span", { style: styles.muted }, s.userAgent ?? ""),
1853
2073
  " ",
1854
- (0, import_vue12.h)(
2074
+ (0, import_vue14.h)(
1855
2075
  "span",
1856
2076
  { style: styles.muted },
1857
2077
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -1859,22 +2079,22 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1859
2079
  ])
1860
2080
  )
1861
2081
  ),
1862
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
1863
- history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue12.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue12.h)(
2082
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
2083
+ history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue14.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue14.h)(
1864
2084
  "ul",
1865
2085
  { style: styles.list },
1866
2086
  history.events.value.map(
1867
- (e) => (0, import_vue12.h)("li", { key: e.id, style: styles.row }, [
2087
+ (e) => (0, import_vue14.h)("li", { key: e.id, style: styles.row }, [
1868
2088
  pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
1869
2089
  " ",
1870
- (0, import_vue12.h)("span", { style: styles.muted }, e.method),
2090
+ (0, import_vue14.h)("span", { style: styles.muted }, e.method),
1871
2091
  " ",
1872
- (0, import_vue12.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
2092
+ (0, import_vue14.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1873
2093
  " ",
1874
- (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
2094
+ (0, import_vue14.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
1875
2095
  e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
1876
2096
  " ",
1877
- (0, import_vue12.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
2097
+ (0, import_vue14.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1878
2098
  ])
1879
2099
  )
1880
2100
  ),
@@ -1886,27 +2106,27 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1886
2106
  });
1887
2107
 
1888
2108
  // src/screens/CatalogScreen.ts
1889
- var import_vue_admin11 = require("@fonderie/vue-admin");
1890
- var import_vue13 = require("vue");
1891
- var CatalogScreen = (0, import_vue13.defineComponent)({
2109
+ var import_vue_admin12 = require("@fonderie/vue-admin");
2110
+ var import_vue15 = require("vue");
2111
+ var CatalogScreen = (0, import_vue15.defineComponent)({
1892
2112
  name: "FonderieCatalogScreen",
1893
2113
  props: { client: { type: Object, required: true } },
1894
2114
  setup(props) {
1895
- const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin11.useAdminCatalog)(props.client);
2115
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin12.useAdminCatalog)(props.client);
1896
2116
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1897
2117
  return () => {
1898
2118
  const c = catalog.value;
1899
- return (0, import_vue13.h)("div", { style: styles.container }, [
2119
+ return (0, import_vue15.h)("div", { style: styles.container }, [
1900
2120
  pageHeader(
1901
2121
  "Catalog",
1902
2122
  "What you sell: plans as configured in code, and as stored in the database, side by side.",
1903
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
2123
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
1904
2124
  ),
1905
- error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1906
- isLoading.value && !c ? (0, import_vue13.h)("p", { style: styles.status }, "Loading\u2026") : null,
2125
+ error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2126
+ isLoading.value && !c ? (0, import_vue15.h)("p", { style: styles.status }, "Loading\u2026") : null,
1907
2127
  c ? [
1908
- (0, import_vue13.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1909
- (0, import_vue13.h)(
2128
+ (0, import_vue15.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2129
+ (0, import_vue15.h)(
1910
2130
  "pre",
1911
2131
  {
1912
2132
  style: {
@@ -1920,7 +2140,7 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1920
2140
  },
1921
2141
  JSON.stringify(c.configured, null, 2)
1922
2142
  ),
1923
- (0, import_vue13.h)("h2", { style: styles.subtitle }, "Stored (database)"),
2143
+ (0, import_vue15.h)("h2", { style: styles.subtitle }, "Stored (database)"),
1924
2144
  c.stored.length === 0 ? empty(
1925
2145
  "No stored plans",
1926
2146
  "Plans are configured in code; nothing has been written to the database.",
@@ -1928,10 +2148,10 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1928
2148
  ) : table(
1929
2149
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
1930
2150
  c.stored.map(
1931
- (p) => (0, import_vue13.h)("tr", { key: p.id }, [
2151
+ (p) => (0, import_vue15.h)("tr", { key: p.id }, [
1932
2152
  td([
1933
- (0, import_vue13.h)("strong", p.name),
1934
- p.description ? (0, import_vue13.h)("div", { style: styles.muted }, p.description) : ""
2153
+ (0, import_vue15.h)("strong", p.name),
2154
+ p.description ? (0, import_vue15.h)("div", { style: styles.muted }, p.description) : ""
1935
2155
  ]),
1936
2156
  td(String(p.tier)),
1937
2157
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -1939,7 +2159,7 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1939
2159
  td(money(p.pricing?.yearly)),
1940
2160
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
1941
2161
  td(
1942
- (0, import_vue13.h)(
2162
+ (0, import_vue15.h)(
1943
2163
  "button",
1944
2164
  {
1945
2165
  type: "button",
@@ -1963,293 +2183,205 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1963
2183
  });
1964
2184
 
1965
2185
  // src/screens/SubscriberScreen.ts
1966
- var import_vue_admin12 = require("@fonderie/vue-admin");
1967
- var import_vue14 = require("vue");
1968
- var SubscriberScreen = (0, import_vue14.defineComponent)({
2186
+ var import_vue_admin13 = require("@fonderie/vue-admin");
2187
+ var import_vue16 = require("vue");
2188
+ var FILTERS = [
2189
+ { label: "All", match: () => true },
2190
+ { label: "Active", match: (s) => s === "active" },
2191
+ { label: "Trialing", match: (s) => s === "trialing" },
2192
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2193
+ { label: "Canceled", match: (s) => s === "canceled" }
2194
+ ];
2195
+ var SubscriberScreen = (0, import_vue16.defineComponent)({
1969
2196
  name: "FonderieSubscriberScreen",
1970
2197
  props: {
1971
2198
  client: { type: Object, required: true },
1972
- pageSize: { type: Number, default: 50 }
2199
+ pageSize: { type: Number, default: 50 },
2200
+ /**
2201
+ * A user subscriber opens on the Users page, where their plan and credits
2202
+ * live beside their account. Omit to open everything here.
2203
+ */
2204
+ onOpenUser: { type: Function, default: void 0 }
1973
2205
  },
1974
2206
  setup(props) {
1975
- const type = (0, import_vue14.ref)("user");
1976
- const idInput = (0, import_vue14.ref)("");
1977
- const subscriber = (0, import_vue14.ref)(null);
1978
- const list = (0, import_vue_admin12.useAdminSubscribers)(props.client, { limit: props.pageSize });
1979
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin12.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
1980
- const open = (t2, id) => {
1981
- granted.value = null;
1982
- type.value = t2;
1983
- idInput.value = id;
1984
- subscriber.value = { type: t2, id };
2207
+ const list = (0, import_vue_admin13.useAdminSubscribers)(props.client, { limit: props.pageSize });
2208
+ const filter = (0, import_vue16.ref)("All");
2209
+ const open = (0, import_vue16.ref)(null);
2210
+ const type = (0, import_vue16.ref)("workspace");
2211
+ const idInput = (0, import_vue16.ref)("");
2212
+ const openRow = (t2, id) => {
2213
+ if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
2214
+ else open.value = { type: t2, id };
1985
2215
  };
1986
- const amount = (0, import_vue14.ref)("");
1987
- const note = (0, import_vue14.ref)("");
1988
- const granted = (0, import_vue14.ref)(null);
1989
- const input = (model, placeholder, extra = {}) => (0, import_vue14.h)("input", {
1990
- value: model.value,
1991
- onInput: (e) => model.value = e.target.value,
1992
- placeholder,
1993
- style: { ...styles.input, ...extra },
1994
- "aria-label": placeholder
1995
- });
1996
2216
  return () => {
1997
- const s = subscription.value;
1998
- const w = wallet.value;
1999
- return (0, import_vue14.h)("div", { style: styles.container }, [
2000
- pageHeader(
2001
- "Subscribers",
2002
- "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
2003
- ),
2004
- (0, import_vue14.h)(
2005
- "form",
2006
- {
2007
- style: styles.toolbar,
2008
- onSubmit: (e) => {
2009
- e.preventDefault();
2010
- granted.value = null;
2011
- subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
2012
- }
2013
- },
2014
- [
2015
- (0, import_vue14.h)(
2016
- "select",
2017
- {
2018
- value: type.value,
2019
- onChange: (e) => type.value = e.target.value,
2020
- style: styles.input,
2021
- "aria-label": "Type"
2022
- },
2023
- [
2024
- (0, import_vue14.h)("option", { value: "user" }, "user"),
2025
- (0, import_vue14.h)("option", { value: "workspace" }, "workspace")
2026
- ]
2027
- ),
2028
- input(idInput, "subscriber id", { minWidth: "280px" }),
2029
- (0, import_vue14.h)(
2030
- "button",
2031
- { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2032
- [icon("search", 14), "Look up"]
2033
- ),
2034
- subscriber.value ? (0, import_vue14.h)(
2035
- "button",
2036
- {
2037
- type: "button",
2038
- style: styles.button,
2039
- onClick: () => {
2040
- subscriber.value = null;
2041
- idInput.value = "";
2042
- granted.value = null;
2043
- }
2044
- },
2045
- [icon("back", 14), "All subscribers"]
2046
- ) : (0, import_vue14.h)(
2217
+ const o = open.value;
2218
+ if (o) {
2219
+ return (0, import_vue16.h)("div", { style: styles.container }, [
2220
+ pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2221
+ (0, import_vue16.h)(
2047
2222
  "button",
2048
- {
2049
- type: "button",
2050
- style: styles.button,
2051
- disabled: list.isLoading.value,
2052
- onClick: () => void list.refresh()
2053
- },
2054
- "Refresh"
2223
+ { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2224
+ [icon("back", 14), "All subscriptions"]
2055
2225
  )
2056
- ]
2226
+ ]),
2227
+ (0, import_vue16.h)(SubscriberBilling, {
2228
+ key: `${o.type}/${o.id}`,
2229
+ client: props.client,
2230
+ subscriberType: o.type,
2231
+ subscriberId: o.id
2232
+ })
2233
+ ]);
2234
+ }
2235
+ const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2236
+ const rows = list.subscriptions.value.filter((s) => match(s.status));
2237
+ return (0, import_vue16.h)("div", { style: styles.container }, [
2238
+ pageHeader(
2239
+ "Subscriptions",
2240
+ "Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
2241
+ [refreshButton(() => void list.refresh(), list.isLoading.value)]
2057
2242
  ),
2058
- !subscriber.value ? (0, import_vue14.h)("div", [
2059
- list.error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2060
- list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
2061
- "No subscribers yet",
2062
- "Subscriptions appear here once someone checks out.",
2063
- "subscriber"
2064
- ) : table(
2065
- ["Subscriber", "Plan", "Status", "Renews"],
2066
- list.subscriptions.value.map(
2067
- (sub) => (0, import_vue14.h)("tr", { key: sub.id }, [
2068
- td(
2069
- (0, import_vue14.h)(
2070
- "button",
2071
- {
2072
- type: "button",
2073
- style: { ...styles.link, ...styles.mono },
2074
- onClick: () => open(sub.subscriberType, sub.subscriberId)
2075
- },
2076
- `${sub.subscriberType}/${sub.subscriberId}`
2077
- )
2078
- ),
2079
- td(`${sub.plan} \xB7 ${sub.interval}`),
2080
- td([
2081
- pill(
2082
- sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
2083
- sub.status
2084
- ),
2085
- sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
2086
- ]),
2087
- td(
2088
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue14.h)("span", { style: styles.muted }, "\u2014")
2089
- )
2090
- ])
2091
- )
2243
+ (0, import_vue16.h)("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2244
+ (0, import_vue16.h)(
2245
+ "fieldset",
2246
+ {
2247
+ style: {
2248
+ display: "flex",
2249
+ gap: "6px",
2250
+ flexWrap: "wrap",
2251
+ border: "none",
2252
+ margin: 0,
2253
+ padding: 0,
2254
+ minWidth: 0
2255
+ },
2256
+ "aria-label": "Filter by status"
2257
+ },
2258
+ FILTERS.map((f) => {
2259
+ const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2260
+ const on = filter.value === f.label;
2261
+ return (0, import_vue16.h)(
2262
+ "button",
2263
+ {
2264
+ key: f.label,
2265
+ type: "button",
2266
+ "aria-pressed": on,
2267
+ onClick: () => filter.value = f.label,
2268
+ style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2269
+ },
2270
+ [f.label, (0, import_vue16.h)("span", { style: { opacity: 0.7 } }, String(n))]
2271
+ );
2272
+ })
2092
2273
  ),
2093
- list.isLoading.value ? (0, import_vue14.h)("p", { style: styles.status }, "Loading\u2026") : null,
2094
- list.hasMore.value && !list.isLoading.value ? (0, import_vue14.h)(
2095
- "button",
2274
+ (0, import_vue16.h)(
2275
+ "form",
2096
2276
  {
2097
- type: "button",
2098
- style: { ...styles.button, marginTop: "8px" },
2099
- onClick: () => void list.loadMore()
2277
+ style: { display: "flex", gap: "6px" },
2278
+ onSubmit: (e) => {
2279
+ e.preventDefault();
2280
+ if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
2281
+ }
2100
2282
  },
2101
- "Load more"
2102
- ) : null
2103
- ]) : null,
2104
- error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2105
- subscriber.value && !isLoading.value ? [
2106
- (0, import_vue14.h)("h2", { style: styles.subtitle }, "Subscription"),
2107
- s ? (0, import_vue14.h)("table", { style: styles.table }, [
2108
- (0, import_vue14.h)("tbody", [
2109
- (0, import_vue14.h)("tr", [td("plan"), td([(0, import_vue14.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
2110
- (0, import_vue14.h)("tr", [
2111
- td("status"),
2112
- td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
2113
- ]),
2114
- (0, import_vue14.h)("tr", [
2115
- td("period"),
2116
- td(
2117
- `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
2118
- )
2119
- ]),
2120
- (0, import_vue14.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
2121
- ])
2122
- ]) : (0, import_vue14.h)("p", { style: styles.muted }, "No subscription."),
2123
- (0, import_vue14.h)("h2", { style: styles.subtitle }, "Wallet"),
2124
- w ? [
2125
- (0, import_vue14.h)(
2126
- "p",
2127
- {
2128
- style: {
2129
- ...styles.card,
2130
- display: "flex",
2131
- alignItems: "baseline",
2132
- gap: "8px",
2133
- flexWrap: "wrap"
2134
- }
2135
- },
2136
- [
2137
- (0, import_vue14.h)(
2138
- "strong",
2139
- {
2140
- style: {
2141
- fontSize: "22px",
2142
- letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2143
- }
2144
- },
2145
- w.balance
2146
- ),
2147
- ` ${w.currency} `,
2148
- (0, import_vue14.h)(
2149
- "span",
2150
- { style: styles.muted },
2151
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
2152
- )
2153
- ]
2154
- ),
2155
- (0, import_vue14.h)(
2156
- "form",
2157
- {
2158
- style: styles.toolbar,
2159
- onSubmit: (e) => {
2160
- e.preventDefault();
2161
- const sub = subscriber.value;
2162
- if (!sub) return;
2163
- const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
2164
- const amt = amount.value.trim();
2165
- void grant({
2166
- amount: amt,
2167
- ...note.value ? { description: note.value } : {},
2168
- idempotencyKey: key
2169
- }).then(() => {
2170
- granted.value = `Granted ${amt} ${w.currency}.`;
2171
- amount.value = "";
2172
- note.value = "";
2173
- }).catch(() => {
2174
- });
2175
- }
2176
- },
2177
- [
2178
- input(amount, "amount (minor units)"),
2179
- input(note, "reason (optional)", { minWidth: "220px" }),
2180
- (0, import_vue14.h)(
2283
+ [
2284
+ (0, import_vue16.h)(
2285
+ "select",
2286
+ {
2287
+ value: type.value,
2288
+ onChange: (e) => type.value = e.target.value,
2289
+ style: styles.input,
2290
+ "aria-label": "Type"
2291
+ },
2292
+ [
2293
+ (0, import_vue16.h)("option", { value: "workspace" }, "workspace"),
2294
+ (0, import_vue16.h)("option", { value: "user" }, "user")
2295
+ ]
2296
+ ),
2297
+ (0, import_vue16.h)("input", {
2298
+ value: idInput.value,
2299
+ onInput: (e) => idInput.value = e.target.value,
2300
+ placeholder: "subscriber id",
2301
+ style: { ...styles.input, width: "220px" },
2302
+ "aria-label": "Subscriber id"
2303
+ }),
2304
+ (0, import_vue16.h)(
2305
+ "button",
2306
+ { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2307
+ [icon("search", 14), "Open"]
2308
+ )
2309
+ ]
2310
+ )
2311
+ ]),
2312
+ list.error.value ? (0, import_vue16.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2313
+ rows.length === 0 && !list.isLoading.value ? empty(
2314
+ filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2315
+ "Subscriptions appear here once someone checks out.",
2316
+ "subscriber"
2317
+ ) : table(
2318
+ ["Subscriber", "Plan", "Status", "Renews / ends"],
2319
+ rows.map((s) => {
2320
+ const end = periodEnd(s);
2321
+ return (0, import_vue16.h)("tr", { key: s.id }, [
2322
+ td(
2323
+ (0, import_vue16.h)(
2181
2324
  "button",
2182
2325
  {
2183
- type: "submit",
2184
- style: styles.buttonPrimary,
2185
- disabled: !/^\d+$/.test(amount.value.trim())
2326
+ type: "button",
2327
+ style: { ...styles.link, ...styles.mono },
2328
+ onClick: () => openRow(s.subscriberType, s.subscriberId)
2186
2329
  },
2187
- [icon("plus", 14), "Grant"]
2188
- ),
2189
- granted.value ? (0, import_vue14.h)("span", { style: styles.ok }, granted.value) : null
2190
- ]
2191
- ),
2192
- (0, import_vue14.h)("h2", { style: styles.subtitle }, "Ledger"),
2193
- ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
2194
- ["When", "Kind", "Amount", "Note"],
2195
- ledger.value.map(
2196
- (t2) => (0, import_vue14.h)("tr", { key: t2.id }, [
2197
- td(new Date(t2.createdAt).toLocaleString(), styles.muted),
2198
- td(t2.type),
2199
- td(t2.amount, styles.mono),
2200
- td(t2.description ?? "", styles.muted)
2201
- ])
2330
+ `${s.subscriberType}/${s.subscriberId}`
2331
+ )
2332
+ ),
2333
+ td([
2334
+ (0, import_vue16.h)("strong", s.plan),
2335
+ " ",
2336
+ (0, import_vue16.h)("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2337
+ ]),
2338
+ td([
2339
+ pill(statusTone(s.status), s.status),
2340
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? (0, import_vue16.h)("span", [" ", pill("warn", "cancels", false)]) : null
2341
+ ]),
2342
+ td(
2343
+ end ? [(0, import_vue16.h)("span", { style: styles.muted }, end.label), ` ${end.date}`] : (0, import_vue16.h)("span", { style: styles.muted }, "\u2014"),
2344
+ { whiteSpace: "nowrap" }
2202
2345
  )
2203
- ),
2204
- hasMoreLedger.value ? (0, import_vue14.h)(
2205
- "button",
2206
- {
2207
- type: "button",
2208
- style: { ...styles.button, marginTop: "8px" },
2209
- onClick: () => void loadMoreLedger()
2210
- },
2211
- "Load more"
2212
- ) : null
2213
- ] : (0, import_vue14.h)(
2214
- "p",
2215
- { style: styles.muted },
2216
- "No wallet \u2014 billing has no wallet configuration."
2217
- )
2218
- ] : null
2346
+ ]);
2347
+ })
2348
+ ),
2349
+ list.isLoading.value ? (0, import_vue16.h)("p", { style: styles.status }, "Loading\u2026") : null,
2350
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2219
2351
  ]);
2220
2352
  };
2221
2353
  }
2222
2354
  });
2223
2355
 
2224
2356
  // src/screens/AuditScreen.ts
2225
- var import_vue_admin13 = require("@fonderie/vue-admin");
2226
- var import_vue15 = require("vue");
2227
- var AuditScreen = (0, import_vue15.defineComponent)({
2357
+ var import_vue_admin14 = require("@fonderie/vue-admin");
2358
+ var import_vue17 = require("vue");
2359
+ var AuditScreen = (0, import_vue17.defineComponent)({
2228
2360
  name: "FonderieAuditScreen",
2229
2361
  props: {
2230
2362
  client: { type: Object, required: true },
2231
2363
  pageSize: { type: Number, default: 50 }
2232
2364
  },
2233
2365
  setup(props) {
2234
- const draft = (0, import_vue15.ref)({ workspaceId: "", type: "", actorId: "" });
2235
- const query = (0, import_vue15.ref)({ limit: props.pageSize });
2236
- const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin13.useAdminAudit)(
2366
+ const draft = (0, import_vue17.ref)({ workspaceId: "", type: "", actorId: "" });
2367
+ const query = (0, import_vue17.ref)({ limit: props.pageSize });
2368
+ const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin14.useAdminAudit)(
2237
2369
  props.client,
2238
2370
  query
2239
2371
  );
2240
- const field = (key, placeholder) => (0, import_vue15.h)("input", {
2372
+ const field = (key, placeholder) => (0, import_vue17.h)("input", {
2241
2373
  value: draft.value[key],
2242
2374
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
2243
2375
  placeholder,
2244
2376
  style: styles.input,
2245
2377
  "aria-label": placeholder
2246
2378
  });
2247
- return () => (0, import_vue15.h)("div", { style: styles.container }, [
2379
+ return () => (0, import_vue17.h)("div", { style: styles.container }, [
2248
2380
  pageHeader(
2249
2381
  "Audit",
2250
2382
  "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2251
2383
  ),
2252
- (0, import_vue15.h)(
2384
+ (0, import_vue17.h)(
2253
2385
  "form",
2254
2386
  {
2255
2387
  style: styles.toolbar,
@@ -2268,19 +2400,19 @@ var AuditScreen = (0, import_vue15.defineComponent)({
2268
2400
  field("workspaceId", "workspace id (all if empty)"),
2269
2401
  field("type", "event type"),
2270
2402
  field("actorId", "actor id"),
2271
- (0, import_vue15.h)(
2403
+ (0, import_vue17.h)(
2272
2404
  "button",
2273
2405
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2274
2406
  [icon("search", 14), "Filter"]
2275
2407
  ),
2276
- refreshButton("Refresh", isLoading.value, () => void refresh())
2408
+ refreshButton2("Refresh", isLoading.value, () => void refresh())
2277
2409
  ]
2278
2410
  ),
2279
- error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2411
+ error.value ? (0, import_vue17.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2280
2412
  events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2281
2413
  ["When", "Type", "Workspace", "Actor", "Request"],
2282
2414
  events.value.map(
2283
- (e) => (0, import_vue15.h)("tr", { key: e.id }, [
2415
+ (e) => (0, import_vue17.h)("tr", { key: e.id }, [
2284
2416
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
2285
2417
  td(e.type, styles.mono),
2286
2418
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -2289,7 +2421,7 @@ var AuditScreen = (0, import_vue15.defineComponent)({
2289
2421
  ])
2290
2422
  )
2291
2423
  ),
2292
- isLoading.value ? (0, import_vue15.h)("p", { style: styles.status }, "Loading\u2026") : null,
2424
+ isLoading.value ? (0, import_vue17.h)("p", { style: styles.status }, "Loading\u2026") : null,
2293
2425
  hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2294
2426
  ]);
2295
2427
  }
@@ -2312,7 +2444,7 @@ var NAV = [
2312
2444
  group: "Money",
2313
2445
  items: [
2314
2446
  { page: "catalog", label: "Catalog", needs: "billing" },
2315
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2447
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2316
2448
  ]
2317
2449
  },
2318
2450
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2346,7 +2478,7 @@ var ICON = {
2346
2478
  migrations: "migrations"
2347
2479
  };
2348
2480
  var NARROW = "(max-width: 820px)";
2349
- var AdminShell = (0, import_vue16.defineComponent)({
2481
+ var AdminShell = (0, import_vue18.defineComponent)({
2350
2482
  name: "FonderieAdminShell",
2351
2483
  props: {
2352
2484
  client: { type: Object, required: true },
@@ -2380,60 +2512,74 @@ var AdminShell = (0, import_vue16.defineComponent)({
2380
2512
  emits: { navigate: (_page) => true },
2381
2513
  setup(props, { emit, slots }) {
2382
2514
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2383
- const narrow = (0, import_vue16.ref)(query?.matches ?? false);
2384
- const drawer = (0, import_vue16.ref)(false);
2515
+ const narrow = (0, import_vue18.ref)(query?.matches ?? false);
2516
+ const drawer = (0, import_vue18.ref)(false);
2385
2517
  const onMedia = (e) => narrow.value = e.matches;
2386
- (0, import_vue16.onMounted)(() => query?.addEventListener("change", onMedia));
2387
- (0, import_vue16.onBeforeUnmount)(() => query?.removeEventListener("change", onMedia));
2388
- const own = (0, import_vue16.ref)("attention");
2389
- const current = (0, import_vue16.computed)(() => props.page ?? own.value);
2390
- const editing = (0, import_vue16.ref)(null);
2518
+ (0, import_vue18.onMounted)(() => query?.addEventListener("change", onMedia));
2519
+ (0, import_vue18.onBeforeUnmount)(() => query?.removeEventListener("change", onMedia));
2520
+ const own = (0, import_vue18.ref)("attention");
2521
+ const current = (0, import_vue18.computed)(() => props.page ?? own.value);
2522
+ const editing = (0, import_vue18.ref)(null);
2523
+ const openUser = (0, import_vue18.ref)(void 0);
2391
2524
  const go = (p) => {
2392
2525
  editing.value = null;
2393
2526
  emit("navigate", p);
2394
2527
  if (props.page === void 0) own.value = p;
2395
2528
  drawer.value = false;
2396
2529
  };
2397
- const back = (label) => (0, import_vue16.h)("div", { style: { padding: "24px 40px 0" } }, [
2398
- (0, import_vue16.h)(
2530
+ const back = (label) => (0, import_vue18.h)("div", { style: { padding: "24px 40px 0" } }, [
2531
+ (0, import_vue18.h)(
2399
2532
  "button",
2400
2533
  { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2401
2534
  [icon("back", 14), label]
2402
2535
  )
2403
2536
  ]);
2404
- const missing = (text) => (0, import_vue16.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2537
+ const missing = (text) => (0, import_vue18.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2405
2538
  const body = () => {
2406
2539
  const c = props.client;
2407
2540
  switch (current.value) {
2408
2541
  case "attention":
2409
- return (0, import_vue16.h)(AttentionScreen, { client: c });
2542
+ return (0, import_vue18.h)(AttentionScreen, { client: c });
2410
2543
  case "modules":
2411
- return (0, import_vue16.h)(ModulesScreen, { client: c });
2544
+ return (0, import_vue18.h)(ModulesScreen, { client: c });
2412
2545
  case "doctor":
2413
- return (0, import_vue16.h)(DoctorScreen, { client: c });
2546
+ return (0, import_vue18.h)(DoctorScreen, { client: c });
2414
2547
  case "environment":
2415
- return (0, import_vue16.h)(EnvironmentScreen, { client: c });
2548
+ return (0, import_vue18.h)(EnvironmentScreen, { client: c });
2416
2549
  case "routes":
2417
- return (0, import_vue16.h)(RoutesScreen, { client: c });
2550
+ return (0, import_vue18.h)(RoutesScreen, { client: c });
2418
2551
  case "tokens":
2419
- return (0, import_vue16.h)(TokensScreen, { client: c });
2552
+ return (0, import_vue18.h)(TokensScreen, { client: c });
2420
2553
  case "operators":
2421
- return (0, import_vue16.h)(OperatorsScreen, {
2554
+ return (0, import_vue18.h)(OperatorsScreen, {
2422
2555
  client: c,
2423
2556
  ...props.currentOperator ? { me: props.currentOperator } : {}
2424
2557
  });
2425
2558
  case "migrations":
2426
- return (0, import_vue16.h)(MigrationsScreen, { client: c });
2559
+ return (0, import_vue18.h)(MigrationsScreen, { client: c });
2427
2560
  case "log":
2428
- return (0, import_vue16.h)(AdminLogScreen, { client: c });
2561
+ return (0, import_vue18.h)(AdminLogScreen, { client: c });
2429
2562
  case "catalog":
2430
- return props.billingClient ? (0, import_vue16.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2563
+ return props.billingClient ? (0, import_vue18.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2431
2564
  case "subscriber":
2432
- return props.billingClient ? (0, import_vue16.h)(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2565
+ return props.billingClient ? (0, import_vue18.h)(SubscriberScreen, {
2566
+ client: props.billingClient,
2567
+ ...props.authClient ? {
2568
+ onOpenUser: (id) => {
2569
+ openUser.value = id;
2570
+ go("users");
2571
+ }
2572
+ } : {}
2573
+ }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2433
2574
  case "audit":
2434
- return props.auditClient ? (0, import_vue16.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2575
+ return props.auditClient ? (0, import_vue18.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2435
2576
  case "users":
2436
- return props.authClient ? (0, import_vue16.h)(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
2577
+ return props.authClient ? (0, import_vue18.h)(UsersScreen, {
2578
+ key: openUser.value ?? "list",
2579
+ client: props.authClient,
2580
+ ...props.billingClient ? { billingClient: props.billingClient } : {},
2581
+ ...openUser.value ? { openUserId: openUser.value } : {}
2582
+ }) : missing("Pass an AuthAdminClient to look up users here.");
2437
2583
  case "settings": {
2438
2584
  const cc = props.configClient;
2439
2585
  if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
@@ -2441,7 +2587,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2441
2587
  if (e && e.kind !== "template")
2442
2588
  return [
2443
2589
  back("Config & secrets"),
2444
- (0, import_vue16.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2590
+ (0, import_vue18.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2445
2591
  client: cc,
2446
2592
  kind: e.kind,
2447
2593
  configKey: e.key,
@@ -2449,7 +2595,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2449
2595
  onSaved: () => editing.value = null
2450
2596
  })
2451
2597
  ];
2452
- return (0, import_vue16.h)(import_vue_config_admin_screens.ConfigListScreen, {
2598
+ return (0, import_vue18.h)(import_vue_config_admin_screens.ConfigListScreen, {
2453
2599
  client: cc,
2454
2600
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2455
2601
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -2466,13 +2612,13 @@ var AdminShell = (0, import_vue16.defineComponent)({
2466
2612
  if (e?.kind === "template")
2467
2613
  return [
2468
2614
  back("Templates"),
2469
- (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2615
+ (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2470
2616
  client: kc,
2471
2617
  type: e.type,
2472
2618
  onSaved: () => editing.value = null
2473
2619
  })
2474
2620
  ];
2475
- return (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2621
+ return (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2476
2622
  client: kc,
2477
2623
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
2478
2624
  });
@@ -2482,11 +2628,11 @@ var AdminShell = (0, import_vue16.defineComponent)({
2482
2628
  const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2483
2629
  const brand = () => {
2484
2630
  const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2485
- return (0, import_vue16.h)("div", { style: styles.navBrand }, [
2486
- (0, import_vue16.h)("div", { style: styles.navMark }, [icon("mark", 15)]),
2487
- (0, import_vue16.h)("div", { style: { minWidth: 0, flex: 1 } }, [
2488
- (0, import_vue16.h)("div", { style: styles.navBrandTitle }, props.appName),
2489
- props.envLabel ? (0, import_vue16.h)(
2631
+ return (0, import_vue18.h)("div", { style: styles.navBrand }, [
2632
+ (0, import_vue18.h)("div", { style: styles.navMark }, [icon("mark", 15)]),
2633
+ (0, import_vue18.h)("div", { style: { minWidth: 0, flex: 1 } }, [
2634
+ (0, import_vue18.h)("div", { style: styles.navBrandTitle }, props.appName),
2635
+ props.envLabel ? (0, import_vue18.h)(
2490
2636
  "span",
2491
2637
  {
2492
2638
  style: {
@@ -2503,7 +2649,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2503
2649
  props.envLabel
2504
2650
  ) : null
2505
2651
  ]),
2506
- narrow.value ? (0, import_vue16.h)(
2652
+ narrow.value ? (0, import_vue18.h)(
2507
2653
  "button",
2508
2654
  {
2509
2655
  type: "button",
@@ -2515,7 +2661,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2515
2661
  ) : null
2516
2662
  ]);
2517
2663
  };
2518
- const nav = () => (0, import_vue16.h)(
2664
+ const nav = () => (0, import_vue18.h)(
2519
2665
  "nav",
2520
2666
  {
2521
2667
  style: {
@@ -2526,17 +2672,17 @@ var AdminShell = (0, import_vue16.defineComponent)({
2526
2672
  },
2527
2673
  [
2528
2674
  brand(),
2529
- (0, import_vue16.h)(
2675
+ (0, import_vue18.h)(
2530
2676
  "div",
2531
2677
  { style: styles.navScroll },
2532
2678
  NAV.map((g) => {
2533
2679
  const items = g.items.filter(visible);
2534
2680
  if (items.length === 0) return null;
2535
- return (0, import_vue16.h)("div", { key: g.group }, [
2536
- (0, import_vue16.h)("div", { style: styles.navGroup }, g.group),
2681
+ return (0, import_vue18.h)("div", { key: g.group }, [
2682
+ (0, import_vue18.h)("div", { style: styles.navGroup }, g.group),
2537
2683
  ...items.map((i) => {
2538
2684
  const active = current.value === i.page;
2539
- return (0, import_vue16.h)(
2685
+ return (0, import_vue18.h)(
2540
2686
  "button",
2541
2687
  {
2542
2688
  key: i.page,
@@ -2544,7 +2690,10 @@ var AdminShell = (0, import_vue16.defineComponent)({
2544
2690
  class: "fonderie-admin-nav-item",
2545
2691
  style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2546
2692
  "aria-current": active ? "page" : void 0,
2547
- onClick: () => go(i.page)
2693
+ onClick: () => {
2694
+ openUser.value = void 0;
2695
+ go(i.page);
2696
+ }
2548
2697
  },
2549
2698
  [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2550
2699
  );
@@ -2552,18 +2701,18 @@ var AdminShell = (0, import_vue16.defineComponent)({
2552
2701
  ]);
2553
2702
  })
2554
2703
  ),
2555
- slots["footer"] ? (0, import_vue16.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2704
+ slots["footer"] ? (0, import_vue18.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2556
2705
  ]
2557
2706
  );
2558
2707
  return () => {
2559
2708
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2560
- return (0, import_vue16.h)(
2709
+ return (0, import_vue18.h)(
2561
2710
  "div",
2562
2711
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2563
2712
  [
2564
2713
  ...narrow.value ? [
2565
- (0, import_vue16.h)("div", { style: styles.topbar }, [
2566
- (0, import_vue16.h)(
2714
+ (0, import_vue18.h)("div", { style: styles.topbar }, [
2715
+ (0, import_vue18.h)(
2567
2716
  "button",
2568
2717
  {
2569
2718
  type: "button",
@@ -2574,19 +2723,19 @@ var AdminShell = (0, import_vue16.defineComponent)({
2574
2723
  },
2575
2724
  [icon("menu")]
2576
2725
  ),
2577
- (0, import_vue16.h)("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2578
- props.envLabel ? (0, import_vue16.h)("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2726
+ (0, import_vue18.h)("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2727
+ props.envLabel ? (0, import_vue18.h)("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2579
2728
  ]),
2580
- drawer.value ? (0, import_vue16.h)("div", { style: styles.drawer }, [
2729
+ drawer.value ? (0, import_vue18.h)("div", { style: styles.drawer }, [
2581
2730
  nav(),
2582
- (0, import_vue16.h)("div", {
2731
+ (0, import_vue18.h)("div", {
2583
2732
  style: styles.scrim,
2584
2733
  "aria-hidden": "true",
2585
2734
  onClick: () => drawer.value = false
2586
2735
  })
2587
2736
  ]) : null
2588
2737
  ] : [nav()],
2589
- (0, import_vue16.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2738
+ (0, import_vue18.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2590
2739
  ]
2591
2740
  );
2592
2741
  };
@@ -2605,6 +2754,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2605
2754
  ModulesScreen,
2606
2755
  OperatorsScreen,
2607
2756
  RoutesScreen,
2757
+ SubscriberBilling,
2608
2758
  SubscriberScreen,
2609
2759
  TokensScreen,
2610
2760
  UsersScreen