@fonderie/vue-admin-screens 0.1.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.ts
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
- import { computed, defineComponent as defineComponent8, h as h9, ref } from "vue";
4
+ import { computed as computed2, defineComponent as defineComponent11, h as h12, ref as ref3 } from "vue";
5
5
 
6
6
  // src/styles.ts
7
7
  var styles = {
@@ -427,6 +427,367 @@ var TokensScreen = defineComponent7({
427
427
  }
428
428
  });
429
429
 
430
+ // src/screens/UsersScreen.ts
431
+ import { useAdminLoginHistory, useAdminUser, useAdminUserSessions } from "@fonderie/vue-admin";
432
+ import { computed, defineComponent as defineComponent8, h as h9, ref } from "vue";
433
+ var UsersScreen = defineComponent8({
434
+ name: "FonderieUsersScreen",
435
+ props: { client: { type: Object, required: true } },
436
+ setup(props) {
437
+ const input = ref("");
438
+ const email = ref("");
439
+ const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
440
+ props.client,
441
+ { email }
442
+ );
443
+ const userId = computed(() => user.value?.id ?? null);
444
+ const sessions = useAdminUserSessions(props.client, userId);
445
+ const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
446
+ const yesNo = (v) => h9("span", { style: v ? styles.ok : styles.muted }, v ? "yes" : "no");
447
+ const row = (k, v) => h9("tr", [td(k), td(v)]);
448
+ return () => {
449
+ const u = user.value;
450
+ return h9("div", { style: styles.container }, [
451
+ h9("h1", { style: styles.title }, "Users"),
452
+ h9(
453
+ "form",
454
+ {
455
+ style: styles.toolbar,
456
+ onSubmit: (e) => {
457
+ e.preventDefault();
458
+ email.value = input.value.trim();
459
+ }
460
+ },
461
+ [
462
+ h9("input", {
463
+ type: "email",
464
+ value: input.value,
465
+ onInput: (e) => input.value = e.target.value,
466
+ placeholder: "email address",
467
+ style: { ...styles.button, cursor: "text", minWidth: "280px" },
468
+ "aria-label": "Email"
469
+ }),
470
+ h9(
471
+ "button",
472
+ { type: "submit", style: styles.button, disabled: isLoading.value },
473
+ "Look up"
474
+ )
475
+ ]
476
+ ),
477
+ error.value ? h9(
478
+ "p",
479
+ { style: styles.error, role: "alert" },
480
+ error.value.status === 404 ? "No user with that email." : error.value.explanation
481
+ ) : null,
482
+ u ? [
483
+ h9("h2", { style: styles.subtitle }, [
484
+ u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
485
+ " ",
486
+ u.suspended ? h9("span", { style: styles.badge }, "suspended") : null,
487
+ u.deletedAt ? h9("span", { style: styles.badge }, "deleted") : null
488
+ ]),
489
+ h9("table", { style: styles.table }, [
490
+ h9("tbody", [
491
+ row("id", h9("span", { style: styles.mono }, u.id)),
492
+ row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
493
+ row("MFA", yesNo(u.mfaEnabled)),
494
+ row(
495
+ "provider",
496
+ `${u.provider || "password"}${u.hasPassword ? "" : " \xB7 no password set"}`
497
+ ),
498
+ row(
499
+ "last login",
500
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h9("span", { style: styles.muted }, "never")
501
+ ),
502
+ row("created", new Date(u.createdAt).toLocaleString())
503
+ ])
504
+ ]),
505
+ h9("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
506
+ u.suspended ? refreshButton("Unsuspend", isLoading.value, () => void unsuspend()) : refreshButton("Suspend", isLoading.value, () => void suspend()),
507
+ refreshButton(
508
+ "Sign out everywhere",
509
+ isLoading.value,
510
+ () => void revokeSessions().then(() => sessions.refresh())
511
+ )
512
+ ]),
513
+ h9("h2", { style: styles.subtitle }, "Live sessions"),
514
+ sessions.sessions.value.length === 0 ? h9("p", { style: styles.muted }, "None.") : h9(
515
+ "ul",
516
+ { style: styles.list },
517
+ sessions.sessions.value.map(
518
+ (s) => h9("li", { key: s.id, style: styles.row }, [
519
+ h9("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
520
+ " ",
521
+ h9("span", { style: styles.muted }, s.userAgent ?? ""),
522
+ " ",
523
+ h9(
524
+ "span",
525
+ { style: styles.muted },
526
+ `since ${new Date(s.createdAt).toLocaleString()}`
527
+ )
528
+ ])
529
+ )
530
+ ),
531
+ h9("h2", { style: styles.subtitle }, "Recent sign-ins"),
532
+ history.events.value.length === 0 && !history.isLoading.value ? h9("p", { style: styles.muted }, "None recorded.") : h9(
533
+ "ul",
534
+ { style: styles.list },
535
+ history.events.value.map(
536
+ (e) => h9("li", { key: e.id, style: styles.row }, [
537
+ h9(
538
+ "span",
539
+ { style: e.outcome === "success" ? styles.ok : styles.bad },
540
+ e.outcome
541
+ ),
542
+ " ",
543
+ h9("span", { style: styles.muted }, e.method),
544
+ " ",
545
+ h9("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
546
+ ])
547
+ )
548
+ ),
549
+ history.hasMore.value && !history.isLoading.value ? h9(
550
+ "button",
551
+ {
552
+ type: "button",
553
+ style: { ...styles.button, marginTop: "8px" },
554
+ onClick: () => void history.loadMore()
555
+ },
556
+ "Load more"
557
+ ) : null
558
+ ] : null
559
+ ]);
560
+ };
561
+ }
562
+ });
563
+
564
+ // src/screens/CatalogScreen.ts
565
+ import { useAdminCatalog } from "@fonderie/vue-admin";
566
+ import { defineComponent as defineComponent9, h as h10 } from "vue";
567
+ var CatalogScreen = defineComponent9({
568
+ name: "FonderieCatalogScreen",
569
+ props: { client: { type: Object, required: true } },
570
+ setup(props) {
571
+ const { catalog, isLoading, error, refresh, deletePlan } = useAdminCatalog(props.client);
572
+ const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
573
+ return () => {
574
+ const c = catalog.value;
575
+ return h10("div", { style: styles.container }, [
576
+ h10("div", { style: styles.toolbar }, [
577
+ h10("h1", { style: { ...styles.title, marginBottom: 0 } }, "Catalog"),
578
+ refreshButton("Refresh", isLoading.value, () => void refresh())
579
+ ]),
580
+ error.value ? h10("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
581
+ isLoading.value && !c ? h10("p", { style: styles.status }, "Loading\u2026") : null,
582
+ c ? [
583
+ h10("h2", { style: styles.subtitle }, "Configured (code)"),
584
+ h10(
585
+ "pre",
586
+ {
587
+ style: {
588
+ ...styles.mono,
589
+ background: "#f9fafb",
590
+ padding: "12px",
591
+ overflowX: "auto"
592
+ }
593
+ },
594
+ JSON.stringify(c.configured, null, 2)
595
+ ),
596
+ h10("h2", { style: styles.subtitle }, "Stored (database)"),
597
+ c.stored.length === 0 ? h10("p", { style: styles.muted }, "No stored plans.") : table(
598
+ ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
599
+ c.stored.map(
600
+ (p) => h10("tr", { key: p.id }, [
601
+ td([
602
+ h10("strong", p.name),
603
+ p.description ? h10("div", { style: styles.muted }, p.description) : ""
604
+ ]),
605
+ td(String(p.tier)),
606
+ td(p.seats == null ? "\u221E" : String(p.seats)),
607
+ td(`${money(p.pricing?.monthly)} ${p.pricing?.currency ?? ""}`),
608
+ td(money(p.pricing?.yearly)),
609
+ td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
610
+ td(
611
+ h10(
612
+ "button",
613
+ {
614
+ type: "button",
615
+ style: styles.button,
616
+ onClick: () => {
617
+ if (window.confirm(`Delete stored plan "${p.name}"?`))
618
+ void deletePlan(p.id).catch(() => {
619
+ });
620
+ }
621
+ },
622
+ "Delete"
623
+ )
624
+ )
625
+ ])
626
+ )
627
+ )
628
+ ] : null
629
+ ]);
630
+ };
631
+ }
632
+ });
633
+
634
+ // src/screens/SubscriberScreen.ts
635
+ import { useAdminSubscriber } from "@fonderie/vue-admin";
636
+ import { defineComponent as defineComponent10, h as h11, ref as ref2 } from "vue";
637
+ var SubscriberScreen = defineComponent10({
638
+ name: "FonderieSubscriberScreen",
639
+ props: { client: { type: Object, required: true } },
640
+ setup(props) {
641
+ const type = ref2("user");
642
+ const idInput = ref2("");
643
+ const subscriber = ref2(null);
644
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
645
+ const amount = ref2("");
646
+ const note = ref2("");
647
+ const granted = ref2(null);
648
+ const input = (model, placeholder, extra = {}) => h11("input", {
649
+ value: model.value,
650
+ onInput: (e) => model.value = e.target.value,
651
+ placeholder,
652
+ style: { ...styles.button, cursor: "text", ...extra },
653
+ "aria-label": placeholder
654
+ });
655
+ return () => {
656
+ const s = subscription.value;
657
+ const w = wallet.value;
658
+ return h11("div", { style: styles.container }, [
659
+ h11("h1", { style: styles.title }, "Subscriber"),
660
+ h11(
661
+ "form",
662
+ {
663
+ style: styles.toolbar,
664
+ onSubmit: (e) => {
665
+ e.preventDefault();
666
+ granted.value = null;
667
+ subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
668
+ }
669
+ },
670
+ [
671
+ h11(
672
+ "select",
673
+ {
674
+ value: type.value,
675
+ onChange: (e) => type.value = e.target.value,
676
+ style: styles.button,
677
+ "aria-label": "Type"
678
+ },
679
+ [
680
+ h11("option", { value: "user" }, "user"),
681
+ h11("option", { value: "workspace" }, "workspace")
682
+ ]
683
+ ),
684
+ input(idInput, "subscriber id", { minWidth: "280px" }),
685
+ h11(
686
+ "button",
687
+ { type: "submit", style: styles.button, disabled: isLoading.value },
688
+ "Look up"
689
+ )
690
+ ]
691
+ ),
692
+ error.value ? h11("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
693
+ subscriber.value && !isLoading.value ? [
694
+ h11("h2", { style: styles.subtitle }, "Subscription"),
695
+ s ? h11("table", { style: styles.table }, [
696
+ h11("tbody", [
697
+ h11("tr", [td("plan"), td([h11("strong", s.plan), ` \xB7 ${s.interval}`])]),
698
+ h11("tr", [
699
+ td("status"),
700
+ td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
701
+ ]),
702
+ h11("tr", [
703
+ td("period"),
704
+ td(
705
+ `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
706
+ )
707
+ ]),
708
+ h11("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
709
+ ])
710
+ ]) : h11("p", { style: styles.muted }, "No subscription."),
711
+ h11("h2", { style: styles.subtitle }, "Wallet"),
712
+ w ? [
713
+ h11("p", [
714
+ h11("strong", w.balance),
715
+ ` ${w.currency} `,
716
+ h11(
717
+ "span",
718
+ { style: styles.muted },
719
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
720
+ )
721
+ ]),
722
+ h11(
723
+ "form",
724
+ {
725
+ style: styles.toolbar,
726
+ onSubmit: (e) => {
727
+ e.preventDefault();
728
+ const sub = subscriber.value;
729
+ if (!sub) return;
730
+ const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
731
+ const amt = amount.value.trim();
732
+ void grant({
733
+ amount: amt,
734
+ ...note.value ? { description: note.value } : {},
735
+ idempotencyKey: key
736
+ }).then(() => {
737
+ granted.value = `Granted ${amt} ${w.currency}.`;
738
+ amount.value = "";
739
+ note.value = "";
740
+ }).catch(() => {
741
+ });
742
+ }
743
+ },
744
+ [
745
+ input(amount, "amount (minor units)"),
746
+ input(note, "reason (optional)", { minWidth: "220px" }),
747
+ h11(
748
+ "button",
749
+ {
750
+ type: "submit",
751
+ style: styles.button,
752
+ disabled: !/^\d+$/.test(amount.value.trim())
753
+ },
754
+ "Grant"
755
+ ),
756
+ granted.value ? h11("span", { style: styles.ok }, granted.value) : null
757
+ ]
758
+ ),
759
+ h11("h2", { style: styles.subtitle }, "Ledger"),
760
+ ledger.value.length === 0 ? h11("p", { style: styles.muted }, "Nothing moved yet.") : table(
761
+ ["When", "Kind", "Amount", "Note"],
762
+ ledger.value.map(
763
+ (t) => h11("tr", { key: t.id }, [
764
+ td(new Date(t.createdAt).toLocaleString(), styles.muted),
765
+ td(t.type),
766
+ td(t.amount, styles.mono),
767
+ td(t.description ?? "", styles.muted)
768
+ ])
769
+ )
770
+ ),
771
+ hasMoreLedger.value ? h11(
772
+ "button",
773
+ {
774
+ type: "button",
775
+ style: { ...styles.button, marginTop: "8px" },
776
+ onClick: () => void loadMoreLedger()
777
+ },
778
+ "Load more"
779
+ ) : null
780
+ ] : h11(
781
+ "p",
782
+ { style: styles.muted },
783
+ "No wallet \u2014 billing has no wallet configuration."
784
+ )
785
+ ] : null
786
+ ]);
787
+ };
788
+ }
789
+ });
790
+
430
791
  // src/screens/AdminShell.ts
431
792
  var NAV = [
432
793
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
@@ -439,6 +800,14 @@ var NAV = [
439
800
  { page: "routes", label: "Routes" }
440
801
  ]
441
802
  },
803
+ { group: "People", items: [{ page: "users", label: "Users", needs: "auth" }] },
804
+ {
805
+ group: "Money",
806
+ items: [
807
+ { page: "catalog", label: "Catalog", needs: "billing" },
808
+ { page: "subscriber", label: "Subscriber", needs: "billing" }
809
+ ]
810
+ },
442
811
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
443
812
  { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
444
813
  {
@@ -449,7 +818,7 @@ var NAV = [
449
818
  ]
450
819
  }
451
820
  ];
452
- var AdminShell = defineComponent8({
821
+ var AdminShell = defineComponent11({
453
822
  name: "FonderieAdminShell",
454
823
  props: {
455
824
  client: { type: Object, required: true },
@@ -457,6 +826,10 @@ var AdminShell = defineComponent8({
457
826
  // `prefix: '/_admin'` to go through the one token.
458
827
  configClient: { type: Object, default: void 0 },
459
828
  courierClient: { type: Object, default: void 0 },
829
+ // Given ⇒ the People page (users) appears; needs @fonderie/auth ≥ 7.8.
830
+ authClient: { type: Object, default: void 0 },
831
+ // Given ⇒ the Money pages (catalog, subscriber) appear; needs @fonderie/billing ≥ 9.10.
832
+ billingClient: { type: Object, default: void 0 },
460
833
  environment: { type: String, default: void 0 },
461
834
  // Controlled navigation: pass `page` and listen to `navigate` to own the
462
835
  // URL. Omit `page` and the shell keeps it itself.
@@ -464,15 +837,15 @@ var AdminShell = defineComponent8({
464
837
  },
465
838
  emits: { navigate: (_page) => true },
466
839
  setup(props, { emit }) {
467
- const own = ref("attention");
468
- const current = computed(() => props.page ?? own.value);
469
- const editing = ref(null);
840
+ const own = ref3("attention");
841
+ const current = computed2(() => props.page ?? own.value);
842
+ const editing = ref3(null);
470
843
  const go = (p) => {
471
844
  editing.value = null;
472
845
  emit("navigate", p);
473
846
  if (props.page === void 0) own.value = p;
474
847
  };
475
- const back = () => h9(
848
+ const back = () => h12(
476
849
  "button",
477
850
  { type: "button", style: styles.button, onClick: () => editing.value = null },
478
851
  "\u2190 Back"
@@ -481,23 +854,37 @@ var AdminShell = defineComponent8({
481
854
  const c = props.client;
482
855
  switch (current.value) {
483
856
  case "attention":
484
- return h9(AttentionScreen, { client: c });
857
+ return h12(AttentionScreen, { client: c });
485
858
  case "modules":
486
- return h9(ModulesScreen, { client: c });
859
+ return h12(ModulesScreen, { client: c });
487
860
  case "doctor":
488
- return h9(DoctorScreen, { client: c });
861
+ return h12(DoctorScreen, { client: c });
489
862
  case "config":
490
- return h9(ConfigScreen, { client: c });
863
+ return h12(ConfigScreen, { client: c });
491
864
  case "routes":
492
- return h9(RoutesScreen, { client: c });
865
+ return h12(RoutesScreen, { client: c });
493
866
  case "tokens":
494
- return h9(TokensScreen, { client: c });
867
+ return h12(TokensScreen, { client: c });
495
868
  case "log":
496
- return h9(AdminLogScreen, { client: c });
869
+ return h12(AdminLogScreen, { client: c });
870
+ case "catalog":
871
+ return props.billingClient ? h12(CatalogScreen, { client: props.billingClient }) : h12(
872
+ "p",
873
+ { style: styles.status },
874
+ "Pass a BillingAdminClient to see the catalog here."
875
+ );
876
+ case "subscriber":
877
+ return props.billingClient ? h12(SubscriberScreen, { client: props.billingClient }) : h12(
878
+ "p",
879
+ { style: styles.status },
880
+ "Pass a BillingAdminClient to look up subscribers here."
881
+ );
882
+ case "users":
883
+ return props.authClient ? h12(UsersScreen, { client: props.authClient }) : h12("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
497
884
  case "settings": {
498
885
  const cc = props.configClient;
499
886
  if (!cc)
500
- return h9(
887
+ return h12(
501
888
  "p",
502
889
  { style: styles.status },
503
890
  "Pass a ConfigAdminClient to manage config and secrets here."
@@ -506,7 +893,7 @@ var AdminShell = defineComponent8({
506
893
  if (e && e.kind !== "template")
507
894
  return [
508
895
  back(),
509
- h9(ConfigEditorScreen, {
896
+ h12(ConfigEditorScreen, {
510
897
  client: cc,
511
898
  kind: e.kind,
512
899
  configKey: e.key,
@@ -514,7 +901,7 @@ var AdminShell = defineComponent8({
514
901
  onSaved: () => editing.value = null
515
902
  })
516
903
  ];
517
- return h9(ConfigListScreen, {
904
+ return h12(ConfigListScreen, {
518
905
  client: cc,
519
906
  ...props.environment !== void 0 ? { environment: props.environment } : {},
520
907
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -524,7 +911,7 @@ var AdminShell = defineComponent8({
524
911
  case "templates": {
525
912
  const kc = props.courierClient;
526
913
  if (!kc)
527
- return h9(
914
+ return h12(
528
915
  "p",
529
916
  { style: styles.status },
530
917
  "Pass a CourierAdminClient to manage templates here."
@@ -533,32 +920,32 @@ var AdminShell = defineComponent8({
533
920
  if (e?.kind === "template")
534
921
  return [
535
922
  back(),
536
- h9(TemplateEditorScreen, {
923
+ h12(TemplateEditorScreen, {
537
924
  client: kc,
538
925
  type: e.type,
539
926
  onSaved: () => editing.value = null
540
927
  })
541
928
  ];
542
- return h9(TemplateListScreen, {
929
+ return h12(TemplateListScreen, {
543
930
  client: kc,
544
931
  "onSelect-template": (t) => editing.value = { kind: "template", type: t.type }
545
932
  });
546
933
  }
547
934
  }
548
935
  };
549
- return () => h9("div", { style: styles.shell }, [
550
- h9(
936
+ return () => h12("div", { style: styles.shell }, [
937
+ h12(
551
938
  "nav",
552
939
  { style: styles.nav, "aria-label": "Admin" },
553
940
  NAV.map((g) => {
554
941
  const items = g.items.filter(
555
- (i) => !i.needs || (i.needs === "config" ? props.configClient : props.courierClient)
942
+ (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : props.billingClient)
556
943
  );
557
944
  if (items.length === 0) return null;
558
- return h9("div", { key: g.group }, [
559
- h9("div", { style: styles.navGroup }, g.group),
945
+ return h12("div", { key: g.group }, [
946
+ h12("div", { style: styles.navGroup }, g.group),
560
947
  ...items.map(
561
- (i) => h9(
948
+ (i) => h12(
562
949
  "button",
563
950
  {
564
951
  key: i.page,
@@ -576,7 +963,7 @@ var AdminShell = defineComponent8({
576
963
  ]);
577
964
  })
578
965
  ),
579
- h9("main", { style: styles.main }, body())
966
+ h12("main", { style: styles.main }, body())
580
967
  ]);
581
968
  }
582
969
  });
@@ -584,10 +971,13 @@ export {
584
971
  AdminLogScreen,
585
972
  AdminShell,
586
973
  AttentionScreen,
974
+ CatalogScreen,
587
975
  ConfigScreen,
588
976
  DoctorScreen,
589
977
  ModulesScreen,
590
978
  RoutesScreen,
591
- TokensScreen
979
+ SubscriberScreen,
980
+ TokensScreen,
981
+ UsersScreen
592
982
  };
593
983
  //# sourceMappingURL=index.js.map