@fonderie/vue-admin-screens 0.2.0 → 0.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/README.md CHANGED
@@ -11,12 +11,13 @@ Status: **experimental** (0.x).
11
11
  | Today | Attention | what needs me today — empty is green |
12
12
  | System | Modules · Configuration · Doctor · Routes | what did I deploy · is it configured · is it working · what is exposed |
13
13
  | People | Users | why can't this person log in — lookup, sessions, sign-ins, suspend, sign out everywhere (`authClient`) |
14
+ | Money | Catalog · Subscriber | what am I selling (configured vs stored) · what is this subscriber on, their wallet, ledger, a manual grant (`billingClient`) |
14
15
  | Settings | Config & secrets | the `@fonderie/config` admin screens, as a sub-page |
15
16
  | Messaging | Templates | the `@fonderie/courier` admin screens, as a sub-page |
16
17
  | Activity | Admin log · Access | who did what · who can be here |
17
18
 
18
19
  ```ts
19
- import { AdminClient, AuthAdminClient, ConfigAdminClient, CourierAdminClient } from '@fonderie/client';
20
+ import { AdminClient, AuthAdminClient, BillingAdminClient, ConfigAdminClient, CourierAdminClient } from '@fonderie/client';
20
21
  import { AdminShell } from '@fonderie/vue-admin-screens';
21
22
 
22
23
  const base = { baseUrl: 'https://api.example.com', adminToken, actor: 'louis' };
@@ -25,10 +26,11 @@ const client = new AdminClient(base);
25
26
  const configClient = new ConfigAdminClient({ ...base, prefix: '/_admin' });
26
27
  const courierClient = new CourierAdminClient({ ...base, prefix: '/_admin' });
27
28
  const authClient = new AuthAdminClient(base); // @fonderie/auth ≥ 7.8
29
+ const billingClient = new BillingAdminClient(base); // @fonderie/billing ≥ 9.10
28
30
  ```
29
31
 
30
32
  ```ts
31
- h(AdminShell, { client, configClient, courierClient, authClient })
33
+ h(AdminShell, { client, configClient, courierClient, authClient, billingClient })
32
34
  ```
33
35
 
34
36
  Pass `page` and listen to `navigate` to own the URL (one route per page);
package/dist/index.cjs CHANGED
@@ -23,10 +23,12 @@ __export(index_exports, {
23
23
  AdminLogScreen: () => AdminLogScreen,
24
24
  AdminShell: () => AdminShell,
25
25
  AttentionScreen: () => AttentionScreen,
26
+ CatalogScreen: () => CatalogScreen,
26
27
  ConfigScreen: () => ConfigScreen,
27
28
  DoctorScreen: () => DoctorScreen,
28
29
  ModulesScreen: () => ModulesScreen,
29
30
  RoutesScreen: () => RoutesScreen,
31
+ SubscriberScreen: () => SubscriberScreen,
30
32
  TokensScreen: () => TokensScreen,
31
33
  UsersScreen: () => UsersScreen
32
34
  });
@@ -35,7 +37,7 @@ module.exports = __toCommonJS(index_exports);
35
37
  // src/screens/AdminShell.ts
36
38
  var import_vue_config_admin_screens = require("@fonderie/vue-config-admin-screens");
37
39
  var import_vue_courier_admin_screens = require("@fonderie/vue-courier-admin-screens");
38
- var import_vue10 = require("vue");
40
+ var import_vue12 = require("vue");
39
41
 
40
42
  // src/styles.ts
41
43
  var styles = {
@@ -595,6 +597,233 @@ var UsersScreen = (0, import_vue9.defineComponent)({
595
597
  }
596
598
  });
597
599
 
600
+ // src/screens/CatalogScreen.ts
601
+ var import_vue_admin9 = require("@fonderie/vue-admin");
602
+ var import_vue10 = require("vue");
603
+ var CatalogScreen = (0, import_vue10.defineComponent)({
604
+ name: "FonderieCatalogScreen",
605
+ props: { client: { type: Object, required: true } },
606
+ setup(props) {
607
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin9.useAdminCatalog)(props.client);
608
+ const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
609
+ return () => {
610
+ const c = catalog.value;
611
+ return (0, import_vue10.h)("div", { style: styles.container }, [
612
+ (0, import_vue10.h)("div", { style: styles.toolbar }, [
613
+ (0, import_vue10.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, "Catalog"),
614
+ refreshButton("Refresh", isLoading.value, () => void refresh())
615
+ ]),
616
+ error.value ? (0, import_vue10.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
617
+ isLoading.value && !c ? (0, import_vue10.h)("p", { style: styles.status }, "Loading\u2026") : null,
618
+ c ? [
619
+ (0, import_vue10.h)("h2", { style: styles.subtitle }, "Configured (code)"),
620
+ (0, import_vue10.h)(
621
+ "pre",
622
+ {
623
+ style: {
624
+ ...styles.mono,
625
+ background: "#f9fafb",
626
+ padding: "12px",
627
+ overflowX: "auto"
628
+ }
629
+ },
630
+ JSON.stringify(c.configured, null, 2)
631
+ ),
632
+ (0, import_vue10.h)("h2", { style: styles.subtitle }, "Stored (database)"),
633
+ c.stored.length === 0 ? (0, import_vue10.h)("p", { style: styles.muted }, "No stored plans.") : table(
634
+ ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
635
+ c.stored.map(
636
+ (p) => (0, import_vue10.h)("tr", { key: p.id }, [
637
+ td([
638
+ (0, import_vue10.h)("strong", p.name),
639
+ p.description ? (0, import_vue10.h)("div", { style: styles.muted }, p.description) : ""
640
+ ]),
641
+ td(String(p.tier)),
642
+ td(p.seats == null ? "\u221E" : String(p.seats)),
643
+ td(`${money(p.pricing?.monthly)} ${p.pricing?.currency ?? ""}`),
644
+ td(money(p.pricing?.yearly)),
645
+ td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
646
+ td(
647
+ (0, import_vue10.h)(
648
+ "button",
649
+ {
650
+ type: "button",
651
+ style: styles.button,
652
+ onClick: () => {
653
+ if (window.confirm(`Delete stored plan "${p.name}"?`))
654
+ void deletePlan(p.id).catch(() => {
655
+ });
656
+ }
657
+ },
658
+ "Delete"
659
+ )
660
+ )
661
+ ])
662
+ )
663
+ )
664
+ ] : null
665
+ ]);
666
+ };
667
+ }
668
+ });
669
+
670
+ // src/screens/SubscriberScreen.ts
671
+ var import_vue_admin10 = require("@fonderie/vue-admin");
672
+ var import_vue11 = require("vue");
673
+ var SubscriberScreen = (0, import_vue11.defineComponent)({
674
+ name: "FonderieSubscriberScreen",
675
+ props: { client: { type: Object, required: true } },
676
+ setup(props) {
677
+ const type = (0, import_vue11.ref)("user");
678
+ const idInput = (0, import_vue11.ref)("");
679
+ const subscriber = (0, import_vue11.ref)(null);
680
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin10.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
681
+ const amount = (0, import_vue11.ref)("");
682
+ const note = (0, import_vue11.ref)("");
683
+ const granted = (0, import_vue11.ref)(null);
684
+ const input = (model, placeholder, extra = {}) => (0, import_vue11.h)("input", {
685
+ value: model.value,
686
+ onInput: (e) => model.value = e.target.value,
687
+ placeholder,
688
+ style: { ...styles.button, cursor: "text", ...extra },
689
+ "aria-label": placeholder
690
+ });
691
+ return () => {
692
+ const s = subscription.value;
693
+ const w = wallet.value;
694
+ return (0, import_vue11.h)("div", { style: styles.container }, [
695
+ (0, import_vue11.h)("h1", { style: styles.title }, "Subscriber"),
696
+ (0, import_vue11.h)(
697
+ "form",
698
+ {
699
+ style: styles.toolbar,
700
+ onSubmit: (e) => {
701
+ e.preventDefault();
702
+ granted.value = null;
703
+ subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
704
+ }
705
+ },
706
+ [
707
+ (0, import_vue11.h)(
708
+ "select",
709
+ {
710
+ value: type.value,
711
+ onChange: (e) => type.value = e.target.value,
712
+ style: styles.button,
713
+ "aria-label": "Type"
714
+ },
715
+ [
716
+ (0, import_vue11.h)("option", { value: "user" }, "user"),
717
+ (0, import_vue11.h)("option", { value: "workspace" }, "workspace")
718
+ ]
719
+ ),
720
+ input(idInput, "subscriber id", { minWidth: "280px" }),
721
+ (0, import_vue11.h)(
722
+ "button",
723
+ { type: "submit", style: styles.button, disabled: isLoading.value },
724
+ "Look up"
725
+ )
726
+ ]
727
+ ),
728
+ error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
729
+ subscriber.value && !isLoading.value ? [
730
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Subscription"),
731
+ s ? (0, import_vue11.h)("table", { style: styles.table }, [
732
+ (0, import_vue11.h)("tbody", [
733
+ (0, import_vue11.h)("tr", [td("plan"), td([(0, import_vue11.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
734
+ (0, import_vue11.h)("tr", [
735
+ td("status"),
736
+ td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
737
+ ]),
738
+ (0, import_vue11.h)("tr", [
739
+ td("period"),
740
+ td(
741
+ `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
742
+ )
743
+ ]),
744
+ (0, import_vue11.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
745
+ ])
746
+ ]) : (0, import_vue11.h)("p", { style: styles.muted }, "No subscription."),
747
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Wallet"),
748
+ w ? [
749
+ (0, import_vue11.h)("p", [
750
+ (0, import_vue11.h)("strong", w.balance),
751
+ ` ${w.currency} `,
752
+ (0, import_vue11.h)(
753
+ "span",
754
+ { style: styles.muted },
755
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
756
+ )
757
+ ]),
758
+ (0, import_vue11.h)(
759
+ "form",
760
+ {
761
+ style: styles.toolbar,
762
+ onSubmit: (e) => {
763
+ e.preventDefault();
764
+ const sub = subscriber.value;
765
+ if (!sub) return;
766
+ const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
767
+ const amt = amount.value.trim();
768
+ void grant({
769
+ amount: amt,
770
+ ...note.value ? { description: note.value } : {},
771
+ idempotencyKey: key
772
+ }).then(() => {
773
+ granted.value = `Granted ${amt} ${w.currency}.`;
774
+ amount.value = "";
775
+ note.value = "";
776
+ }).catch(() => {
777
+ });
778
+ }
779
+ },
780
+ [
781
+ input(amount, "amount (minor units)"),
782
+ input(note, "reason (optional)", { minWidth: "220px" }),
783
+ (0, import_vue11.h)(
784
+ "button",
785
+ {
786
+ type: "submit",
787
+ style: styles.button,
788
+ disabled: !/^\d+$/.test(amount.value.trim())
789
+ },
790
+ "Grant"
791
+ ),
792
+ granted.value ? (0, import_vue11.h)("span", { style: styles.ok }, granted.value) : null
793
+ ]
794
+ ),
795
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Ledger"),
796
+ ledger.value.length === 0 ? (0, import_vue11.h)("p", { style: styles.muted }, "Nothing moved yet.") : table(
797
+ ["When", "Kind", "Amount", "Note"],
798
+ ledger.value.map(
799
+ (t) => (0, import_vue11.h)("tr", { key: t.id }, [
800
+ td(new Date(t.createdAt).toLocaleString(), styles.muted),
801
+ td(t.type),
802
+ td(t.amount, styles.mono),
803
+ td(t.description ?? "", styles.muted)
804
+ ])
805
+ )
806
+ ),
807
+ hasMoreLedger.value ? (0, import_vue11.h)(
808
+ "button",
809
+ {
810
+ type: "button",
811
+ style: { ...styles.button, marginTop: "8px" },
812
+ onClick: () => void loadMoreLedger()
813
+ },
814
+ "Load more"
815
+ ) : null
816
+ ] : (0, import_vue11.h)(
817
+ "p",
818
+ { style: styles.muted },
819
+ "No wallet \u2014 billing has no wallet configuration."
820
+ )
821
+ ] : null
822
+ ]);
823
+ };
824
+ }
825
+ });
826
+
598
827
  // src/screens/AdminShell.ts
599
828
  var NAV = [
600
829
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
@@ -608,6 +837,13 @@ var NAV = [
608
837
  ]
609
838
  },
610
839
  { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
840
+ {
841
+ group: "Money",
842
+ items: [
843
+ { page: "catalog", label: "Catalog", needs: "billing" },
844
+ { page: "subscriber", label: "Subscriber", needs: "billing" }
845
+ ]
846
+ },
611
847
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
612
848
  { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
613
849
  {
@@ -618,7 +854,7 @@ var NAV = [
618
854
  ]
619
855
  }
620
856
  ];
621
- var AdminShell = (0, import_vue10.defineComponent)({
857
+ var AdminShell = (0, import_vue12.defineComponent)({
622
858
  name: "FonderieAdminShell",
623
859
  props: {
624
860
  client: { type: Object, required: true },
@@ -628,6 +864,8 @@ var AdminShell = (0, import_vue10.defineComponent)({
628
864
  courierClient: { type: Object, default: void 0 },
629
865
  // Given ⇒ the People page (users) appears; needs @fonderie/auth ≥ 7.8.
630
866
  authClient: { type: Object, default: void 0 },
867
+ // Given ⇒ the Money pages (catalog, subscriber) appear; needs @fonderie/billing ≥ 9.10.
868
+ billingClient: { type: Object, default: void 0 },
631
869
  environment: { type: String, default: void 0 },
632
870
  // Controlled navigation: pass `page` and listen to `navigate` to own the
633
871
  // URL. Omit `page` and the shell keeps it itself.
@@ -635,15 +873,15 @@ var AdminShell = (0, import_vue10.defineComponent)({
635
873
  },
636
874
  emits: { navigate: (_page) => true },
637
875
  setup(props, { emit }) {
638
- const own = (0, import_vue10.ref)("attention");
639
- const current = (0, import_vue10.computed)(() => props.page ?? own.value);
640
- const editing = (0, import_vue10.ref)(null);
876
+ const own = (0, import_vue12.ref)("attention");
877
+ const current = (0, import_vue12.computed)(() => props.page ?? own.value);
878
+ const editing = (0, import_vue12.ref)(null);
641
879
  const go = (p) => {
642
880
  editing.value = null;
643
881
  emit("navigate", p);
644
882
  if (props.page === void 0) own.value = p;
645
883
  };
646
- const back = () => (0, import_vue10.h)(
884
+ const back = () => (0, import_vue12.h)(
647
885
  "button",
648
886
  { type: "button", style: styles.button, onClick: () => editing.value = null },
649
887
  "\u2190 Back"
@@ -652,25 +890,37 @@ var AdminShell = (0, import_vue10.defineComponent)({
652
890
  const c = props.client;
653
891
  switch (current.value) {
654
892
  case "attention":
655
- return (0, import_vue10.h)(AttentionScreen, { client: c });
893
+ return (0, import_vue12.h)(AttentionScreen, { client: c });
656
894
  case "modules":
657
- return (0, import_vue10.h)(ModulesScreen, { client: c });
895
+ return (0, import_vue12.h)(ModulesScreen, { client: c });
658
896
  case "doctor":
659
- return (0, import_vue10.h)(DoctorScreen, { client: c });
897
+ return (0, import_vue12.h)(DoctorScreen, { client: c });
660
898
  case "config":
661
- return (0, import_vue10.h)(ConfigScreen, { client: c });
899
+ return (0, import_vue12.h)(ConfigScreen, { client: c });
662
900
  case "routes":
663
- return (0, import_vue10.h)(RoutesScreen, { client: c });
901
+ return (0, import_vue12.h)(RoutesScreen, { client: c });
664
902
  case "tokens":
665
- return (0, import_vue10.h)(TokensScreen, { client: c });
903
+ return (0, import_vue12.h)(TokensScreen, { client: c });
666
904
  case "log":
667
- return (0, import_vue10.h)(AdminLogScreen, { client: c });
905
+ return (0, import_vue12.h)(AdminLogScreen, { client: c });
906
+ case "catalog":
907
+ return props.billingClient ? (0, import_vue12.h)(CatalogScreen, { client: props.billingClient }) : (0, import_vue12.h)(
908
+ "p",
909
+ { style: styles.status },
910
+ "Pass a BillingAdminClient to see the catalog here."
911
+ );
912
+ case "subscriber":
913
+ return props.billingClient ? (0, import_vue12.h)(SubscriberScreen, { client: props.billingClient }) : (0, import_vue12.h)(
914
+ "p",
915
+ { style: styles.status },
916
+ "Pass a BillingAdminClient to look up subscribers here."
917
+ );
668
918
  case "users":
669
- return props.authClient ? (0, import_vue10.h)(UsersScreen, { client: props.authClient }) : (0, import_vue10.h)("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
919
+ return props.authClient ? (0, import_vue12.h)(UsersScreen, { client: props.authClient }) : (0, import_vue12.h)("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
670
920
  case "settings": {
671
921
  const cc = props.configClient;
672
922
  if (!cc)
673
- return (0, import_vue10.h)(
923
+ return (0, import_vue12.h)(
674
924
  "p",
675
925
  { style: styles.status },
676
926
  "Pass a ConfigAdminClient to manage config and secrets here."
@@ -679,7 +929,7 @@ var AdminShell = (0, import_vue10.defineComponent)({
679
929
  if (e && e.kind !== "template")
680
930
  return [
681
931
  back(),
682
- (0, import_vue10.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
932
+ (0, import_vue12.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
683
933
  client: cc,
684
934
  kind: e.kind,
685
935
  configKey: e.key,
@@ -687,7 +937,7 @@ var AdminShell = (0, import_vue10.defineComponent)({
687
937
  onSaved: () => editing.value = null
688
938
  })
689
939
  ];
690
- return (0, import_vue10.h)(import_vue_config_admin_screens.ConfigListScreen, {
940
+ return (0, import_vue12.h)(import_vue_config_admin_screens.ConfigListScreen, {
691
941
  client: cc,
692
942
  ...props.environment !== void 0 ? { environment: props.environment } : {},
693
943
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -697,7 +947,7 @@ var AdminShell = (0, import_vue10.defineComponent)({
697
947
  case "templates": {
698
948
  const kc = props.courierClient;
699
949
  if (!kc)
700
- return (0, import_vue10.h)(
950
+ return (0, import_vue12.h)(
701
951
  "p",
702
952
  { style: styles.status },
703
953
  "Pass a CourierAdminClient to manage templates here."
@@ -706,32 +956,32 @@ var AdminShell = (0, import_vue10.defineComponent)({
706
956
  if (e?.kind === "template")
707
957
  return [
708
958
  back(),
709
- (0, import_vue10.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
959
+ (0, import_vue12.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
710
960
  client: kc,
711
961
  type: e.type,
712
962
  onSaved: () => editing.value = null
713
963
  })
714
964
  ];
715
- return (0, import_vue10.h)(import_vue_courier_admin_screens.TemplateListScreen, {
965
+ return (0, import_vue12.h)(import_vue_courier_admin_screens.TemplateListScreen, {
716
966
  client: kc,
717
967
  "onSelect-template": (t) => editing.value = { kind: "template", type: t.type }
718
968
  });
719
969
  }
720
970
  }
721
971
  };
722
- return () => (0, import_vue10.h)("div", { style: styles.shell }, [
723
- (0, import_vue10.h)(
972
+ return () => (0, import_vue12.h)("div", { style: styles.shell }, [
973
+ (0, import_vue12.h)(
724
974
  "nav",
725
975
  { style: styles.nav, "aria-label": "Admin" },
726
976
  NAV.map((g) => {
727
977
  const items = g.items.filter(
728
- (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : props.authClient)
978
+ (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : props.billingClient)
729
979
  );
730
980
  if (items.length === 0) return null;
731
- return (0, import_vue10.h)("div", { key: g.group }, [
732
- (0, import_vue10.h)("div", { style: styles.navGroup }, g.group),
981
+ return (0, import_vue12.h)("div", { key: g.group }, [
982
+ (0, import_vue12.h)("div", { style: styles.navGroup }, g.group),
733
983
  ...items.map(
734
- (i) => (0, import_vue10.h)(
984
+ (i) => (0, import_vue12.h)(
735
985
  "button",
736
986
  {
737
987
  key: i.page,
@@ -749,7 +999,7 @@ var AdminShell = (0, import_vue10.defineComponent)({
749
999
  ]);
750
1000
  })
751
1001
  ),
752
- (0, import_vue10.h)("main", { style: styles.main }, body())
1002
+ (0, import_vue12.h)("main", { style: styles.main }, body())
753
1003
  ]);
754
1004
  }
755
1005
  });
@@ -758,10 +1008,12 @@ var AdminShell = (0, import_vue10.defineComponent)({
758
1008
  AdminLogScreen,
759
1009
  AdminShell,
760
1010
  AttentionScreen,
1011
+ CatalogScreen,
761
1012
  ConfigScreen,
762
1013
  DoctorScreen,
763
1014
  ModulesScreen,
764
1015
  RoutesScreen,
1016
+ SubscriberScreen,
765
1017
  TokensScreen,
766
1018
  UsersScreen
767
1019
  });