@fonderie/vue-admin-screens 1.2.0 → 1.3.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/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,42 @@ 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 && !(error.value.status === 404 && selectedId.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,
2019
+ // Opened by id (e.g. from Subscriptions) and the account is gone —
2020
+ // deleted or purged — while its billing rows remain. Say so, and
2021
+ // still show the money.
2022
+ error.value?.status === 404 && selectedId.value ? [
2023
+ (0, import_vue14.h)("div", { style: styles.notice, role: "status" }, [
2024
+ (0, import_vue14.h)("strong", "No account with id "),
2025
+ (0, import_vue14.h)("code", { style: styles.code }, selectedId.value),
2026
+ (0, import_vue14.h)("strong", "."),
2027
+ " It was deleted, or never existed here. Its billing records remain."
2028
+ ]),
2029
+ props.billingClient ? (0, import_vue14.h)("h2", { style: styles.subtitle }, "Plan & credits") : null,
2030
+ props.billingClient ? (0, import_vue14.h)(SubscriberBilling, {
2031
+ key: `missing-${selectedId.value}`,
2032
+ client: props.billingClient,
2033
+ subscriberType: "user",
2034
+ subscriberId: selectedId.value
2035
+ }) : null
2036
+ ] : null,
1806
2037
  u ? [
1807
- (0, import_vue12.h)("h2", { style: styles.subtitle }, [
2038
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, [
1808
2039
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
1809
2040
  " ",
1810
2041
  u.suspended ? pill("warn", "suspended") : null,
1811
2042
  u.deletedAt ? pill("neutral", "deleted") : null
1812
2043
  ]),
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)),
2044
+ (0, import_vue14.h)("table", { style: styles.table }, [
2045
+ (0, import_vue14.h)("tbody", [
2046
+ row("id", (0, import_vue14.h)("span", { style: styles.mono }, u.id)),
1816
2047
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
1817
2048
  row("MFA", yesNo(u.mfaEnabled)),
1818
2049
  row(
@@ -1821,12 +2052,12 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1821
2052
  ),
1822
2053
  row(
1823
2054
  "last login",
1824
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue12.h)("span", { style: styles.muted }, "never")
2055
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue14.h)("span", { style: styles.muted }, "never")
1825
2056
  ),
1826
2057
  row("created", new Date(u.createdAt).toLocaleString())
1827
2058
  ])
1828
2059
  ]),
1829
- (0, import_vue12.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2060
+ (0, import_vue14.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1830
2061
  u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1831
2062
  "Suspend",
1832
2063
  isLoading.value,
@@ -1839,19 +2070,26 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1839
2070
  () => void revokeSessions().then(() => sessions.refresh())
1840
2071
  )
1841
2072
  ]),
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)(
2073
+ props.billingClient ? (0, import_vue14.h)("h2", { style: styles.subtitle }, "Plan & credits") : null,
2074
+ props.billingClient ? (0, import_vue14.h)(SubscriberBilling, {
2075
+ key: u.id,
2076
+ client: props.billingClient,
2077
+ subscriberType: "user",
2078
+ subscriberId: u.id
2079
+ }) : null,
2080
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Live sessions"),
2081
+ sessions.sessions.value.length === 0 ? (0, import_vue14.h)("p", { style: styles.muted }, "None.") : (0, import_vue14.h)(
1844
2082
  "ul",
1845
2083
  { style: styles.list },
1846
2084
  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"),
2085
+ (s) => (0, import_vue14.h)("li", { key: s.id, style: styles.row }, [
2086
+ (0, import_vue14.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1849
2087
  " ",
1850
- (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
2088
+ (0, import_vue14.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
1851
2089
  " ",
1852
- (0, import_vue12.h)("span", { style: styles.muted }, s.userAgent ?? ""),
2090
+ (0, import_vue14.h)("span", { style: styles.muted }, s.userAgent ?? ""),
1853
2091
  " ",
1854
- (0, import_vue12.h)(
2092
+ (0, import_vue14.h)(
1855
2093
  "span",
1856
2094
  { style: styles.muted },
1857
2095
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -1859,22 +2097,22 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1859
2097
  ])
1860
2098
  )
1861
2099
  ),
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)(
2100
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
2101
+ history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue14.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue14.h)(
1864
2102
  "ul",
1865
2103
  { style: styles.list },
1866
2104
  history.events.value.map(
1867
- (e) => (0, import_vue12.h)("li", { key: e.id, style: styles.row }, [
2105
+ (e) => (0, import_vue14.h)("li", { key: e.id, style: styles.row }, [
1868
2106
  pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
1869
2107
  " ",
1870
- (0, import_vue12.h)("span", { style: styles.muted }, e.method),
2108
+ (0, import_vue14.h)("span", { style: styles.muted }, e.method),
1871
2109
  " ",
1872
- (0, import_vue12.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
2110
+ (0, import_vue14.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1873
2111
  " ",
1874
- (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
2112
+ (0, import_vue14.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
1875
2113
  e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
1876
2114
  " ",
1877
- (0, import_vue12.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
2115
+ (0, import_vue14.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1878
2116
  ])
1879
2117
  )
1880
2118
  ),
@@ -1886,27 +2124,27 @@ var UsersScreen = (0, import_vue12.defineComponent)({
1886
2124
  });
1887
2125
 
1888
2126
  // 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)({
2127
+ var import_vue_admin12 = require("@fonderie/vue-admin");
2128
+ var import_vue15 = require("vue");
2129
+ var CatalogScreen = (0, import_vue15.defineComponent)({
1892
2130
  name: "FonderieCatalogScreen",
1893
2131
  props: { client: { type: Object, required: true } },
1894
2132
  setup(props) {
1895
- const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin11.useAdminCatalog)(props.client);
2133
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin12.useAdminCatalog)(props.client);
1896
2134
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1897
2135
  return () => {
1898
2136
  const c = catalog.value;
1899
- return (0, import_vue13.h)("div", { style: styles.container }, [
2137
+ return (0, import_vue15.h)("div", { style: styles.container }, [
1900
2138
  pageHeader(
1901
2139
  "Catalog",
1902
2140
  "What you sell: plans as configured in code, and as stored in the database, side by side.",
1903
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
2141
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
1904
2142
  ),
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,
2143
+ error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2144
+ isLoading.value && !c ? (0, import_vue15.h)("p", { style: styles.status }, "Loading\u2026") : null,
1907
2145
  c ? [
1908
- (0, import_vue13.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1909
- (0, import_vue13.h)(
2146
+ (0, import_vue15.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2147
+ (0, import_vue15.h)(
1910
2148
  "pre",
1911
2149
  {
1912
2150
  style: {
@@ -1920,7 +2158,7 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1920
2158
  },
1921
2159
  JSON.stringify(c.configured, null, 2)
1922
2160
  ),
1923
- (0, import_vue13.h)("h2", { style: styles.subtitle }, "Stored (database)"),
2161
+ (0, import_vue15.h)("h2", { style: styles.subtitle }, "Stored (database)"),
1924
2162
  c.stored.length === 0 ? empty(
1925
2163
  "No stored plans",
1926
2164
  "Plans are configured in code; nothing has been written to the database.",
@@ -1928,10 +2166,10 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1928
2166
  ) : table(
1929
2167
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
1930
2168
  c.stored.map(
1931
- (p) => (0, import_vue13.h)("tr", { key: p.id }, [
2169
+ (p) => (0, import_vue15.h)("tr", { key: p.id }, [
1932
2170
  td([
1933
- (0, import_vue13.h)("strong", p.name),
1934
- p.description ? (0, import_vue13.h)("div", { style: styles.muted }, p.description) : ""
2171
+ (0, import_vue15.h)("strong", p.name),
2172
+ p.description ? (0, import_vue15.h)("div", { style: styles.muted }, p.description) : ""
1935
2173
  ]),
1936
2174
  td(String(p.tier)),
1937
2175
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -1939,7 +2177,7 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1939
2177
  td(money(p.pricing?.yearly)),
1940
2178
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
1941
2179
  td(
1942
- (0, import_vue13.h)(
2180
+ (0, import_vue15.h)(
1943
2181
  "button",
1944
2182
  {
1945
2183
  type: "button",
@@ -1963,293 +2201,205 @@ var CatalogScreen = (0, import_vue13.defineComponent)({
1963
2201
  });
1964
2202
 
1965
2203
  // 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)({
2204
+ var import_vue_admin13 = require("@fonderie/vue-admin");
2205
+ var import_vue16 = require("vue");
2206
+ var FILTERS = [
2207
+ { label: "All", match: () => true },
2208
+ { label: "Active", match: (s) => s === "active" },
2209
+ { label: "Trialing", match: (s) => s === "trialing" },
2210
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2211
+ { label: "Canceled", match: (s) => s === "canceled" }
2212
+ ];
2213
+ var SubscriberScreen = (0, import_vue16.defineComponent)({
1969
2214
  name: "FonderieSubscriberScreen",
1970
2215
  props: {
1971
2216
  client: { type: Object, required: true },
1972
- pageSize: { type: Number, default: 50 }
2217
+ pageSize: { type: Number, default: 50 },
2218
+ /**
2219
+ * A user subscriber opens on the Users page, where their plan and credits
2220
+ * live beside their account. Omit to open everything here.
2221
+ */
2222
+ onOpenUser: { type: Function, default: void 0 }
1973
2223
  },
1974
2224
  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 };
2225
+ const list = (0, import_vue_admin13.useAdminSubscribers)(props.client, { limit: props.pageSize });
2226
+ const filter = (0, import_vue16.ref)("All");
2227
+ const open = (0, import_vue16.ref)(null);
2228
+ const type = (0, import_vue16.ref)("workspace");
2229
+ const idInput = (0, import_vue16.ref)("");
2230
+ const openRow = (t2, id) => {
2231
+ if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
2232
+ else open.value = { type: t2, id };
1985
2233
  };
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
2234
  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)(
2235
+ const o = open.value;
2236
+ if (o) {
2237
+ return (0, import_vue16.h)("div", { style: styles.container }, [
2238
+ pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2239
+ (0, import_vue16.h)(
2030
2240
  "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)(
2047
- "button",
2048
- {
2049
- type: "button",
2050
- style: styles.button,
2051
- disabled: list.isLoading.value,
2052
- onClick: () => void list.refresh()
2053
- },
2054
- "Refresh"
2241
+ { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2242
+ [icon("back", 14), "All subscriptions"]
2055
2243
  )
2056
- ]
2244
+ ]),
2245
+ (0, import_vue16.h)(SubscriberBilling, {
2246
+ key: `${o.type}/${o.id}`,
2247
+ client: props.client,
2248
+ subscriberType: o.type,
2249
+ subscriberId: o.id
2250
+ })
2251
+ ]);
2252
+ }
2253
+ const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2254
+ const rows = list.subscriptions.value.filter((s) => match(s.status));
2255
+ return (0, import_vue16.h)("div", { style: styles.container }, [
2256
+ pageHeader(
2257
+ "Subscriptions",
2258
+ "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.",
2259
+ [refreshButton(() => void list.refresh(), list.isLoading.value)]
2057
2260
  ),
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
- )
2261
+ (0, import_vue16.h)("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2262
+ (0, import_vue16.h)(
2263
+ "fieldset",
2264
+ {
2265
+ style: {
2266
+ display: "flex",
2267
+ gap: "6px",
2268
+ flexWrap: "wrap",
2269
+ border: "none",
2270
+ margin: 0,
2271
+ padding: 0,
2272
+ minWidth: 0
2273
+ },
2274
+ "aria-label": "Filter by status"
2275
+ },
2276
+ FILTERS.map((f) => {
2277
+ const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2278
+ const on = filter.value === f.label;
2279
+ return (0, import_vue16.h)(
2280
+ "button",
2281
+ {
2282
+ key: f.label,
2283
+ type: "button",
2284
+ "aria-pressed": on,
2285
+ onClick: () => filter.value = f.label,
2286
+ style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2287
+ },
2288
+ [f.label, (0, import_vue16.h)("span", { style: { opacity: 0.7 } }, String(n))]
2289
+ );
2290
+ })
2092
2291
  ),
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",
2292
+ (0, import_vue16.h)(
2293
+ "form",
2096
2294
  {
2097
- type: "button",
2098
- style: { ...styles.button, marginTop: "8px" },
2099
- onClick: () => void list.loadMore()
2295
+ style: { display: "flex", gap: "6px" },
2296
+ onSubmit: (e) => {
2297
+ e.preventDefault();
2298
+ if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
2299
+ }
2100
2300
  },
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)(
2301
+ [
2302
+ (0, import_vue16.h)(
2303
+ "select",
2304
+ {
2305
+ value: type.value,
2306
+ onChange: (e) => type.value = e.target.value,
2307
+ style: styles.input,
2308
+ "aria-label": "Type"
2309
+ },
2310
+ [
2311
+ (0, import_vue16.h)("option", { value: "workspace" }, "workspace"),
2312
+ (0, import_vue16.h)("option", { value: "user" }, "user")
2313
+ ]
2314
+ ),
2315
+ (0, import_vue16.h)("input", {
2316
+ value: idInput.value,
2317
+ onInput: (e) => idInput.value = e.target.value,
2318
+ placeholder: "subscriber id",
2319
+ style: { ...styles.input, width: "220px" },
2320
+ "aria-label": "Subscriber id"
2321
+ }),
2322
+ (0, import_vue16.h)(
2323
+ "button",
2324
+ { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2325
+ [icon("search", 14), "Open"]
2326
+ )
2327
+ ]
2328
+ )
2329
+ ]),
2330
+ list.error.value ? (0, import_vue16.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2331
+ rows.length === 0 && !list.isLoading.value ? empty(
2332
+ filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2333
+ "Subscriptions appear here once someone checks out.",
2334
+ "subscriber"
2335
+ ) : table(
2336
+ ["Subscriber", "Plan", "Status", "Renews / ends"],
2337
+ rows.map((s) => {
2338
+ const end = periodEnd(s);
2339
+ return (0, import_vue16.h)("tr", { key: s.id }, [
2340
+ td(
2341
+ (0, import_vue16.h)(
2181
2342
  "button",
2182
2343
  {
2183
- type: "submit",
2184
- style: styles.buttonPrimary,
2185
- disabled: !/^\d+$/.test(amount.value.trim())
2344
+ type: "button",
2345
+ style: { ...styles.link, ...styles.mono },
2346
+ onClick: () => openRow(s.subscriberType, s.subscriberId)
2186
2347
  },
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
- ])
2348
+ `${s.subscriberType}/${s.subscriberId}`
2349
+ )
2350
+ ),
2351
+ td([
2352
+ (0, import_vue16.h)("strong", s.plan),
2353
+ " ",
2354
+ (0, import_vue16.h)("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2355
+ ]),
2356
+ td([
2357
+ pill(statusTone(s.status), s.status),
2358
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? (0, import_vue16.h)("span", [" ", pill("warn", "cancels", false)]) : null
2359
+ ]),
2360
+ td(
2361
+ end ? [(0, import_vue16.h)("span", { style: styles.muted }, end.label), ` ${end.date}`] : (0, import_vue16.h)("span", { style: styles.muted }, "\u2014"),
2362
+ { whiteSpace: "nowrap" }
2202
2363
  )
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
2364
+ ]);
2365
+ })
2366
+ ),
2367
+ list.isLoading.value ? (0, import_vue16.h)("p", { style: styles.status }, "Loading\u2026") : null,
2368
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2219
2369
  ]);
2220
2370
  };
2221
2371
  }
2222
2372
  });
2223
2373
 
2224
2374
  // 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)({
2375
+ var import_vue_admin14 = require("@fonderie/vue-admin");
2376
+ var import_vue17 = require("vue");
2377
+ var AuditScreen = (0, import_vue17.defineComponent)({
2228
2378
  name: "FonderieAuditScreen",
2229
2379
  props: {
2230
2380
  client: { type: Object, required: true },
2231
2381
  pageSize: { type: Number, default: 50 }
2232
2382
  },
2233
2383
  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)(
2384
+ const draft = (0, import_vue17.ref)({ workspaceId: "", type: "", actorId: "" });
2385
+ const query = (0, import_vue17.ref)({ limit: props.pageSize });
2386
+ const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin14.useAdminAudit)(
2237
2387
  props.client,
2238
2388
  query
2239
2389
  );
2240
- const field = (key, placeholder) => (0, import_vue15.h)("input", {
2390
+ const field = (key, placeholder) => (0, import_vue17.h)("input", {
2241
2391
  value: draft.value[key],
2242
2392
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
2243
2393
  placeholder,
2244
2394
  style: styles.input,
2245
2395
  "aria-label": placeholder
2246
2396
  });
2247
- return () => (0, import_vue15.h)("div", { style: styles.container }, [
2397
+ return () => (0, import_vue17.h)("div", { style: styles.container }, [
2248
2398
  pageHeader(
2249
2399
  "Audit",
2250
2400
  "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2251
2401
  ),
2252
- (0, import_vue15.h)(
2402
+ (0, import_vue17.h)(
2253
2403
  "form",
2254
2404
  {
2255
2405
  style: styles.toolbar,
@@ -2268,19 +2418,19 @@ var AuditScreen = (0, import_vue15.defineComponent)({
2268
2418
  field("workspaceId", "workspace id (all if empty)"),
2269
2419
  field("type", "event type"),
2270
2420
  field("actorId", "actor id"),
2271
- (0, import_vue15.h)(
2421
+ (0, import_vue17.h)(
2272
2422
  "button",
2273
2423
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2274
2424
  [icon("search", 14), "Filter"]
2275
2425
  ),
2276
- refreshButton("Refresh", isLoading.value, () => void refresh())
2426
+ refreshButton2("Refresh", isLoading.value, () => void refresh())
2277
2427
  ]
2278
2428
  ),
2279
- error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2429
+ error.value ? (0, import_vue17.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2280
2430
  events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2281
2431
  ["When", "Type", "Workspace", "Actor", "Request"],
2282
2432
  events.value.map(
2283
- (e) => (0, import_vue15.h)("tr", { key: e.id }, [
2433
+ (e) => (0, import_vue17.h)("tr", { key: e.id }, [
2284
2434
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
2285
2435
  td(e.type, styles.mono),
2286
2436
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -2289,7 +2439,7 @@ var AuditScreen = (0, import_vue15.defineComponent)({
2289
2439
  ])
2290
2440
  )
2291
2441
  ),
2292
- isLoading.value ? (0, import_vue15.h)("p", { style: styles.status }, "Loading\u2026") : null,
2442
+ isLoading.value ? (0, import_vue17.h)("p", { style: styles.status }, "Loading\u2026") : null,
2293
2443
  hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2294
2444
  ]);
2295
2445
  }
@@ -2312,7 +2462,7 @@ var NAV = [
2312
2462
  group: "Money",
2313
2463
  items: [
2314
2464
  { page: "catalog", label: "Catalog", needs: "billing" },
2315
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2465
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2316
2466
  ]
2317
2467
  },
2318
2468
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2346,7 +2496,7 @@ var ICON = {
2346
2496
  migrations: "migrations"
2347
2497
  };
2348
2498
  var NARROW = "(max-width: 820px)";
2349
- var AdminShell = (0, import_vue16.defineComponent)({
2499
+ var AdminShell = (0, import_vue18.defineComponent)({
2350
2500
  name: "FonderieAdminShell",
2351
2501
  props: {
2352
2502
  client: { type: Object, required: true },
@@ -2380,60 +2530,74 @@ var AdminShell = (0, import_vue16.defineComponent)({
2380
2530
  emits: { navigate: (_page) => true },
2381
2531
  setup(props, { emit, slots }) {
2382
2532
  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);
2533
+ const narrow = (0, import_vue18.ref)(query?.matches ?? false);
2534
+ const drawer = (0, import_vue18.ref)(false);
2385
2535
  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);
2536
+ (0, import_vue18.onMounted)(() => query?.addEventListener("change", onMedia));
2537
+ (0, import_vue18.onBeforeUnmount)(() => query?.removeEventListener("change", onMedia));
2538
+ const own = (0, import_vue18.ref)("attention");
2539
+ const current = (0, import_vue18.computed)(() => props.page ?? own.value);
2540
+ const editing = (0, import_vue18.ref)(null);
2541
+ const openUser = (0, import_vue18.ref)(void 0);
2391
2542
  const go = (p) => {
2392
2543
  editing.value = null;
2393
2544
  emit("navigate", p);
2394
2545
  if (props.page === void 0) own.value = p;
2395
2546
  drawer.value = false;
2396
2547
  };
2397
- const back = (label) => (0, import_vue16.h)("div", { style: { padding: "24px 40px 0" } }, [
2398
- (0, import_vue16.h)(
2548
+ const back = (label) => (0, import_vue18.h)("div", { style: { padding: "24px 40px 0" } }, [
2549
+ (0, import_vue18.h)(
2399
2550
  "button",
2400
2551
  { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2401
2552
  [icon("back", 14), label]
2402
2553
  )
2403
2554
  ]);
2404
- const missing = (text) => (0, import_vue16.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2555
+ const missing = (text) => (0, import_vue18.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2405
2556
  const body = () => {
2406
2557
  const c = props.client;
2407
2558
  switch (current.value) {
2408
2559
  case "attention":
2409
- return (0, import_vue16.h)(AttentionScreen, { client: c });
2560
+ return (0, import_vue18.h)(AttentionScreen, { client: c });
2410
2561
  case "modules":
2411
- return (0, import_vue16.h)(ModulesScreen, { client: c });
2562
+ return (0, import_vue18.h)(ModulesScreen, { client: c });
2412
2563
  case "doctor":
2413
- return (0, import_vue16.h)(DoctorScreen, { client: c });
2564
+ return (0, import_vue18.h)(DoctorScreen, { client: c });
2414
2565
  case "environment":
2415
- return (0, import_vue16.h)(EnvironmentScreen, { client: c });
2566
+ return (0, import_vue18.h)(EnvironmentScreen, { client: c });
2416
2567
  case "routes":
2417
- return (0, import_vue16.h)(RoutesScreen, { client: c });
2568
+ return (0, import_vue18.h)(RoutesScreen, { client: c });
2418
2569
  case "tokens":
2419
- return (0, import_vue16.h)(TokensScreen, { client: c });
2570
+ return (0, import_vue18.h)(TokensScreen, { client: c });
2420
2571
  case "operators":
2421
- return (0, import_vue16.h)(OperatorsScreen, {
2572
+ return (0, import_vue18.h)(OperatorsScreen, {
2422
2573
  client: c,
2423
2574
  ...props.currentOperator ? { me: props.currentOperator } : {}
2424
2575
  });
2425
2576
  case "migrations":
2426
- return (0, import_vue16.h)(MigrationsScreen, { client: c });
2577
+ return (0, import_vue18.h)(MigrationsScreen, { client: c });
2427
2578
  case "log":
2428
- return (0, import_vue16.h)(AdminLogScreen, { client: c });
2579
+ return (0, import_vue18.h)(AdminLogScreen, { client: c });
2429
2580
  case "catalog":
2430
- return props.billingClient ? (0, import_vue16.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2581
+ return props.billingClient ? (0, import_vue18.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2431
2582
  case "subscriber":
2432
- return props.billingClient ? (0, import_vue16.h)(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2583
+ return props.billingClient ? (0, import_vue18.h)(SubscriberScreen, {
2584
+ client: props.billingClient,
2585
+ ...props.authClient ? {
2586
+ onOpenUser: (id) => {
2587
+ openUser.value = id;
2588
+ go("users");
2589
+ }
2590
+ } : {}
2591
+ }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2433
2592
  case "audit":
2434
- return props.auditClient ? (0, import_vue16.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2593
+ return props.auditClient ? (0, import_vue18.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2435
2594
  case "users":
2436
- return props.authClient ? (0, import_vue16.h)(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
2595
+ return props.authClient ? (0, import_vue18.h)(UsersScreen, {
2596
+ key: openUser.value ?? "list",
2597
+ client: props.authClient,
2598
+ ...props.billingClient ? { billingClient: props.billingClient } : {},
2599
+ ...openUser.value ? { openUserId: openUser.value } : {}
2600
+ }) : missing("Pass an AuthAdminClient to look up users here.");
2437
2601
  case "settings": {
2438
2602
  const cc = props.configClient;
2439
2603
  if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
@@ -2441,7 +2605,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2441
2605
  if (e && e.kind !== "template")
2442
2606
  return [
2443
2607
  back("Config & secrets"),
2444
- (0, import_vue16.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2608
+ (0, import_vue18.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2445
2609
  client: cc,
2446
2610
  kind: e.kind,
2447
2611
  configKey: e.key,
@@ -2449,7 +2613,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2449
2613
  onSaved: () => editing.value = null
2450
2614
  })
2451
2615
  ];
2452
- return (0, import_vue16.h)(import_vue_config_admin_screens.ConfigListScreen, {
2616
+ return (0, import_vue18.h)(import_vue_config_admin_screens.ConfigListScreen, {
2453
2617
  client: cc,
2454
2618
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2455
2619
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -2466,13 +2630,13 @@ var AdminShell = (0, import_vue16.defineComponent)({
2466
2630
  if (e?.kind === "template")
2467
2631
  return [
2468
2632
  back("Templates"),
2469
- (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2633
+ (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2470
2634
  client: kc,
2471
2635
  type: e.type,
2472
2636
  onSaved: () => editing.value = null
2473
2637
  })
2474
2638
  ];
2475
- return (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2639
+ return (0, import_vue18.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2476
2640
  client: kc,
2477
2641
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
2478
2642
  });
@@ -2482,11 +2646,11 @@ var AdminShell = (0, import_vue16.defineComponent)({
2482
2646
  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
2647
  const brand = () => {
2484
2648
  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)(
2649
+ return (0, import_vue18.h)("div", { style: styles.navBrand }, [
2650
+ (0, import_vue18.h)("div", { style: styles.navMark }, [icon("mark", 15)]),
2651
+ (0, import_vue18.h)("div", { style: { minWidth: 0, flex: 1 } }, [
2652
+ (0, import_vue18.h)("div", { style: styles.navBrandTitle }, props.appName),
2653
+ props.envLabel ? (0, import_vue18.h)(
2490
2654
  "span",
2491
2655
  {
2492
2656
  style: {
@@ -2503,7 +2667,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2503
2667
  props.envLabel
2504
2668
  ) : null
2505
2669
  ]),
2506
- narrow.value ? (0, import_vue16.h)(
2670
+ narrow.value ? (0, import_vue18.h)(
2507
2671
  "button",
2508
2672
  {
2509
2673
  type: "button",
@@ -2515,7 +2679,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2515
2679
  ) : null
2516
2680
  ]);
2517
2681
  };
2518
- const nav = () => (0, import_vue16.h)(
2682
+ const nav = () => (0, import_vue18.h)(
2519
2683
  "nav",
2520
2684
  {
2521
2685
  style: {
@@ -2526,17 +2690,17 @@ var AdminShell = (0, import_vue16.defineComponent)({
2526
2690
  },
2527
2691
  [
2528
2692
  brand(),
2529
- (0, import_vue16.h)(
2693
+ (0, import_vue18.h)(
2530
2694
  "div",
2531
2695
  { style: styles.navScroll },
2532
2696
  NAV.map((g) => {
2533
2697
  const items = g.items.filter(visible);
2534
2698
  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),
2699
+ return (0, import_vue18.h)("div", { key: g.group }, [
2700
+ (0, import_vue18.h)("div", { style: styles.navGroup }, g.group),
2537
2701
  ...items.map((i) => {
2538
2702
  const active = current.value === i.page;
2539
- return (0, import_vue16.h)(
2703
+ return (0, import_vue18.h)(
2540
2704
  "button",
2541
2705
  {
2542
2706
  key: i.page,
@@ -2544,7 +2708,10 @@ var AdminShell = (0, import_vue16.defineComponent)({
2544
2708
  class: "fonderie-admin-nav-item",
2545
2709
  style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2546
2710
  "aria-current": active ? "page" : void 0,
2547
- onClick: () => go(i.page)
2711
+ onClick: () => {
2712
+ openUser.value = void 0;
2713
+ go(i.page);
2714
+ }
2548
2715
  },
2549
2716
  [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2550
2717
  );
@@ -2552,18 +2719,18 @@ var AdminShell = (0, import_vue16.defineComponent)({
2552
2719
  ]);
2553
2720
  })
2554
2721
  ),
2555
- slots["footer"] ? (0, import_vue16.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2722
+ slots["footer"] ? (0, import_vue18.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2556
2723
  ]
2557
2724
  );
2558
2725
  return () => {
2559
2726
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2560
- return (0, import_vue16.h)(
2727
+ return (0, import_vue18.h)(
2561
2728
  "div",
2562
2729
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2563
2730
  [
2564
2731
  ...narrow.value ? [
2565
- (0, import_vue16.h)("div", { style: styles.topbar }, [
2566
- (0, import_vue16.h)(
2732
+ (0, import_vue18.h)("div", { style: styles.topbar }, [
2733
+ (0, import_vue18.h)(
2567
2734
  "button",
2568
2735
  {
2569
2736
  type: "button",
@@ -2574,19 +2741,19 @@ var AdminShell = (0, import_vue16.defineComponent)({
2574
2741
  },
2575
2742
  [icon("menu")]
2576
2743
  ),
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
2744
+ (0, import_vue18.h)("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2745
+ props.envLabel ? (0, import_vue18.h)("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2579
2746
  ]),
2580
- drawer.value ? (0, import_vue16.h)("div", { style: styles.drawer }, [
2747
+ drawer.value ? (0, import_vue18.h)("div", { style: styles.drawer }, [
2581
2748
  nav(),
2582
- (0, import_vue16.h)("div", {
2749
+ (0, import_vue18.h)("div", {
2583
2750
  style: styles.scrim,
2584
2751
  "aria-hidden": "true",
2585
2752
  onClick: () => drawer.value = false
2586
2753
  })
2587
2754
  ]) : null
2588
2755
  ] : [nav()],
2589
- (0, import_vue16.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2756
+ (0, import_vue18.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2590
2757
  ]
2591
2758
  );
2592
2759
  };
@@ -2605,6 +2772,7 @@ var AdminShell = (0, import_vue16.defineComponent)({
2605
2772
  ModulesScreen,
2606
2773
  OperatorsScreen,
2607
2774
  RoutesScreen,
2775
+ SubscriberBilling,
2608
2776
  SubscriberScreen,
2609
2777
  TokensScreen,
2610
2778
  UsersScreen