@saasicat/ui-vue 0.19.0 → 0.21.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.
Files changed (66) hide show
  1. package/dist/{catalog-JXdKdIqG.d.cts → catalog-BY_t9prv.d.cts} +1 -0
  2. package/dist/{catalog-JXdKdIqG.d.ts → catalog-BY_t9prv.d.ts} +1 -0
  3. package/dist/{chunk-RVG6LQHL.js → chunk-7IFKVGZV.js} +1 -1
  4. package/dist/{chunk-DPTIOQXX.js → chunk-JN6HZGLV.js} +2 -0
  5. package/dist/{chunk-ORX3ZG47.js → chunk-OTP3OSSW.js} +1 -1
  6. package/dist/client/index.cjs +2 -0
  7. package/dist/client/index.d.cts +2 -2
  8. package/dist/client/index.d.ts +2 -2
  9. package/dist/client/index.js +2 -2
  10. package/dist/index.cjs +2 -0
  11. package/dist/index.d.cts +4 -4
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +3 -3
  14. package/dist/quasar/index.cjs +2 -0
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +2 -2
  18. package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-DlJRGIVq.d.cts} +1 -1
  19. package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-c_Eqsu9u.d.ts} +1 -1
  20. package/package.json +2 -2
  21. package/src/client/i18n/messages/common.ts +2 -0
  22. package/src/components/admin-page/AdminBody.vue +40 -0
  23. package/src/components/admin-page/AdminHero.vue +32 -0
  24. package/src/components/admin-page/AdminPage.vue +16 -0
  25. package/src/components/admin-page/AdminSection.vue +38 -0
  26. package/src/components/plan-cockpit/PlanCockpit.vue +15 -0
  27. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +8 -8
  28. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +10 -11
  29. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +8 -8
  30. package/src/components/plan-cockpit/PlanCockpitVersions.vue +9 -11
  31. package/src/components/plan-detail/PlanAuditLog.vue +7 -8
  32. package/src/components/plan-detail/PlanDetail.vue +14 -1
  33. package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
  34. package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
  35. package/src/components/plan-list/PlanList.vue +224 -257
  36. package/src/pages-standard/AdminLayout.vue +4 -2
  37. package/src/pages-standard/AuditPage.vue +18 -43
  38. package/src/pages-standard/BundlesPage.vue +17 -34
  39. package/src/pages-standard/DashboardPage.vue +20 -51
  40. package/src/pages-standard/DiscoveryPage.vue +38 -51
  41. package/src/pages-standard/EmailHistoryPage.vue +20 -71
  42. package/src/pages-standard/MarketingCatalogPage.vue +304 -315
  43. package/src/pages-standard/PilotsPage.vue +24 -56
  44. package/src/pages-standard/PlansPage.vue +25 -22
  45. package/src/pages-standard/PlatformEmailPage.vue +19 -51
  46. package/src/pages-standard/PromoCodeDetailPage.vue +39 -50
  47. package/src/pages-standard/PromoCodesPage.vue +8 -15
  48. package/src/pages-standard/SubscriptionsPage.vue +9 -35
  49. package/src/pages-standard/SuperAdminSetupWizard.vue +2 -1
  50. package/src/pages-standard/TenantDetailPage.vue +79 -174
  51. package/src/pages-standard/TenantsPage.vue +8 -33
  52. package/src/pages-standard/UsersPage.vue +21 -54
  53. package/src/pages-standard/bundles-page/BundlesToolbar.vue +67 -0
  54. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
  55. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +54 -62
  56. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
  57. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
  58. package/src/pages-standard/plans-page/PlansPageToast.vue +5 -5
  59. package/src/pages-standard/sa-theme.css +85 -29
  60. package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
  61. package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
  62. package/src/pages-standard/tenant-detail/TenantUsers.vue +21 -0
  63. package/src/pages-standard/tenant-detail/types.ts +38 -0
  64. package/src/vue/use-mfa-prompt.ts +72 -0
  65. package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
  66. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
@@ -86,9 +86,11 @@
86
86
  </q-drawer>
87
87
 
88
88
  <q-page-container>
89
- <div :class="['sa-admin-content', { 'sa-admin-content--fullbleed': isFullbleed }]">
89
+ <!-- The document's only <main>. Pages must not add one of their
90
+ own, and must not use QPage, which renders <main> itself. -->
91
+ <main :class="['sa-admin-content', { 'sa-admin-content--fullbleed': isFullbleed }]">
90
92
  <router-view />
91
- </div>
93
+ </main>
92
94
  </q-page-container>
93
95
  </q-layout>
94
96
  </template>
@@ -1,20 +1,19 @@
1
1
  <template>
2
- <div class="sa-audit">
3
- <header class="sa-audit__head">
4
- <div>
5
- <h1 class="sa-audit__title">{{ msg.title }}</h1>
6
- <p class="sa-audit__sub">
7
- {{ formatMessage(msg.subtitle, { count: rows.length }) }}
8
- </p>
9
- </div>
10
- <q-btn
11
- unelevated
12
- color="primary"
13
- icon="refresh"
14
- :label="msg.filterButton"
15
- @click="reload"
16
- />
17
- </header>
2
+ <AdminPage class="sa-audit">
3
+ <AdminHero
4
+ :title="msg.title"
5
+ :subtitle="formatMessage(msg.subtitle, { count: rows.length })"
6
+ >
7
+ <template #actions>
8
+ <q-btn
9
+ unelevated
10
+ color="primary"
11
+ icon="refresh"
12
+ :label="msg.filterButton"
13
+ @click="reload"
14
+ />
15
+ </template>
16
+ </AdminHero>
18
17
 
19
18
  <div class="sa-audit__body">
20
19
  <div class="sa-audit__filter">
@@ -89,11 +88,13 @@
89
88
  </q-card-actions>
90
89
  </q-card>
91
90
  </q-dialog>
92
- </div>
91
+ </AdminPage>
93
92
  </template>
94
93
 
95
94
  <script setup lang="ts">
96
95
  import { computed, onMounted, reactive, ref } from 'vue';
96
+ import AdminHero from '../components/admin-page/AdminHero.vue';
97
+ import AdminPage from '../components/admin-page/AdminPage.vue';
97
98
  import { formatMessage } from '../client/i18n/format.js';
98
99
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
99
100
 
@@ -206,32 +207,6 @@ function formatTs(iso: string | null | undefined): string {
206
207
  </script>
207
208
 
208
209
  <style scoped>
209
- .sa-audit {
210
- min-height: calc(100vh - 56px);
211
- background: var(--sa-bg-app, #f1f5f9);
212
- }
213
- .sa-audit__head {
214
- padding: 20px 28px 8px;
215
- display: flex;
216
- align-items: flex-start;
217
- justify-content: space-between;
218
- gap: 16px;
219
- }
220
- .sa-audit__title {
221
- margin: 0;
222
- font-family: var(--sa-font-head, system-ui, sans-serif);
223
- font-weight: 700;
224
- font-size: 22px;
225
- color: var(--sa-heading, #0f172a);
226
- }
227
- .sa-audit__sub {
228
- margin: 4px 0 0;
229
- color: var(--sa-muted-dark, #475569);
230
- font-size: 13.5px;
231
- }
232
- .sa-audit__body {
233
- padding: 12px 28px 28px;
234
- }
235
210
  .sa-audit__filter {
236
211
  display: flex;
237
212
  gap: 10px;
@@ -1,13 +1,17 @@
1
1
  <template>
2
- <div class="sa-bundles">
3
- <BundlesHeader
4
- :loading="loading"
5
- :display-locale="displayLocale"
6
- :locales="locales"
7
- @create="openCreatePanel"
8
- @refresh="load"
9
- @update:display-locale="(value) => (displayLocale = value)"
10
- />
2
+ <AdminPage class="sa-bundles">
3
+ <AdminHero :title="msg.header.title" :subtitle="msg.header.subtitle">
4
+ <template #actions>
5
+ <BundlesToolbar
6
+ :loading="loading"
7
+ :display-locale="displayLocale"
8
+ :locales="locales"
9
+ @create="openCreatePanel"
10
+ @refresh="load"
11
+ @update:display-locale="(value) => (displayLocale = value)"
12
+ />
13
+ </template>
14
+ </AdminHero>
11
15
 
12
16
  <q-banner v-if="error" class="sa-bundles__error" rounded>
13
17
  <template #avatar><q-icon name="warning" color="negative" /></template>
@@ -131,7 +135,7 @@
131
135
  :submit="onPublishSubmit"
132
136
  @submitted="onPublishSubmitted"
133
137
  />
134
- </div>
138
+ </AdminPage>
135
139
  </template>
136
140
 
137
141
  <script setup lang="ts">
@@ -174,7 +178,9 @@ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
174
178
  import BundleAccordionList from './bundles-page/BundleAccordionList.vue';
175
179
  import BundleDetailPanel from './bundles-page/BundleDetailPanel.vue';
176
180
  import BundlesFilterBar from './bundles-page/BundlesFilterBar.vue';
177
- import BundlesHeader from './bundles-page/BundlesHeader.vue';
181
+ import AdminHero from '../components/admin-page/AdminHero.vue';
182
+ import AdminPage from '../components/admin-page/AdminPage.vue';
183
+ import BundlesToolbar from './bundles-page/BundlesToolbar.vue';
178
184
  import BundlesKpis from './bundles-page/BundlesKpis.vue';
179
185
  import type {
180
186
  BundleEditForm,
@@ -643,33 +649,10 @@ const classifyDiff = computed(() => props.classifyDiff);
643
649
 
644
650
  <style>
645
651
  .sa-bundles {
646
- padding: 16px;
647
652
  display: flex;
648
653
  flex-direction: column;
649
654
  gap: 14px;
650
655
  }
651
- .sa-bundles__head {
652
- display: flex;
653
- justify-content: space-between;
654
- align-items: flex-end;
655
- gap: 16px;
656
- }
657
- .sa-bundles__title {
658
- font-size: 20px;
659
- font-weight: 700;
660
- margin: 0;
661
- }
662
- .sa-bundles__sub {
663
- margin: 4px 0 0;
664
- color: #64748b;
665
- font-size: 12px;
666
- max-width: 560px;
667
- }
668
- .sa-bundles__head-actions {
669
- display: flex;
670
- align-items: center;
671
- gap: 12px;
672
- }
673
656
  .sa-bundles__error {
674
657
  border-left: 4px solid #dc2626;
675
658
  }
@@ -1,11 +1,7 @@
1
1
  <template>
2
- <div class="sa-dashboard">
3
- <header class="sa-page-head sa-dashboard__head">
4
- <div>
5
- <h1 class="sa-page-head__title">{{ msg.title }}</h1>
6
- <p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
7
- </div>
8
- <div class="sa-page-head__actions">
2
+ <AdminPage class="sa-dashboard">
3
+ <AdminHero :title="msg.title" :subtitle="subtitle">
4
+ <template #actions>
9
5
  <q-btn
10
6
  flat
11
7
  dense
@@ -14,8 +10,8 @@
14
10
  :aria-label="common.reload"
15
11
  @click="reload"
16
12
  />
17
- </div>
18
- </header>
13
+ </template>
14
+ </AdminHero>
19
15
 
20
16
  <q-banner v-if="error" class="bg-red-1 text-red-9 q-mb-md" rounded>
21
17
  <strong>{{ common.error }}:</strong> {{ error.message }}
@@ -47,15 +43,15 @@
47
43
  </div>
48
44
 
49
45
  <div v-if="distributions && distributions.length > 0" class="sa-dashboard__rows">
50
- <section
46
+ <AdminSection
51
47
  v-for="dist in distributions"
52
48
  :key="dist.id"
53
- class="sa-dashboard__card sa-dashboard__row-card"
49
+ :title="dist.label"
50
+ class="sa-card sa-dashboard__row-card"
54
51
  >
55
- <header class="sa-dashboard__row-head">
56
- <h2>{{ dist.label }}</h2>
57
- <span v-if="dist.total" class="sa-dashboard__count">{{ dist.total }}</span>
58
- </header>
52
+ <template v-if="dist.total" #actions>
53
+ <span class="sa-dashboard__count">{{ dist.total }}</span>
54
+ </template>
59
55
  <ul class="sa-dashboard__bar-list">
60
56
  <li
61
57
  v-for="entry in dist.entries"
@@ -75,16 +71,14 @@
75
71
  <span class="sa-dashboard__bar-count">{{ entry.value }}</span>
76
72
  </li>
77
73
  </ul>
78
- </section>
74
+ </AdminSection>
79
75
  </div>
80
76
 
81
- <section
77
+ <AdminSection
82
78
  v-if="resolvedShortcuts.length > 0"
83
- class="sa-dashboard__card sa-dashboard__shortcuts"
79
+ :title="msg.shortcutsTitle"
80
+ class="sa-card sa-dashboard__shortcuts"
84
81
  >
85
- <header class="sa-dashboard__row-head">
86
- <h2>{{ msg.shortcutsTitle }}</h2>
87
- </header>
88
82
  <div class="sa-dashboard__shortcut-grid">
89
83
  <a
90
84
  v-for="s in resolvedShortcuts"
@@ -99,10 +93,10 @@
99
93
  </div>
100
94
  </a>
101
95
  </div>
102
- </section>
96
+ </AdminSection>
103
97
 
104
98
  <slot name="after-kpis" />
105
- </div>
99
+ </AdminPage>
106
100
  </template>
107
101
 
108
102
  <script setup lang="ts">
@@ -114,6 +108,9 @@ import { formatMessage } from '../client/i18n/format.js';
114
108
  import { SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
115
109
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
116
110
  import { useSuperAdminHttp } from '../vue/use-super-admin-context.js';
111
+ import AdminHero from '../components/admin-page/AdminHero.vue';
112
+ import AdminPage from '../components/admin-page/AdminPage.vue';
113
+ import AdminSection from '../components/admin-page/AdminSection.vue';
117
114
 
118
115
  // Platform standard page: Dashboard.
119
116
  //
@@ -403,16 +400,7 @@ function barWidth(value: number, max?: number): number {
403
400
  </script>
404
401
 
405
402
  <style scoped>
406
- .sa-dashboard {
407
- min-height: calc(100vh - 56px);
408
- background: var(--sa-bg-app, #f1f5f9);
409
- padding: 20px 28px 28px;
410
- }
411
403
  /* Header look comes from the global .sa-page-head — only a margin tweak here. */
412
- .sa-dashboard__head {
413
- margin-bottom: 4px;
414
- }
415
-
416
404
  .sa-dashboard__strip {
417
405
  display: grid;
418
406
  /* Responsive: 4 cards while there is space, then automatically 3 → 2 → 1. */
@@ -494,25 +482,6 @@ function barWidth(value: number, max?: number): number {
494
482
  }
495
483
  }
496
484
 
497
- .sa-dashboard__card {
498
- background: #fff;
499
- border: 1px solid var(--sa-border, #e2e8f0);
500
- border-radius: 12px;
501
- padding: 16px 18px;
502
- }
503
- .sa-dashboard__row-head {
504
- display: flex;
505
- align-items: center;
506
- gap: 10px;
507
- margin-bottom: 12px;
508
- }
509
- .sa-dashboard__row-head h2 {
510
- margin: 0;
511
- font-family: var(--sa-font-head, system-ui, sans-serif);
512
- font-size: 14px;
513
- font-weight: 700;
514
- color: var(--sa-heading, #0f172a);
515
- }
516
485
  .sa-dashboard__count {
517
486
  background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
518
487
  color: var(--sa-primary, #3f6bff);
@@ -1,6 +1,20 @@
1
1
  <template>
2
- <div class="sa-discovery">
3
- <DiscoveryHeader :loading="loading" @run-discovery="onRunDiscovery" />
2
+ <AdminPage class="sa-discovery">
3
+ <AdminHero :title="msg.title">
4
+ <template #subtitle>
5
+ {{ msg.subtitleLead }} <b>{{ msg.subtitleEmphasis }}</b> {{ msg.subtitleTail }}
6
+ </template>
7
+ <template #actions>
8
+ <q-btn
9
+ unelevated
10
+ color="primary"
11
+ icon="bolt"
12
+ :label="msg.runDiscovery"
13
+ :loading="loading"
14
+ @click="onRunDiscovery"
15
+ />
16
+ </template>
17
+ </AdminHero>
4
18
 
5
19
  <DiscoveryMetaBanner
6
20
  :app-label="appLabel"
@@ -53,11 +67,15 @@
53
67
  />
54
68
  </div>
55
69
 
56
- <div v-for="group in featureGroups" :key="group.label" class="sa-discovery__group">
57
- <div class="sa-discovery__group-head">
58
- <span class="sa-discovery__group-title">{{ group.label }}</span>
70
+ <AdminSection
71
+ v-for="group in featureGroups"
72
+ :key="group.label"
73
+ :title="group.label"
74
+ class="sa-discovery__group"
75
+ >
76
+ <template #actions>
59
77
  <span class="sa-discovery__group-count">{{ group.features.length }}</span>
60
- </div>
78
+ </template>
61
79
  <div class="sa-discovery__cardlist">
62
80
  <DiscoveryFeatureCard
63
81
  v-for="f in group.features"
@@ -74,19 +92,19 @@
74
92
  @feature-locale="onFeatureLocale"
75
93
  />
76
94
  </div>
77
- </div>
95
+ </AdminSection>
78
96
  <div v-if="filteredFeatures.length === 0" class="sa-discovery__empty-row">
79
97
  {{ msg.noFeaturesMatchFilters }}
80
98
  </div>
81
99
 
82
- <div v-if="orphanCaps.length" class="sa-discovery__group">
83
- <div class="sa-discovery__group-head">
84
- <span class="sa-discovery__group-title sa-discovery__group-title--orphan">
85
- <q-icon name="warning" size="14px" />
86
- {{ msg.orphansTitle }}
87
- </span>
100
+ <AdminSection
101
+ v-if="orphanCaps.length"
102
+ :title="msg.orphansTitle"
103
+ class="sa-discovery__group sa-discovery__group--orphan"
104
+ >
105
+ <template #actions>
88
106
  <span class="sa-discovery__group-count">{{ orphanCaps.length }}</span>
89
- </div>
107
+ </template>
90
108
  <p class="sa-discovery__orphan-hint">
91
109
  {{ msg.orphanHint.before }} <code>feature:</code>{{ msg.orphanHint.middle }}
92
110
  <code>@ImplementsCapability</code>{{ msg.orphanHint.after }}
@@ -95,7 +113,7 @@
95
113
  :capabilities="orphanCaps"
96
114
  :declared-at-by-key="declaredAtByKey"
97
115
  />
98
- </div>
116
+ </AdminSection>
99
117
  </q-tab-panel>
100
118
 
101
119
  <q-tab-panel name="quotas" class="sa-discovery__panel">
@@ -117,7 +135,7 @@
117
135
  </div>
118
136
  </q-tab-panel>
119
137
  </q-tab-panels>
120
- </div>
138
+ </AdminPage>
121
139
  </template>
122
140
 
123
141
  <script setup lang="ts">
@@ -135,7 +153,9 @@ import type {
135
153
  } from '@saasicat/types';
136
154
  import DiscoveryCapList from './discovery-page/DiscoveryCapList.vue';
137
155
  import DiscoveryFeatureCard from './discovery-page/DiscoveryFeatureCard.vue';
138
- import DiscoveryHeader from './discovery-page/DiscoveryHeader.vue';
156
+ import AdminHero from '../components/admin-page/AdminHero.vue';
157
+ import AdminSection from '../components/admin-page/AdminSection.vue';
158
+ import AdminPage from '../components/admin-page/AdminPage.vue';
139
159
  import DiscoveryKpis from './discovery-page/DiscoveryKpis.vue';
140
160
  import DiscoveryMetaBanner from './discovery-page/DiscoveryMetaBanner.vue';
141
161
  import DiscoveryQuotaCard from './discovery-page/DiscoveryQuotaCard.vue';
@@ -438,23 +458,6 @@ onMounted(() => {
438
458
  flex-direction: column;
439
459
  gap: 14px;
440
460
  }
441
- .sa-discovery__head {
442
- display: flex;
443
- justify-content: space-between;
444
- align-items: flex-end;
445
- gap: 16px;
446
- }
447
- .sa-discovery__title {
448
- font-size: 20px;
449
- font-weight: 700;
450
- margin: 0;
451
- }
452
- .sa-discovery__sub {
453
- margin: 4px 0 0;
454
- color: #64748b;
455
- font-size: 12px;
456
- max-width: 640px;
457
- }
458
461
  .sa-discovery__banner {
459
462
  display: flex;
460
463
  justify-content: space-between;
@@ -559,23 +562,7 @@ onMounted(() => {
559
562
  .sa-discovery__group {
560
563
  margin-bottom: 16px;
561
564
  }
562
- .sa-discovery__group-head {
563
- display: flex;
564
- align-items: center;
565
- gap: 8px;
566
- margin-bottom: 8px;
567
- }
568
- .sa-discovery__group-title {
569
- font-size: 11px;
570
- font-weight: 700;
571
- text-transform: uppercase;
572
- letter-spacing: 0.5px;
573
- color: #475569;
574
- display: flex;
575
- align-items: center;
576
- gap: 5px;
577
- }
578
- .sa-discovery__group-title--orphan {
565
+ .sa-discovery__group--orphan .sa-section__title {
579
566
  color: #b45309;
580
567
  }
581
568
  .sa-discovery__group-count {
@@ -1,12 +1,10 @@
1
1
  <template>
2
- <div class="sa-emh">
3
- <header class="sa-emh__head">
4
- <div>
5
- <h1 class="sa-emh__title">{{ resolvedTitle }}</h1>
6
- <p class="sa-emh__sub">{{ msg.history.subtitle }}</p>
7
- </div>
8
- <q-btn flat icon="refresh" :label="common.reload" @click="applyFilter" />
9
- </header>
2
+ <AdminPage class="sa-emh">
3
+ <AdminHero :title="resolvedTitle" :subtitle="msg.history.subtitle">
4
+ <template #actions>
5
+ <q-btn flat icon="refresh" :label="common.reload" @click="applyFilter" />
6
+ </template>
7
+ </AdminHero>
10
8
 
11
9
  <div class="sa-emh__body">
12
10
  <div class="sa-emh__filter">
@@ -206,18 +204,21 @@
206
204
 
207
205
  <MfaPromptDialog
208
206
  v-if="requireMfaForWrite"
209
- :model-value="showMfa"
210
- :description="mfaDescription"
211
- :error="mfaError"
207
+ :model-value="mfa.show.value"
208
+ :description="mfa.description.value"
209
+ :error="mfa.error.value"
212
210
  :setup-hint="mfaSetupHint"
213
- @update:model-value="onMfaDialogVisibility"
214
- @confirm="onMfaConfirm"
211
+ @update:model-value="mfa.onVisibility"
212
+ @confirm="mfa.onConfirm"
215
213
  />
216
- </div>
214
+ </AdminPage>
217
215
  </template>
218
216
 
219
217
  <script setup lang="ts">
220
218
  import { computed, reactive, ref } from 'vue';
219
+ import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
220
+ import AdminHero from '../components/admin-page/AdminHero.vue';
221
+ import AdminPage from '../components/admin-page/AdminPage.vue';
221
222
  import { useSuperAdminNotify } from '../quasar/notify.js';
222
223
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
223
224
  import { formatMessage } from '../client/i18n/format.js';
@@ -317,10 +318,7 @@ const confirmDeleteOpen = ref(false);
317
318
  const pendingDeleteId = ref<string | null>(null);
318
319
 
319
320
  // MFA loop analogous to PlatformEmailPage (promise-resolver pattern).
320
- const showMfa = ref(false);
321
- const mfaError = ref('');
322
- const mfaDescription = ref('');
323
- let pendingMfaResolve: ((code: string | null) => void) | null = null;
321
+ const mfa = useMfaPrompt();
324
322
 
325
323
  // Sequence guard: with rapidly changing filters a stale (out-of-order)
326
324
  // response must not overwrite the newest.
@@ -428,48 +426,25 @@ async function runWrite<T>(
428
426
  }
429
427
  }
430
428
  for (;;) {
431
- const code = await promptMfa(label);
429
+ const code = await mfa.prompt(label);
432
430
  if (code === null) return { ok: false };
433
431
  try {
434
432
  const result = await invoke(code);
435
- showMfa.value = false;
433
+ mfa.show.value = false;
436
434
  return { ok: true, result };
437
435
  } catch (err) {
438
436
  const status = (err as { response?: { status?: number } })?.response?.status;
439
437
  if (status === 401) {
440
- mfaError.value = shell.value.mfa.invalidCode;
438
+ mfa.error.value = shell.value.mfa.invalidCode;
441
439
  continue;
442
440
  }
443
- showMfa.value = false;
441
+ mfa.show.value = false;
444
442
  notify('negative', errMsg(err));
445
443
  return { ok: false };
446
444
  }
447
445
  }
448
446
  }
449
447
 
450
- function promptMfa(description: string): Promise<string | null> {
451
- return new Promise((resolve) => {
452
- mfaDescription.value = description;
453
- mfaError.value = '';
454
- showMfa.value = true;
455
- pendingMfaResolve = (code) => {
456
- pendingMfaResolve = null;
457
- resolve(code);
458
- };
459
- });
460
- }
461
-
462
- function onMfaConfirm(code: string): void {
463
- pendingMfaResolve?.(code);
464
- }
465
-
466
- function onMfaDialogVisibility(open: boolean): void {
467
- showMfa.value = open;
468
- if (!open && pendingMfaResolve) {
469
- pendingMfaResolve(null);
470
- }
471
- }
472
-
473
448
  function statusColor(status: EmailHistoryStatus): string {
474
449
  switch (status) {
475
450
  case 'SENT':
@@ -521,32 +496,6 @@ function formatTs(iso: string | null | undefined): string {
521
496
  </script>
522
497
 
523
498
  <style scoped>
524
- .sa-emh {
525
- min-height: calc(100vh - 56px);
526
- background: var(--sa-bg-app, #f1f5f9);
527
- }
528
- .sa-emh__head {
529
- padding: 20px 28px 8px;
530
- display: flex;
531
- align-items: flex-start;
532
- justify-content: space-between;
533
- gap: 16px;
534
- }
535
- .sa-emh__title {
536
- margin: 0;
537
- font-family: var(--sa-font-head, system-ui, sans-serif);
538
- font-weight: 700;
539
- font-size: 22px;
540
- color: var(--sa-heading, #0f172a);
541
- }
542
- .sa-emh__sub {
543
- margin: 4px 0 0;
544
- color: var(--sa-muted-dark, #475569);
545
- font-size: 13.5px;
546
- }
547
- .sa-emh__body {
548
- padding: 12px 28px 28px;
549
- }
550
499
  .sa-emh__filter {
551
500
  display: flex;
552
501
  gap: 10px;