@saasicat/ui-vue 0.19.0 → 0.22.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 (145) hide show
  1. package/dist/{catalog-JXdKdIqG.d.cts → catalog-DJcfm5N2.d.cts} +21 -7
  2. package/dist/{catalog-JXdKdIqG.d.ts → catalog-DJcfm5N2.d.ts} +21 -7
  3. package/dist/{chunk-DPTIOQXX.js → chunk-F7NRM6KI.js} +56 -28
  4. package/dist/{chunk-RVG6LQHL.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-ORX3ZG47.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +134 -29
  7. package/dist/client/index.d.cts +53 -3
  8. package/dist/client/index.d.ts +53 -3
  9. package/dist/client/index.js +14 -2
  10. package/dist/index.cjs +321 -162
  11. package/dist/index.d.cts +30 -8
  12. package/dist/index.d.ts +30 -8
  13. package/dist/index.js +176 -133
  14. package/dist/quasar/index.cjs +78 -29
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +6 -2
  18. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  19. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  20. package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
  22. package/package.json +18 -8
  23. package/src/client/i18n/messages/bundles.ts +0 -2
  24. package/src/client/i18n/messages/common.ts +24 -0
  25. package/src/client/i18n/messages/discovery.ts +2 -2
  26. package/src/client/i18n/messages/marketing.ts +2 -4
  27. package/src/client/i18n/messages/plan-detail.ts +0 -4
  28. package/src/client/i18n/messages/plans.ts +20 -8
  29. package/src/client/i18n/messages/tenants.ts +0 -2
  30. package/src/client/index.ts +1 -0
  31. package/src/client/manifest-loader.ts +1 -1
  32. package/src/client/resolve-plans.ts +144 -0
  33. package/src/components/BundleVersionPublishDialog.vue +6 -6
  34. package/src/components/KvBlock.vue +4 -4
  35. package/src/components/MarketingPromotionsTab.vue +34 -48
  36. package/src/components/MfaPromptDialog.vue +8 -2
  37. package/src/components/TenantActionConfirmDialog.vue +4 -4
  38. package/src/components/VersionDiffPreview.vue +7 -7
  39. package/src/components/admin-page/AdminBody.vue +40 -0
  40. package/src/components/admin-page/AdminFilters.vue +12 -0
  41. package/src/components/admin-page/AdminHero.vue +49 -0
  42. package/src/components/admin-page/AdminKpi.vue +84 -0
  43. package/src/components/admin-page/AdminPage.vue +16 -0
  44. package/src/components/admin-page/AdminPaginator.vue +195 -0
  45. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  46. package/src/components/admin-page/AdminSection.vue +53 -0
  47. package/src/components/admin-page/AdminStatistics.vue +40 -0
  48. package/src/components/admin-page/AdminTable.vue +175 -0
  49. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  50. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  51. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  52. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  53. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  54. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  55. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  56. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  57. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  58. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  59. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  60. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  61. package/src/components/plan/PlanGrid.vue +1 -1
  62. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  63. package/src/components/plan-detail/PlanAuditLog.vue +7 -8
  64. package/src/components/plan-detail/PlanDetail.vue +95 -292
  65. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  66. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  67. package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
  68. package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
  69. package/src/components/plan-list/PlanList.vue +206 -457
  70. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  71. package/src/components/plan-review/PlanReview.vue +191 -275
  72. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  73. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  74. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  75. package/src/index.ts +1 -0
  76. package/src/pages-standard/AdminLayout.vue +38 -12
  77. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  78. package/src/pages-standard/AuditPage.vue +65 -109
  79. package/src/pages-standard/BundlesPage.vue +146 -179
  80. package/src/pages-standard/DashboardPage.vue +59 -154
  81. package/src/pages-standard/DiscoveryPage.vue +172 -227
  82. package/src/pages-standard/EmailHistoryPage.vue +118 -159
  83. package/src/pages-standard/MarketingCatalogPage.vue +399 -440
  84. package/src/pages-standard/PilotsPage.vue +69 -117
  85. package/src/pages-standard/PlansPage.vue +240 -112
  86. package/src/pages-standard/PlatformEmailPage.vue +56 -91
  87. package/src/pages-standard/PromoCodeDetailPage.vue +49 -74
  88. package/src/pages-standard/PromoCodesPage.vue +95 -128
  89. package/src/pages-standard/SubscriptionsPage.vue +19 -52
  90. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  91. package/src/pages-standard/SuperAdminSetupWizard.vue +22 -18
  92. package/src/pages-standard/TenantDetailPage.vue +100 -191
  93. package/src/pages-standard/TenantsPage.vue +190 -295
  94. package/src/pages-standard/UsersPage.vue +89 -157
  95. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  96. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  97. package/src/pages-standard/bundles-page/BundlesToolbar.vue +63 -0
  98. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  99. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  100. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  101. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  102. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  103. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
  104. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +43 -62
  105. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
  106. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
  107. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  108. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  109. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  110. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  111. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  112. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  113. package/src/pages-standard/sa-theme.css +552 -86
  114. package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
  115. package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
  116. package/src/pages-standard/tenant-detail/TenantUsers.vue +15 -0
  117. package/src/pages-standard/tenant-detail/types.ts +38 -0
  118. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  119. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  120. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  121. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  122. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  123. package/src/quasar/create-super-admin-app.ts +4 -0
  124. package/src/vue/entitlement-provider.ts +0 -1
  125. package/src/vue/feature-router-guard.ts +0 -1
  126. package/src/vue/super-admin-context.ts +106 -1
  127. package/src/vue/use-api-list.ts +2 -1
  128. package/src/vue/use-mfa-prompt.ts +72 -0
  129. package/src/vue/use-pagination.ts +63 -0
  130. package/src/vue/use-plans.ts +1 -2
  131. package/src/vue/use-sign-out.ts +90 -0
  132. package/src/vue/use-tenant-billing.ts +1 -1
  133. package/src/vue/use-tenant-manifest.ts +0 -1
  134. package/src/components/KpiCard.vue +0 -53
  135. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1012
  136. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  137. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -100
  138. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  139. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  140. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  141. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -251
  142. package/src/components/plan-cockpit/types.ts +0 -72
  143. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
  144. package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
  145. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
@@ -1,84 +1,52 @@
1
1
  <template>
2
- <header class="pve-bar">
3
- <div class="pve-bar-left">
4
- <div class="pve-titlechip">
5
- <span class="pve-titlechip-kicker">{{ msg.header.planKicker }}</span>
2
+ <AdminHero :title="title">
3
+ <template #before-title>
4
+ <span class="pve-kicker">
5
+ {{ msg.header.planKicker }}
6
6
  <span class="pve-chip pve-chip--plan">{{ planKey }}</span>
7
- </div>
8
- <h2 class="pve-title">{{ title }}</h2>
9
- <span class="pve-chip pve-chip--draft pve-chip--dot">{{ msg.header.draftChip }}</span>
10
- <span class="pve-bar-note">
11
- {{ msg.header.supersedeNoteBefore }}
12
- <code class="pve-mono">{{ predecessorValidUntilHint }}</code>
13
- {{ msg.header.supersedeNoteAfter }}
14
7
  </span>
15
- </div>
16
- <div class="pve-bar-right">
8
+ </template>
9
+ <template #subtitle>
10
+ <span class="pve-chip pve-chip--draft pve-chip--dot">{{ msg.header.draftChip }}</span>
11
+ {{ msg.header.supersedeNoteBefore }}
12
+ <code class="pve-mono">{{ predecessorValidUntilHint }}</code>
13
+ {{ msg.header.supersedeNoteAfter }}
14
+ </template>
15
+ <template #actions>
17
16
  <button
18
- class="pve-btn"
17
+ class="sa-btn"
19
18
  type="button"
20
19
  :disabled="!hasPredecessor"
21
20
  :title="hasPredecessor ? undefined : msg.header.noPredecessorHint"
22
21
  @click="$emit('showDiff')"
23
22
  >
24
- <span class="pve-ico" aria-hidden="true">
25
- <svg
26
- width="14"
27
- height="14"
28
- viewBox="0 0 24 24"
29
- fill="none"
30
- stroke="currentColor"
31
- stroke-width="2"
32
- >
33
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
34
- <circle cx="12" cy="12" r="3" />
35
- </svg>
36
- </span>
23
+ <q-icon name="visibility" size="16px" />
37
24
  <span>{{ msg.header.diffButton }}</span>
38
25
  </button>
39
- <button class="pve-btn" type="button" @click="$emit('cancel')">
40
- <span class="pve-ico" style="transform: rotate(180deg)" aria-hidden="true">
41
- <svg
42
- width="14"
43
- height="14"
44
- viewBox="0 0 24 24"
45
- fill="none"
46
- stroke="currentColor"
47
- stroke-width="2"
48
- >
49
- <path d="M5 12h14M13 5l7 7-7 7" />
50
- </svg>
51
- </span>
26
+ <button class="sa-btn" type="button" @click="$emit('cancel')">
27
+ <q-icon name="arrow_back" size="16px" />
52
28
  <span>{{ common.back }}</span>
53
29
  </button>
54
30
  <button
55
- class="pve-btn pve-btn--primary"
31
+ class="sa-btn sa-btn--primary"
56
32
  type="button"
57
33
  :disabled="!canSave"
58
34
  @click="$emit('save')"
59
35
  >
60
36
  <span>{{ msg.header.saveButton }}</span>
61
- <span class="pve-ico" aria-hidden="true">
62
- <svg
63
- width="14"
64
- height="14"
65
- viewBox="0 0 24 24"
66
- fill="none"
67
- stroke="currentColor"
68
- stroke-width="2"
69
- >
70
- <path d="M5 12h14M13 5l7 7-7 7" />
71
- </svg>
72
- </span>
37
+ <q-icon name="arrow_forward" size="16px" />
73
38
  </button>
74
- </div>
75
- </header>
39
+ </template>
40
+ </AdminHero>
76
41
  </template>
77
42
 
78
43
  <script setup lang="ts">
79
44
  import { computed } from 'vue';
80
45
  import { formatMessage } from '../../client/i18n/format.js';
81
46
  import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
47
+ // The editor takes over the page hero while it is open: its actions depend on
48
+ // the draft's validity and on the diff dialog, both of which live in this view.
49
+ import AdminHero from '../admin-page/AdminHero.vue';
82
50
 
83
51
  const props = defineProps<{
84
52
  planKey: string;
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export * from './vue/use-super-admin-i18n.js';
39
39
  // ---------------------------------------------------------------------------
40
40
  export * from './vue/create-admin-routes.js';
41
41
  export * from './vue/use-api-list.js';
42
+ export * from './vue/use-pagination.js';
42
43
  export * from './vue/use-tenants.js';
43
44
  export * from './vue/use-audit-entries.js';
44
45
  export * from './vue/use-entitlement.js';
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <q-layout view="lHh Lpr lFf">
2
+ <q-layout view="lHh Lpr lFf" class="sa-admin-layout">
3
3
  <div v-if="isProduction" class="sa-admin-banner sa-admin-banner--prod">
4
4
  <q-icon name="warning" size="14px" />
5
5
  <strong>PRODUCTION</strong> — {{ msg.header.productionWarning }}
@@ -24,7 +24,7 @@
24
24
  <div class="text-body2 text-weight-semibold">{{ userName }}</div>
25
25
  <div class="text-caption sa-admin-user__email">{{ userEmail }}</div>
26
26
  </div>
27
- <q-btn flat round dense icon="logout" size="sm" @click="emit('logout')">
27
+ <q-btn flat round dense icon="logout" size="sm" @click="onLogoutClick">
28
28
  <q-tooltip>{{ msg.header.logout }}</q-tooltip>
29
29
  </q-btn>
30
30
  </div>
@@ -86,21 +86,24 @@
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>
95
97
 
96
98
  <script setup lang="ts">
97
- import { computed, inject, ref } from 'vue';
99
+ import { computed, inject, ref, getCurrentInstance } from 'vue';
98
100
  import { useRoute } from 'vue-router';
99
101
  import type { AdminManifest, StandardPageKey } from '@saasicat/types';
100
102
  import { buildRoutes, buildSidebar, defaultSectionOrder } from '../client/nav-builder.js';
101
103
  import LocaleSwitcher from '../components/LocaleSwitcher.vue';
102
104
  import { SUPER_ADMIN_BRAND_KEY, SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
103
105
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
106
+ import { useSignOut } from '../vue/use-sign-out.js';
104
107
  import { ADMIN_UI_VERSION } from '../client/version.js';
105
108
 
106
109
  // SuperAdmin layout — universal platform shell for all consumer apps.
@@ -187,6 +190,29 @@ const emit = defineEmits<{
187
190
  (e: 'logout'): void;
188
191
  }>();
189
192
 
193
+ const signOut = useSignOut();
194
+ const instance = getCurrentInstance();
195
+
196
+ /**
197
+ * Signs out, and only emits instead when the app is actually listening.
198
+ *
199
+ * The layout is mounted as a plain route record in every consumer we have
200
+ * (`{ path: '/admin', component: AdminLayout }`), and a route record attaches
201
+ * no listeners — so `emit('logout')` alone made the header's sign-out button
202
+ * do nothing at all. Apps that pass `@logout` keep full control.
203
+ */
204
+ function onLogoutClick(): void | Promise<void> {
205
+ // Both spellings: `@logout` lands on `onLogout`, `@logout.once` on
206
+ // `onLogoutOnce`. Checking only the first would run the platform default
207
+ // and never emit — silently ignoring a listener the consumer did attach.
208
+ const listeners = instance?.vnode.props ?? {};
209
+ if (listeners.onLogout || listeners.onLogoutOnce) {
210
+ emit('logout');
211
+ return;
212
+ }
213
+ return signOut();
214
+ }
215
+
190
216
  const route = useRoute();
191
217
  const msg = useSaMessages('shell');
192
218
  const { messages } = useSuperAdminI18n();
@@ -293,7 +319,7 @@ const currentPageTitle = computed(() => {
293
319
  }
294
320
 
295
321
  .sa-admin-header {
296
- background: var(--sa-admin-header-bg, linear-gradient(90deg, #0f172a, #1e293b));
322
+ background: var(--sa-admin-header-bg, linear-gradient(90deg, var(--sa-heading), #1e293b));
297
323
  color: var(--sa-admin-header-fg, #fff);
298
324
  }
299
325
  .sa-admin-header__sub {
@@ -304,7 +330,7 @@ const currentPageTitle = computed(() => {
304
330
 
305
331
  .sa-admin-badge {
306
332
  background: var(--sa-admin-badge-bg, #f59e0b);
307
- color: var(--sa-admin-badge-fg, #0f172a);
333
+ color: var(--sa-admin-badge-fg, var(--sa-heading));
308
334
  font-weight: 800;
309
335
  letter-spacing: 0.08em;
310
336
  font-size: 10px;
@@ -331,7 +357,7 @@ const currentPageTitle = computed(() => {
331
357
  .sa-admin-drawer :deep(.q-drawer__content),
332
358
  .sa-admin-drawer__stack {
333
359
  background: var(--sa-admin-drawer-bg, #0b1220);
334
- color: var(--sa-admin-drawer-fg, #f1f5f9);
360
+ color: var(--sa-admin-drawer-fg, var(--sa-border-soft));
335
361
  }
336
362
  .sa-admin-drawer__stack {
337
363
  display: flex;
@@ -351,7 +377,7 @@ const currentPageTitle = computed(() => {
351
377
  height: 36px;
352
378
  border-radius: 10px;
353
379
  background: var(--sa-admin-drawer-logo-bg, linear-gradient(135deg, #f59e0b, #d97706));
354
- color: var(--sa-admin-drawer-logo-fg, #0f172a);
380
+ color: var(--sa-admin-drawer-logo-fg, var(--sa-heading));
355
381
  font-weight: 800;
356
382
  display: flex;
357
383
  align-items: center;
@@ -376,7 +402,7 @@ const currentPageTitle = computed(() => {
376
402
  font-size: 10px;
377
403
  text-transform: uppercase;
378
404
  letter-spacing: 0.12em;
379
- color: var(--sa-admin-drawer-section-fg, #64748b);
405
+ color: var(--sa-admin-drawer-section-fg, var(--sa-muted));
380
406
  padding: 14px 18px 6px;
381
407
  font-weight: 600;
382
408
  }
@@ -394,7 +420,7 @@ const currentPageTitle = computed(() => {
394
420
  padding-right: 8px;
395
421
  }
396
422
  .sa-admin-drawer__list :deep(.q-item .q-icon) {
397
- color: #e2e8f0;
423
+ color: var(--sa-border);
398
424
  }
399
425
  .sa-admin-drawer__list :deep(.q-item:hover) {
400
426
  background: var(--sa-admin-drawer-hover-bg, rgba(245, 158, 11, 0.15));
@@ -423,7 +449,7 @@ const currentPageTitle = computed(() => {
423
449
  align-items: center;
424
450
  gap: 6px;
425
451
  margin-top: 6px;
426
- color: #e2e8f0;
452
+ color: var(--sa-border);
427
453
  text-decoration: none;
428
454
  }
429
455
  .sa-admin-drawer__doc:hover {
@@ -11,8 +11,8 @@
11
11
  <strong>{{ msg.manifestError.detailLabel }}</strong> {{ errorMessage }}
12
12
  </p>
13
13
  <div class="sa-manifest-error__actions">
14
- <q-btn unelevated color="primary" :label="common.reload" @click="onRetry" />
15
- <q-btn flat :label="msg.header.logout" @click="onLogout" />
14
+ <q-btn unelevated color="primary" :label="common.reload" @click="retry" />
15
+ <q-btn flat :label="msg.header.logout" @click="logout" />
16
16
  </div>
17
17
  </q-card-section>
18
18
  </q-card>
@@ -20,21 +20,91 @@
20
20
  </template>
21
21
 
22
22
  <script setup lang="ts">
23
+ import { inject } from 'vue';
24
+ import { useRouter } from 'vue-router';
23
25
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
26
+ import { useSignOut } from '../vue/use-sign-out.js';
27
+ import { SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY } from '../vue/super-admin-context.js';
24
28
 
25
29
  // Platform standard error page for `manifestGuard.errorRoute`.
26
- // Consumers pass `onRetry`/`onLogout` in as callbacks and decide
27
- // themselves what happens (e.g. manifestStore.clearCache() + ensureLoaded,
28
- // authStore.logout, router.replace).
30
+ //
31
+ // This page is mounted BY THE ROUTER (`createAdminRoutes({ adminErrorPage })`),
32
+ // and a route record passes no props. Every prop here must therefore be
33
+ // optional with a default that works standalone — a required prop would be
34
+ // unsatisfiable by construction, and its two buttons would call `undefined`
35
+ // at the exact moment the app is already in trouble.
36
+ //
37
+ // Apps that want app-specific behaviour (clear the manifest cache, call
38
+ // authStore.logout, route somewhere else) mount the page themselves with
39
+ // `props:` on the route record and override the callbacks.
29
40
 
30
41
  const msg = useSaMessages('shell');
31
42
  const common = useSaMessages('common');
43
+ const router = useRouter();
32
44
 
33
- defineProps<{
45
+ const props = defineProps<{
34
46
  errorMessage?: string | null;
35
- onRetry: () => Promise<void> | void;
36
- onLogout: () => void;
47
+ /** Default: fresh boot into `retryPath`, which re-runs the manifest guard. */
48
+ onRetry?: () => Promise<void> | void;
49
+ /** Default: navigate to the login route `createAdminRoutes` registers. */
50
+ onLogout?: () => void | Promise<void>;
51
+ /** Login path for the default `onLogout`. */
52
+ loginPath?: string;
53
+ /** Guarded route the default `onRetry` boots into. */
54
+ retryPath?: string;
37
55
  }>();
56
+
57
+ const signOut = useSignOut({ loginPath: props.loginPath });
58
+
59
+ // Provided by the shell when the app passes `manifestGuard.clearCache`. The
60
+ // page is route-mounted, so injection is the only route in.
61
+ const clearManifestCache = inject(SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY, undefined);
62
+
63
+ // Both handlers RETURN their promise rather than discarding it with `void`.
64
+ // Vue's event invoker forwards a returned rejection to the app's error handler;
65
+ // a dropped one becomes an unhandled rejection instead — and this is the page
66
+ // that renders when things are already going wrong, so a consumer's failing
67
+ // `onRetry` would vanish exactly where it matters most.
68
+ function retry(): void | Promise<void> {
69
+ if (props.onRetry) return props.onRetry();
70
+ return bootIntoGuardedRoute();
71
+ }
72
+
73
+ /**
74
+ * Discards the cached manifest, then boots into a route the guard protects.
75
+ *
76
+ * Two things have to happen, and each covers a failure the other does not:
77
+ *
78
+ * The target must be GUARDED. `createAdminRoutes` registers `/admin-error` as
79
+ * `meta.public` — it has to be, or the guard would redirect to itself forever
80
+ * — and the guard returns before it ever reaches `ensureLoaded()` on a public
81
+ * route. Reloading in place therefore re-renders the same error even after the
82
+ * backend has recovered.
83
+ *
84
+ * And the cache must go first. The loader keeps an ETag in storage, which a
85
+ * full document load does not touch. Its own documented failure — "server
86
+ * returned 304 but the cache body is missing" — would otherwise repeat exactly:
87
+ * same `If-None-Match`, same 304, same missing body, straight back here.
88
+ *
89
+ * `router.resolve` rather than a bare path so an app served under a base href
90
+ * lands inside its own app and not at the domain root.
91
+ */
92
+ async function bootIntoGuardedRoute(): Promise<void> {
93
+ if (clearManifestCache) {
94
+ try {
95
+ await clearManifestCache();
96
+ } catch (err) {
97
+ // Retrying with a stale cache is still better than not retrying.
98
+ console.warn('[SaaSiCat] Clearing the manifest cache failed before retry:', err);
99
+ }
100
+ }
101
+ window.location.assign(router.resolve(props.retryPath ?? '/admin').href);
102
+ }
103
+
104
+ function logout(): void | Promise<void> {
105
+ if (props.onLogout) return props.onLogout();
106
+ return signOut();
107
+ }
38
108
  </script>
39
109
 
40
110
  <style scoped>
@@ -59,7 +129,7 @@ defineProps<{
59
129
  .sa-manifest-error__lead,
60
130
  .sa-manifest-error__detail {
61
131
  margin: 0 0 12px;
62
- color: #475569;
132
+ color: var(--sa-muted-dark);
63
133
  line-height: 1.6;
64
134
  }
65
135
  .sa-manifest-error__detail {
@@ -1,64 +1,56 @@
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>
18
-
19
- <div class="sa-audit__body">
20
- <div class="sa-audit__filter">
21
- <q-input
22
- v-model="filter.actor"
23
- outlined
24
- dense
25
- :label="msg.filters.actor"
26
- clearable
27
- />
28
- <q-input
29
- v-model="filter.action"
30
- outlined
31
- dense
32
- :label="msg.filters.action"
33
- clearable
34
- />
35
- <q-input
36
- v-model="filter.entity"
37
- outlined
38
- dense
39
- :label="msg.filters.entity"
40
- clearable
41
- />
42
- <q-input
43
- v-model="filter.since"
44
- outlined
45
- dense
46
- type="date"
47
- :label="msg.filters.since"
48
- clearable
49
- />
50
- </div>
51
-
52
- <div class="sa-audit__card">
53
- <q-table
54
- flat
55
- :rows="rows"
56
- :columns="columns"
57
- row-key="id"
58
- :pagination="{ rowsPerPage: 0 }"
59
- :loading="loading"
60
- hide-pagination
61
- >
2
+ <AdminPage class="sa-audit">
3
+ <AdminHero
4
+ :title="msg.title"
5
+ :subtitle="formatMessage(msg.subtitle, { count: rows.length })"
6
+ >
7
+ <template #actions>
8
+ <AdminRefreshBtn :loading="loading" @refresh="reload" />
9
+ </template>
10
+ </AdminHero>
11
+
12
+ <AdminBody>
13
+ <AdminSection class="sa-audit__card">
14
+ <AdminFilters class="q-mb-lg">
15
+ <q-input
16
+ v-model="filter.actor"
17
+ outlined
18
+ dense
19
+ :label="msg.filters.actor"
20
+ clearable
21
+ debounce="250"
22
+ @update:model-value="reload"
23
+ />
24
+ <q-input
25
+ v-model="filter.action"
26
+ outlined
27
+ dense
28
+ :label="msg.filters.action"
29
+ clearable
30
+ debounce="250"
31
+ @update:model-value="reload"
32
+ />
33
+ <q-input
34
+ v-model="filter.entity"
35
+ outlined
36
+ dense
37
+ :label="msg.filters.entity"
38
+ clearable
39
+ debounce="250"
40
+ @update:model-value="reload"
41
+ />
42
+ <q-input
43
+ v-model="filter.since"
44
+ outlined
45
+ dense
46
+ type="date"
47
+ :label="msg.filters.since"
48
+ clearable
49
+ @update:model-value="reload"
50
+ />
51
+ </AdminFilters>
52
+
53
+ <AdminTable :rows="rows" :columns="columns" :loading="loading" storage-key="audit">
62
54
  <template #body-cell-changes="{ row }">
63
55
  <q-td>
64
56
  <q-btn flat dense icon="code" color="primary" @click="openDetail(row)">
@@ -66,9 +58,9 @@
66
58
  </q-btn>
67
59
  </q-td>
68
60
  </template>
69
- </q-table>
70
- </div>
71
- </div>
61
+ </AdminTable>
62
+ </AdminSection>
63
+ </AdminBody>
72
64
 
73
65
  <q-dialog v-model="detailOpen">
74
66
  <q-card style="min-width: 480px; max-width: 96vw">
@@ -89,11 +81,18 @@
89
81
  </q-card-actions>
90
82
  </q-card>
91
83
  </q-dialog>
92
- </div>
84
+ </AdminPage>
93
85
  </template>
94
86
 
95
87
  <script setup lang="ts">
88
+ import AdminTable from '../components/admin-page/AdminTable.vue';
96
89
  import { computed, onMounted, reactive, ref } from 'vue';
90
+ import AdminRefreshBtn from '../components/admin-page/AdminRefreshBtn.vue';
91
+ import AdminBody from '../components/admin-page/AdminBody.vue';
92
+ import AdminFilters from '../components/admin-page/AdminFilters.vue';
93
+ import AdminHero from '../components/admin-page/AdminHero.vue';
94
+ import AdminSection from '../components/admin-page/AdminSection.vue';
95
+ import AdminPage from '../components/admin-page/AdminPage.vue';
97
96
  import { formatMessage } from '../client/i18n/format.js';
98
97
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
99
98
 
@@ -206,52 +205,9 @@ function formatTs(iso: string | null | undefined): string {
206
205
  </script>
207
206
 
208
207
  <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
- .sa-audit__filter {
236
- display: flex;
237
- gap: 10px;
238
- margin-bottom: 12px;
239
- flex-wrap: wrap;
240
- }
241
- .sa-audit__filter > * {
242
- flex: 1;
243
- min-width: 180px;
244
- }
245
- .sa-audit__card {
246
- background: #fff;
247
- border: 1px solid var(--sa-border, #e2e8f0);
248
- border-radius: 12px;
249
- overflow: hidden;
250
- padding: 8px 0;
251
- }
252
208
  .sa-audit__kv {
253
- background: #f8fafc;
254
- border: 1px solid var(--sa-border, #e2e8f0);
209
+ background: var(--sa-bg-surface-2);
210
+ border: 1px solid var(--sa-border, var(--sa-border));
255
211
  border-radius: 8px;
256
212
  padding: 12px;
257
213
  font-size: 12px;