@saasicat/ui-vue 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/{catalog-BY_t9prv.d.cts → catalog-DJcfm5N2.d.cts} +20 -7
  2. package/dist/{catalog-BY_t9prv.d.ts → catalog-DJcfm5N2.d.ts} +20 -7
  3. package/dist/{chunk-JN6HZGLV.js → chunk-F7NRM6KI.js} +54 -28
  4. package/dist/{chunk-7IFKVGZV.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-OTP3OSSW.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +132 -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 +319 -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 +76 -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-c_Eqsu9u.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-DlJRGIVq.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 +22 -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/AdminFilters.vue +12 -0
  40. package/src/components/admin-page/AdminHero.vue +22 -5
  41. package/src/components/admin-page/AdminKpi.vue +84 -0
  42. package/src/components/admin-page/AdminPaginator.vue +195 -0
  43. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  44. package/src/components/admin-page/AdminSection.vue +31 -16
  45. package/src/components/admin-page/AdminStatistics.vue +40 -0
  46. package/src/components/admin-page/AdminTable.vue +175 -0
  47. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  48. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  49. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  50. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  51. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  52. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  53. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  54. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  55. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  56. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  57. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  58. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  59. package/src/components/plan/PlanGrid.vue +1 -1
  60. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  61. package/src/components/plan-detail/PlanDetail.vue +92 -302
  62. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  63. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  64. package/src/components/plan-list/PlanList.vue +72 -290
  65. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  66. package/src/components/plan-review/PlanReview.vue +191 -275
  67. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  70. package/src/index.ts +1 -0
  71. package/src/pages-standard/AdminLayout.vue +34 -10
  72. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  73. package/src/pages-standard/AuditPage.vue +53 -72
  74. package/src/pages-standard/BundlesPage.vue +128 -144
  75. package/src/pages-standard/DashboardPage.vue +43 -107
  76. package/src/pages-standard/DiscoveryPage.vue +158 -200
  77. package/src/pages-standard/EmailHistoryPage.vue +98 -88
  78. package/src/pages-standard/MarketingCatalogPage.vue +237 -267
  79. package/src/pages-standard/PilotsPage.vue +44 -60
  80. package/src/pages-standard/PlansPage.vue +230 -105
  81. package/src/pages-standard/PlatformEmailPage.vue +37 -40
  82. package/src/pages-standard/PromoCodeDetailPage.vue +16 -30
  83. package/src/pages-standard/PromoCodesPage.vue +86 -112
  84. package/src/pages-standard/SubscriptionsPage.vue +10 -17
  85. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  86. package/src/pages-standard/SuperAdminSetupWizard.vue +20 -17
  87. package/src/pages-standard/TenantDetailPage.vue +45 -41
  88. package/src/pages-standard/TenantsPage.vue +184 -264
  89. package/src/pages-standard/UsersPage.vue +69 -104
  90. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  91. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  92. package/src/pages-standard/bundles-page/BundlesToolbar.vue +6 -10
  93. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  94. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  95. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  96. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  97. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  98. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +1 -1
  99. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +2 -13
  100. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  101. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  102. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  103. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  104. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  105. package/src/pages-standard/plans-page/PlansPageToast.vue +1 -1
  106. package/src/pages-standard/sa-theme.css +481 -71
  107. package/src/pages-standard/tenant-detail/TenantUsers.vue +2 -8
  108. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  109. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  110. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  111. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  112. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  113. package/src/quasar/create-super-admin-app.ts +4 -0
  114. package/src/vue/entitlement-provider.ts +0 -1
  115. package/src/vue/feature-router-guard.ts +0 -1
  116. package/src/vue/super-admin-context.ts +106 -1
  117. package/src/vue/use-api-list.ts +2 -1
  118. package/src/vue/use-pagination.ts +63 -0
  119. package/src/vue/use-plans.ts +1 -2
  120. package/src/vue/use-sign-out.ts +90 -0
  121. package/src/vue/use-tenant-billing.ts +1 -1
  122. package/src/vue/use-tenant-manifest.ts +0 -1
  123. package/src/components/KpiCard.vue +0 -53
  124. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1027
  125. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  126. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -99
  127. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  128. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  129. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  130. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -249
  131. package/src/components/plan-cockpit/types.ts +0 -72
  132. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
@@ -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>
@@ -96,13 +96,14 @@
96
96
  </template>
97
97
 
98
98
  <script setup lang="ts">
99
- import { computed, inject, ref } from 'vue';
99
+ import { computed, inject, ref, getCurrentInstance } from 'vue';
100
100
  import { useRoute } from 'vue-router';
101
101
  import type { AdminManifest, StandardPageKey } from '@saasicat/types';
102
102
  import { buildRoutes, buildSidebar, defaultSectionOrder } from '../client/nav-builder.js';
103
103
  import LocaleSwitcher from '../components/LocaleSwitcher.vue';
104
104
  import { SUPER_ADMIN_BRAND_KEY, SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
105
105
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
106
+ import { useSignOut } from '../vue/use-sign-out.js';
106
107
  import { ADMIN_UI_VERSION } from '../client/version.js';
107
108
 
108
109
  // SuperAdmin layout — universal platform shell for all consumer apps.
@@ -189,6 +190,29 @@ const emit = defineEmits<{
189
190
  (e: 'logout'): void;
190
191
  }>();
191
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
+
192
216
  const route = useRoute();
193
217
  const msg = useSaMessages('shell');
194
218
  const { messages } = useSuperAdminI18n();
@@ -295,7 +319,7 @@ const currentPageTitle = computed(() => {
295
319
  }
296
320
 
297
321
  .sa-admin-header {
298
- 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));
299
323
  color: var(--sa-admin-header-fg, #fff);
300
324
  }
301
325
  .sa-admin-header__sub {
@@ -306,7 +330,7 @@ const currentPageTitle = computed(() => {
306
330
 
307
331
  .sa-admin-badge {
308
332
  background: var(--sa-admin-badge-bg, #f59e0b);
309
- color: var(--sa-admin-badge-fg, #0f172a);
333
+ color: var(--sa-admin-badge-fg, var(--sa-heading));
310
334
  font-weight: 800;
311
335
  letter-spacing: 0.08em;
312
336
  font-size: 10px;
@@ -333,7 +357,7 @@ const currentPageTitle = computed(() => {
333
357
  .sa-admin-drawer :deep(.q-drawer__content),
334
358
  .sa-admin-drawer__stack {
335
359
  background: var(--sa-admin-drawer-bg, #0b1220);
336
- color: var(--sa-admin-drawer-fg, #f1f5f9);
360
+ color: var(--sa-admin-drawer-fg, var(--sa-border-soft));
337
361
  }
338
362
  .sa-admin-drawer__stack {
339
363
  display: flex;
@@ -353,7 +377,7 @@ const currentPageTitle = computed(() => {
353
377
  height: 36px;
354
378
  border-radius: 10px;
355
379
  background: var(--sa-admin-drawer-logo-bg, linear-gradient(135deg, #f59e0b, #d97706));
356
- color: var(--sa-admin-drawer-logo-fg, #0f172a);
380
+ color: var(--sa-admin-drawer-logo-fg, var(--sa-heading));
357
381
  font-weight: 800;
358
382
  display: flex;
359
383
  align-items: center;
@@ -378,7 +402,7 @@ const currentPageTitle = computed(() => {
378
402
  font-size: 10px;
379
403
  text-transform: uppercase;
380
404
  letter-spacing: 0.12em;
381
- color: var(--sa-admin-drawer-section-fg, #64748b);
405
+ color: var(--sa-admin-drawer-section-fg, var(--sa-muted));
382
406
  padding: 14px 18px 6px;
383
407
  font-weight: 600;
384
408
  }
@@ -396,7 +420,7 @@ const currentPageTitle = computed(() => {
396
420
  padding-right: 8px;
397
421
  }
398
422
  .sa-admin-drawer__list :deep(.q-item .q-icon) {
399
- color: #e2e8f0;
423
+ color: var(--sa-border);
400
424
  }
401
425
  .sa-admin-drawer__list :deep(.q-item:hover) {
402
426
  background: var(--sa-admin-drawer-hover-bg, rgba(245, 158, 11, 0.15));
@@ -425,7 +449,7 @@ const currentPageTitle = computed(() => {
425
449
  align-items: center;
426
450
  gap: 6px;
427
451
  margin-top: 6px;
428
- color: #e2e8f0;
452
+ color: var(--sa-border);
429
453
  text-decoration: none;
430
454
  }
431
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 {
@@ -5,59 +5,52 @@
5
5
  :subtitle="formatMessage(msg.subtitle, { count: rows.length })"
6
6
  >
7
7
  <template #actions>
8
- <q-btn
9
- unelevated
10
- color="primary"
11
- icon="refresh"
12
- :label="msg.filterButton"
13
- @click="reload"
14
- />
8
+ <AdminRefreshBtn :loading="loading" @refresh="reload" />
15
9
  </template>
16
10
  </AdminHero>
17
11
 
18
- <div class="sa-audit__body">
19
- <div class="sa-audit__filter">
20
- <q-input
21
- v-model="filter.actor"
22
- outlined
23
- dense
24
- :label="msg.filters.actor"
25
- clearable
26
- />
27
- <q-input
28
- v-model="filter.action"
29
- outlined
30
- dense
31
- :label="msg.filters.action"
32
- clearable
33
- />
34
- <q-input
35
- v-model="filter.entity"
36
- outlined
37
- dense
38
- :label="msg.filters.entity"
39
- clearable
40
- />
41
- <q-input
42
- v-model="filter.since"
43
- outlined
44
- dense
45
- type="date"
46
- :label="msg.filters.since"
47
- clearable
48
- />
49
- </div>
50
-
51
- <div class="sa-audit__card">
52
- <q-table
53
- flat
54
- :rows="rows"
55
- :columns="columns"
56
- row-key="id"
57
- :pagination="{ rowsPerPage: 0 }"
58
- :loading="loading"
59
- hide-pagination
60
- >
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">
61
54
  <template #body-cell-changes="{ row }">
62
55
  <q-td>
63
56
  <q-btn flat dense icon="code" color="primary" @click="openDetail(row)">
@@ -65,9 +58,9 @@
65
58
  </q-btn>
66
59
  </q-td>
67
60
  </template>
68
- </q-table>
69
- </div>
70
- </div>
61
+ </AdminTable>
62
+ </AdminSection>
63
+ </AdminBody>
71
64
 
72
65
  <q-dialog v-model="detailOpen">
73
66
  <q-card style="min-width: 480px; max-width: 96vw">
@@ -92,8 +85,13 @@
92
85
  </template>
93
86
 
94
87
  <script setup lang="ts">
88
+ import AdminTable from '../components/admin-page/AdminTable.vue';
95
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';
96
93
  import AdminHero from '../components/admin-page/AdminHero.vue';
94
+ import AdminSection from '../components/admin-page/AdminSection.vue';
97
95
  import AdminPage from '../components/admin-page/AdminPage.vue';
98
96
  import { formatMessage } from '../client/i18n/format.js';
99
97
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
@@ -207,26 +205,9 @@ function formatTs(iso: string | null | undefined): string {
207
205
  </script>
208
206
 
209
207
  <style scoped>
210
- .sa-audit__filter {
211
- display: flex;
212
- gap: 10px;
213
- margin-bottom: 12px;
214
- flex-wrap: wrap;
215
- }
216
- .sa-audit__filter > * {
217
- flex: 1;
218
- min-width: 180px;
219
- }
220
- .sa-audit__card {
221
- background: #fff;
222
- border: 1px solid var(--sa-border, #e2e8f0);
223
- border-radius: 12px;
224
- overflow: hidden;
225
- padding: 8px 0;
226
- }
227
208
  .sa-audit__kv {
228
- background: #f8fafc;
229
- border: 1px solid var(--sa-border, #e2e8f0);
209
+ background: var(--sa-bg-surface-2);
210
+ border: 1px solid var(--sa-border, var(--sa-border));
230
211
  border-radius: 8px;
231
212
  padding: 12px;
232
213
  font-size: 12px;