@saasicat/ui-vue 0.5.0 → 0.7.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 (186) hide show
  1. package/README.md +17 -0
  2. package/dist/{chunk-EH7TOD4H.js → chunk-A3W2N26K.js} +73 -77
  3. package/dist/{chunk-3YP2ZAOD.js → chunk-QZOSDCNP.js} +36 -1
  4. package/dist/chunk-X5SQ5HMB.js +3231 -0
  5. package/dist/client/index.cjs +3279 -76
  6. package/dist/client/index.d.cts +31 -5
  7. package/dist/client/index.d.ts +31 -5
  8. package/dist/client/index.js +23 -5
  9. package/dist/index.cjs +3709 -465
  10. package/dist/index.d.cts +37 -54
  11. package/dist/index.d.ts +37 -54
  12. package/dist/index.js +347 -311
  13. package/dist/messages-7b0P8W75.d.cts +1652 -0
  14. package/dist/messages-7b0P8W75.d.ts +1652 -0
  15. package/dist/quasar/index.cjs +3185 -4
  16. package/dist/quasar/index.d.cts +11 -2
  17. package/dist/quasar/index.d.ts +11 -2
  18. package/dist/quasar/index.js +8 -3
  19. package/dist/testing-e2e/admin-pages-suite.cjs +14 -11
  20. package/dist/testing-e2e/admin-pages-suite.js +14 -11
  21. package/dist/{ui-notify-COUzXEQ4.d.ts → use-super-admin-i18n-B3v3-SWZ.d.ts} +35 -3
  22. package/dist/{ui-notify-D_eAAwRh.d.cts → use-super-admin-i18n-DMktRVEt.d.cts} +35 -3
  23. package/package.json +2 -2
  24. package/src/client/action-registry.ts +4 -4
  25. package/src/client/batch-column-fetcher.ts +4 -4
  26. package/src/client/boot-loader.ts +3 -3
  27. package/src/client/i18n/currency.ts +42 -0
  28. package/src/client/i18n/define.ts +59 -0
  29. package/src/client/i18n/format.ts +12 -0
  30. package/src/client/i18n/index.ts +14 -0
  31. package/src/client/i18n/locale.ts +15 -0
  32. package/src/client/i18n/messages/audit.ts +42 -0
  33. package/src/client/i18n/messages/bundles.ts +459 -0
  34. package/src/client/i18n/messages/common.ts +147 -0
  35. package/src/client/i18n/messages/dashboard.ts +16 -0
  36. package/src/client/i18n/messages/discovery.ts +229 -0
  37. package/src/client/i18n/messages/email.ts +125 -0
  38. package/src/client/i18n/messages/marketing.ts +270 -0
  39. package/src/client/i18n/messages/nav.ts +72 -0
  40. package/src/client/i18n/messages/pilots.ts +192 -0
  41. package/src/client/i18n/messages/plan-detail.ts +266 -0
  42. package/src/client/i18n/messages/plan-editor.ts +177 -0
  43. package/src/client/i18n/messages/plan-versions.ts +251 -0
  44. package/src/client/i18n/messages/plans.ts +503 -0
  45. package/src/client/i18n/messages/promos.ts +202 -0
  46. package/src/client/i18n/messages/shell.ts +145 -0
  47. package/src/client/i18n/messages/tenants.ts +104 -0
  48. package/src/client/i18n/messages/users.ts +78 -0
  49. package/src/client/i18n/messages.ts +84 -0
  50. package/src/client/index.ts +2 -0
  51. package/src/client/manifest-loader.ts +4 -4
  52. package/src/client/nav-builder.ts +44 -51
  53. package/src/client/plan-versions-catalog.ts +42 -30
  54. package/src/components/BundleVersionPublishDialog.vue +35 -24
  55. package/src/components/MarketingPromotionsTab.vue +112 -59
  56. package/src/components/MfaPromptDialog.vue +11 -11
  57. package/src/components/TenantActionConfirmDialog.vue +19 -17
  58. package/src/components/VersionDiffPreview.vue +72 -43
  59. package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
  60. package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
  61. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
  62. package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
  63. package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
  64. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
  65. package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
  66. package/src/components/bundle-editor/bundle-version-status.ts +29 -29
  67. package/src/components/dialogs/PilotCreateDialog.vue +58 -63
  68. package/src/components/dialogs/PilotEditDialog.vue +40 -34
  69. package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
  70. package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
  71. package/src/components/dialogs/types.ts +3 -13
  72. package/src/components/plan/PlanCycleToggle.vue +20 -9
  73. package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
  74. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
  75. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
  76. package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
  77. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
  78. package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
  79. package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
  80. package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
  81. package/src/components/plan-detail/PlanAuditLog.vue +11 -6
  82. package/src/components/plan-detail/PlanDetail.vue +21 -23
  83. package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
  84. package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
  85. package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
  86. package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
  87. package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
  88. package/src/components/plan-list/PlanList.vue +99 -57
  89. package/src/components/plan-matrix/PlanMatrix.vue +85 -38
  90. package/src/components/plan-review/PlanReview.vue +105 -80
  91. package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
  92. package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
  93. package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
  94. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
  95. package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
  96. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
  97. package/src/index.ts +1 -1
  98. package/src/pages-standard/AdminLayout.vue +42 -17
  99. package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
  100. package/src/pages-standard/AuditPage.vue +58 -19
  101. package/src/pages-standard/BundlesPage.vue +29 -18
  102. package/src/pages-standard/DashboardPage.vue +29 -15
  103. package/src/pages-standard/DiscoveryPage.vue +40 -28
  104. package/src/pages-standard/EmailHistoryPage.vue +92 -59
  105. package/src/pages-standard/MarketingCatalogPage.vue +35 -16
  106. package/src/pages-standard/PilotsPage.vue +57 -41
  107. package/src/pages-standard/PlanVersionsPage.vue +7 -3
  108. package/src/pages-standard/PlansPage.vue +89 -57
  109. package/src/pages-standard/PlatformEmailPage.vue +115 -50
  110. package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
  111. package/src/pages-standard/PromoCodesPage.vue +46 -35
  112. package/src/pages-standard/SubscriptionsPage.vue +14 -9
  113. package/src/pages-standard/SuperAdminLoginPage.vue +16 -13
  114. package/src/pages-standard/SuperAdminSetupWizard.vue +51 -47
  115. package/src/pages-standard/TenantDetailPage.vue +50 -36
  116. package/src/pages-standard/TenantsPage.vue +54 -41
  117. package/src/pages-standard/UsersPage.vue +66 -42
  118. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
  119. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
  120. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
  121. package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
  122. package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
  123. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
  124. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
  125. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
  126. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
  127. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
  128. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
  129. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
  130. package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
  131. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
  132. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
  133. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
  134. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
  135. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
  136. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +41 -25
  137. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +45 -26
  138. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +7 -5
  139. package/src/pages-standard/plan-versions/PlanVersionsList.vue +39 -16
  140. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +24 -12
  141. package/src/pages-standard/plan-versions/format.ts +33 -17
  142. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
  143. package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
  144. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
  145. package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
  146. package/src/pages-standard/tenants/format.ts +3 -2
  147. package/src/pages-tenant/MySubscriptionBundlesPage.vue +72 -37
  148. package/src/pages-tenant/PlanChangeWizard.vue +8 -4
  149. package/src/pages-tenant/TenantPlanSection.vue +17 -13
  150. package/src/pages-tenant/default-i18n.ts +217 -1
  151. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +8 -6
  152. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +9 -10
  153. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
  154. package/src/quasar/create-super-admin-app.ts +19 -0
  155. package/src/testing-e2e/admin-pages-suite.ts +14 -11
  156. package/src/vue/project-page-host.ts +5 -3
  157. package/src/vue/use-audit-entries.ts +3 -3
  158. package/src/vue/use-bulk-publish.ts +6 -4
  159. package/src/vue/use-bundle-versions-map.ts +6 -4
  160. package/src/vue/use-bundles.ts +13 -13
  161. package/src/vue/use-catalog-entries.ts +13 -10
  162. package/src/vue/use-discovery.ts +8 -5
  163. package/src/vue/use-entitlement.ts +3 -3
  164. package/src/vue/use-live-plan-versions.ts +4 -4
  165. package/src/vue/use-marketing-projections.ts +9 -6
  166. package/src/vue/use-nav.ts +8 -2
  167. package/src/vue/use-plan-editor.ts +2 -2
  168. package/src/vue/use-plan-versions.ts +3 -3
  169. package/src/vue/use-plans.ts +12 -12
  170. package/src/vue/use-platform-tenant-actions.ts +9 -6
  171. package/src/vue/use-promotions.ts +9 -10
  172. package/src/vue/use-subscription-draft.ts +2 -3
  173. package/src/vue/use-super-admin-context.ts +3 -3
  174. package/src/vue/use-super-admin-i18n.ts +82 -0
  175. package/src/vue/use-tenant-action-flow.ts +11 -6
  176. package/src/vue/use-tenant-manifest.ts +1 -3
  177. package/src/vue/use-tenant-subscription-bundles.ts +4 -4
  178. package/src/vue/use-tenants.ts +3 -3
  179. package/dist/action-registry-CQ2wL3R-.d.cts +0 -85
  180. package/dist/action-registry-CQ2wL3R-.d.ts +0 -85
  181. package/dist/chunk-LVGUSD3T.js +0 -26
  182. package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
  183. package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
  184. package/src/pages-standard/BusinessTypesPage.vue +0 -662
  185. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
  186. package/src/vue/use-business-types.ts +0 -269
@@ -2,7 +2,7 @@
2
2
  <q-layout view="lHh Lpr lFf">
3
3
  <div v-if="isProduction" class="sa-admin-banner sa-admin-banner--prod">
4
4
  <q-icon name="warning" size="14px" />
5
- <strong>PRODUCTION</strong> — Aktionen wirken sich sofort auf alle Mandanten aus.
5
+ <strong>PRODUCTION</strong> — {{ msg.header.productionWarning }}
6
6
  </div>
7
7
 
8
8
  <q-header elevated class="sa-admin-header">
@@ -10,11 +10,11 @@
10
10
  <q-btn flat dense round icon="menu" @click="leftDrawerOpen = !leftDrawerOpen" />
11
11
  <q-toolbar-title class="text-weight-bold">
12
12
  {{ currentPageTitle }}
13
- <div class="sa-admin-header__sub">SuperAdmin · Plattform-Verwaltung</div>
13
+ <div class="sa-admin-header__sub">{{ msg.header.subtitle }}</div>
14
14
  </q-toolbar-title>
15
15
  <q-space />
16
16
  <slot name="header-actions" />
17
- <q-badge class="sa-admin-badge q-mr-sm">SUPER ADMIN</q-badge>
17
+ <q-badge class="sa-admin-badge q-mr-sm">{{ msg.header.roleBadge }}</q-badge>
18
18
  <div class="sa-admin-user">
19
19
  <q-avatar size="32px" class="sa-admin-user__avatar">
20
20
  {{ initials }}
@@ -24,7 +24,7 @@
24
24
  <div class="text-caption sa-admin-user__email">{{ userEmail }}</div>
25
25
  </div>
26
26
  <q-btn flat round dense icon="logout" size="sm" @click="emit('logout')">
27
- <q-tooltip>Abmelden</q-tooltip>
27
+ <q-tooltip>{{ msg.header.logout }}</q-tooltip>
28
28
  </q-btn>
29
29
  </div>
30
30
  </q-toolbar>
@@ -39,10 +39,12 @@
39
39
  >
40
40
  <div class="sa-admin-drawer__stack">
41
41
  <div class="sa-admin-drawer__brand">
42
- <div class="sa-admin-drawer__logo">{{ brandLogoText }}</div>
42
+ <div class="sa-admin-drawer__logo">{{ brand.logoText }}</div>
43
43
  <div>
44
- <div class="sa-admin-drawer__brand-name">{{ brandName }}</div>
45
- <div class="sa-admin-drawer__brand-tag">{{ brandTag }} v{{ adminUiVersion }}</div>
44
+ <div class="sa-admin-drawer__brand-name">{{ brand.name }}</div>
45
+ <div class="sa-admin-drawer__brand-tag">
46
+ {{ brand.tag }} v{{ adminUiVersion }}
47
+ </div>
46
48
  </div>
47
49
  </div>
48
50
 
@@ -75,7 +77,7 @@
75
77
  target="_blank"
76
78
  rel="noopener"
77
79
  >
78
- <q-icon name="menu_book" size="14px" /> Doku öffnen
80
+ <q-icon name="menu_book" size="14px" /> {{ msg.drawer.docs }}
79
81
  </a>
80
82
  </slot>
81
83
  </div>
@@ -91,10 +93,12 @@
91
93
  </template>
92
94
 
93
95
  <script setup lang="ts">
94
- import { computed, ref } from 'vue';
96
+ import { computed, inject, ref } from 'vue';
95
97
  import { useRoute } from 'vue-router';
96
98
  import type { AdminManifest, StandardPageKey } from '@saasicat/types';
97
- import { buildRoutes, buildSidebar, DEFAULT_SECTION_ORDER } from '../client/nav-builder.js';
99
+ import { buildRoutes, buildSidebar, defaultSectionOrder } from '../client/nav-builder.js';
100
+ import { SUPER_ADMIN_BRAND_KEY, SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
101
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
98
102
  import { ADMIN_UI_VERSION } from '../client/version.js';
99
103
 
100
104
  // SuperAdmin layout — universal platform shell for all consumer apps.
@@ -131,8 +135,13 @@ export interface SidebarItem {
131
135
 
132
136
  const props = withDefaults(
133
137
  defineProps<{
134
- brandLogoText: string;
135
- brandName: string;
138
+ /**
139
+ * Branding. Optional — when the layout is mounted as a route component
140
+ * inside `createSuperAdminApp({ brand })`, these default to the
141
+ * provided brand and only need passing to override it.
142
+ */
143
+ brandLogoText?: string;
144
+ brandName?: string;
136
145
  brandTag?: string;
137
146
  manifest?: AdminManifest | null;
138
147
  staticNavFallback?: readonly SidebarItem[];
@@ -164,13 +173,11 @@ const props = withDefaults(
164
173
  adminPathPrefix?: string;
165
174
  }>(),
166
175
  {
167
- brandTag: 'SuperAdmin',
168
176
  staticNavFallback: () => [],
169
177
  localItems: () => [],
170
178
  localItemsSection: null,
171
179
  adminPathPrefix: '/admin',
172
180
  isProduction: false,
173
- sectionOrder: () => DEFAULT_SECTION_ORDER,
174
181
  },
175
182
  );
176
183
 
@@ -179,6 +186,23 @@ const emit = defineEmits<{
179
186
  }>();
180
187
 
181
188
  const route = useRoute();
189
+ const msg = useSaMessages('shell');
190
+ const { locale } = useSuperAdminI18n();
191
+ // Provided by createSuperAdminApp(); null when the layout is mounted
192
+ // stand-alone. Grouped into one object so the names cannot shadow the
193
+ // same-named props inside the template.
194
+ const injectedBrand = inject(SUPER_ADMIN_BRAND_KEY, null);
195
+ // Same idea for the manifest: createSuperAdminApp({ manifestGuard: {
196
+ // getManifest } }) provides an accessor, so pages mounted straight as route
197
+ // components do not have to be wrapped just to thread the prop through.
198
+ const injectedManifest = inject(SUPER_ADMIN_MANIFEST_KEY, null);
199
+ const activeManifest = computed(() => props.manifest ?? injectedManifest?.() ?? null);
200
+ const brand = computed(() => ({
201
+ logoText: props.brandLogoText ?? injectedBrand?.logoText ?? '',
202
+ name: props.brandName ?? injectedBrand?.name ?? '',
203
+ tag: props.brandTag ?? injectedBrand?.tag ?? 'SuperAdmin',
204
+ }));
205
+
182
206
  const leftDrawerOpen = ref(false);
183
207
 
184
208
  const adminUiVersion = ADMIN_UI_VERSION;
@@ -195,18 +219,19 @@ interface NavSection {
195
219
  }
196
220
 
197
221
  const navSections = computed<NavSection[]>(() => {
198
- const m = props.manifest;
222
+ const m = activeManifest.value;
199
223
  if (!m) {
200
224
  // Pre-manifest: static fallback + local items as one unnamed
201
225
  // section, so that the UI has no flicker before manifest load.
202
226
  return [{ title: null, items: [...props.staticNavFallback, ...props.localItems] }];
203
227
  }
204
228
  const routes = buildRoutes(m, {
229
+ locale: locale.value,
205
230
  standardPageRoutes: props.standardPageRoutes,
206
231
  standardPageNavSection: props.standardPageNavSection,
207
232
  availableExtensions: props.availableExtensions,
208
233
  });
209
- const sections = buildSidebar(routes, props.sectionOrder);
234
+ const sections = buildSidebar(routes, props.sectionOrder ?? defaultSectionOrder(locale.value));
210
235
  const result: NavSection[] = sections.map((s) => ({
211
236
  title: s.section,
212
237
  items: s.items.map((item) => ({
@@ -241,7 +266,7 @@ const currentPageTitle = computed(() => {
241
266
  }
242
267
  // Default: sidebar item label that matches the active route.
243
268
  const item = resolvedNav.value.find((i) => i.to === route.path);
244
- return item?.label ?? `${props.brandName} SuperAdmin`;
269
+ return item?.label ?? `${brand.value.name} SuperAdmin`;
245
270
  });
246
271
  </script>
247
272
 
@@ -3,21 +3,16 @@
3
3
  <q-card class="sa-manifest-error__card">
4
4
  <q-card-section class="sa-manifest-error__head">
5
5
  <q-icon name="cloud_off" size="32px" color="negative" />
6
- <h1 class="sa-manifest-error__title">Admin-Manifest nicht erreichbar</h1>
6
+ <h1 class="sa-manifest-error__title">{{ msg.manifestError.title }}</h1>
7
7
  </q-card-section>
8
8
  <q-card-section>
9
- <p class="sa-manifest-error__lead">
10
- Das Plattform-Manifest konnte nicht geladen werden. Ohne Manifest sind Sidebar,
11
- Capabilities und Action-Buttons nicht zuverlässig — die UI würde sonst falsche
12
- oder unerreichbare Buttons rendern. Der Router hat dich deshalb hierher
13
- geschickt (fail-closed).
14
- </p>
9
+ <p class="sa-manifest-error__lead">{{ msg.manifestError.lead }}</p>
15
10
  <p v-if="errorMessage" class="sa-manifest-error__detail">
16
- <strong>Detail:</strong> {{ errorMessage }}
11
+ <strong>{{ msg.manifestError.detailLabel }}</strong> {{ errorMessage }}
17
12
  </p>
18
13
  <div class="sa-manifest-error__actions">
19
- <q-btn unelevated color="primary" label="Erneut laden" @click="onRetry" />
20
- <q-btn flat label="Logout" @click="onLogout" />
14
+ <q-btn unelevated color="primary" :label="common.reload" @click="onRetry" />
15
+ <q-btn flat :label="msg.header.logout" @click="onLogout" />
21
16
  </div>
22
17
  </q-card-section>
23
18
  </q-card>
@@ -25,11 +20,16 @@
25
20
  </template>
26
21
 
27
22
  <script setup lang="ts">
23
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
24
+
28
25
  // Platform standard error page for `manifestGuard.errorRoute`.
29
26
  // Consumers pass `onRetry`/`onLogout` in as callbacks and decide
30
27
  // themselves what happens (e.g. manifestStore.clearCache() + ensureLoaded,
31
28
  // authStore.logout, router.replace).
32
29
 
30
+ const msg = useSaMessages('shell');
31
+ const common = useSaMessages('common');
32
+
33
33
  defineProps<{
34
34
  errorMessage?: string | null;
35
35
  onRetry: () => Promise<void> | void;
@@ -2,18 +2,51 @@
2
2
  <div class="sa-audit">
3
3
  <header class="sa-audit__head">
4
4
  <div>
5
- <h1 class="sa-audit__title">Audit-Trail</h1>
6
- <p class="sa-audit__sub">{{ rows.length }} Einträge · plattformweit.</p>
5
+ <h1 class="sa-audit__title">{{ msg.title }}</h1>
6
+ <p class="sa-audit__sub">
7
+ {{ formatMessage(msg.subtitle, { count: rows.length }) }}
8
+ </p>
7
9
  </div>
8
- <q-btn unelevated color="primary" icon="refresh" label="Filtern" @click="reload" />
10
+ <q-btn
11
+ unelevated
12
+ color="primary"
13
+ icon="refresh"
14
+ :label="msg.filterButton"
15
+ @click="reload"
16
+ />
9
17
  </header>
10
18
 
11
19
  <div class="sa-audit__body">
12
20
  <div class="sa-audit__filter">
13
- <q-input v-model="filter.actor" outlined dense label="Actor (E-Mail)" clearable />
14
- <q-input v-model="filter.action" outlined dense label="Action" clearable />
15
- <q-input v-model="filter.entity" outlined dense label="Entity" clearable />
16
- <q-input v-model="filter.since" outlined dense type="date" label="Seit" clearable />
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
+ />
17
50
  </div>
18
51
 
19
52
  <div class="sa-audit__card">
@@ -29,7 +62,7 @@
29
62
  <template #body-cell-changes="{ row }">
30
63
  <q-td>
31
64
  <q-btn flat dense icon="code" color="primary" @click="openDetail(row)">
32
- <q-tooltip>Details</q-tooltip>
65
+ <q-tooltip>{{ common.details }}</q-tooltip>
33
66
  </q-btn>
34
67
  </q-td>
35
68
  </template>
@@ -42,7 +75,7 @@
42
75
  <q-card-section>
43
76
  <div class="text-h6">{{ detail?.action }} · {{ detail?.entity }}</div>
44
77
  <div class="text-caption text-grey-7">
45
- {{ detail ? formatTs(detail.createdAt) : '' }} · Actor:
78
+ {{ detail ? formatTs(detail.createdAt) : '' }} · {{ msg.detailActorPrefix }}
46
79
  {{ detail ? actorEmail(detail) : '—' }}
47
80
  </div>
48
81
  </q-card-section>
@@ -52,7 +85,7 @@
52
85
  }}</pre>
53
86
  </q-card-section>
54
87
  <q-card-actions align="right">
55
- <q-btn v-close-popup flat label="Schließen" />
88
+ <q-btn v-close-popup flat :label="common.close" />
56
89
  </q-card-actions>
57
90
  </q-card>
58
91
  </q-dialog>
@@ -60,7 +93,9 @@
60
93
  </template>
61
94
 
62
95
  <script setup lang="ts">
63
- import { onMounted, reactive, ref } from 'vue';
96
+ import { computed, onMounted, reactive, ref } from 'vue';
97
+ import { formatMessage } from '../client/i18n/format.js';
98
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
64
99
 
65
100
  // Platform standard page: audit trail. Data-agnostic — the app passes
66
101
  // `loadAudit({ actor, action, entity, since, limit })` through.
@@ -89,36 +124,40 @@ const props = defineProps<{
89
124
  pageSize?: number;
90
125
  }>();
91
126
 
127
+ const msg = useSaMessages('audit');
128
+ const common = useSaMessages('common');
129
+ const { intlLocale } = useSuperAdminI18n();
130
+
92
131
  const rows = ref<AuditRow[]>([]);
93
132
  const loading = ref(false);
94
133
  const filter = reactive({ actor: '', action: '', entity: '', since: '' });
95
134
  const detailOpen = ref(false);
96
135
  const detail = ref<AuditRow | null>(null);
97
136
 
98
- const columns = [
137
+ const columns = computed(() => [
99
138
  {
100
139
  name: 'createdAt',
101
- label: 'Zeit',
140
+ label: msg.value.columns.time,
102
141
  field: (r: AuditRow) => formatTs(r.createdAt),
103
142
  align: 'left' as const,
104
143
  sortable: true,
105
144
  },
106
145
  {
107
146
  name: 'actor',
108
- label: 'Actor',
147
+ label: msg.value.columns.actor,
109
148
  field: actorEmail,
110
149
  align: 'left' as const,
111
150
  },
112
- { name: 'action', label: 'Action', field: 'action', align: 'left' as const },
113
- { name: 'entity', label: 'Entity', field: 'entity', align: 'left' as const },
151
+ { name: 'action', label: msg.value.columns.action, field: 'action', align: 'left' as const },
152
+ { name: 'entity', label: msg.value.columns.entity, field: 'entity', align: 'left' as const },
114
153
  {
115
154
  name: 'entityId',
116
- label: 'ID',
155
+ label: msg.value.columns.id,
117
156
  field: (r: AuditRow) => r.entityId.slice(0, 8),
118
157
  align: 'left' as const,
119
158
  },
120
159
  { name: 'changes', label: '', field: 'id', align: 'right' as const },
121
- ];
160
+ ]);
122
161
 
123
162
  async function reload() {
124
163
  loading.value = true;
@@ -153,7 +192,7 @@ function actorEmail(r: AuditRow): string {
153
192
  function formatTs(iso: string | null | undefined): string {
154
193
  if (!iso) return '—';
155
194
  try {
156
- return new Date(iso).toLocaleString('de-DE', {
195
+ return new Date(iso).toLocaleString(intlLocale.value, {
157
196
  day: '2-digit',
158
197
  month: 'short',
159
198
  year: 'numeric',
@@ -11,7 +11,7 @@
11
11
 
12
12
  <q-banner v-if="error" class="sa-bundles__error" rounded>
13
13
  <template #avatar><q-icon name="warning" color="negative" /></template>
14
- Fehler beim Laden: {{ error.message }}
14
+ {{ loadErrorText }}
15
15
  </q-banner>
16
16
 
17
17
  <BundlesKpis
@@ -54,8 +54,8 @@
54
54
  rounded
55
55
  >
56
56
  <template #avatar><q-icon name="info" color="info" /></template>
57
- Noch keine Bundles angelegt. Über <strong>Neues Bundle</strong> einen Bundle-Stamm
58
- anlegen, dann Features &amp; Quotas in einer Draft-Version kuratieren.
57
+ {{ msg.page.emptyBefore }} <strong>{{ msg.header.newBundle }}</strong>
58
+ {{ msg.page.emptyAfter }}
59
59
  </q-banner>
60
60
 
61
61
  <BundleAccordionList
@@ -104,7 +104,7 @@
104
104
  rounded
105
105
  >
106
106
  <template #avatar><q-icon name="warning" color="warning" /></template>
107
- <strong>{{ lastWarnings.length }} Strict-Mode-Warnung(en) bei letzter Operation</strong>
107
+ <strong>{{ strictWarningsText }}</strong>
108
108
  <ul class="sa-bundles__warnings-list">
109
109
  <li v-for="(w, i) in lastWarnings" :key="i">
110
110
  <code>{{ w.code }}</code>
@@ -115,7 +115,7 @@
115
115
  </li>
116
116
  </ul>
117
117
  <template #action>
118
- <q-btn flat dense label="Schließen" @click="lastWarnings = []" />
118
+ <q-btn flat dense :label="common.close" @click="lastWarnings = []" />
119
119
  </template>
120
120
  </q-banner>
121
121
 
@@ -169,6 +169,8 @@ import {
169
169
  bundleVersionsSorted,
170
170
  type BundleAggregateStatus,
171
171
  } from '../components/bundle-editor/bundle-version-status';
172
+ import { formatMessage } from '../client/i18n/format.js';
173
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
172
174
  import BundleAccordionList from './bundles-page/BundleAccordionList.vue';
173
175
  import BundleDetailPanel from './bundles-page/BundleDetailPanel.vue';
174
176
  import BundlesFilterBar from './bundles-page/BundlesFilterBar.vue';
@@ -263,6 +265,9 @@ const props = withDefaults(
263
265
  },
264
266
  );
265
267
 
268
+ const msg = useSaMessages('bundles');
269
+ const common = useSaMessages('common');
270
+
266
271
  const locales = computed(() => props.activeLocales ?? [DEFAULT_LOCALE]);
267
272
  const translatableLocales = computed(() => locales.value.filter((l) => l !== DEFAULT_LOCALE));
268
273
 
@@ -350,14 +355,14 @@ const createOpen = ref(false);
350
355
  /** For the bundle-key conflict check in the wizard. */
351
356
  const existingBundleKeys = computed(() => props.bundles.map((b) => b.bundleKey));
352
357
 
353
- const statusFilterOptions: BundlesStatusFilterOption[] = [
354
- { label: 'Alle Status', value: 'all' as const },
355
- { label: 'Nur Live', value: 'live' as const },
356
- { label: 'Mit geplanter Version', value: 'scheduled' as const },
357
- { label: 'Drafts', value: 'draft' as const },
358
- { label: 'Abgelöste', value: 'superseded' as const },
359
- { label: 'Retired', value: 'retired' as const },
360
- ];
358
+ const statusFilterOptions = computed<BundlesStatusFilterOption[]>(() => [
359
+ { label: msg.value.filter.all, value: 'all' },
360
+ { label: msg.value.filter.live, value: 'live' },
361
+ { label: msg.value.filter.scheduled, value: 'scheduled' },
362
+ { label: msg.value.filter.draft, value: 'draft' },
363
+ { label: msg.value.filter.superseded, value: 'superseded' },
364
+ { label: msg.value.filter.retired, value: 'retired' },
365
+ ]);
361
366
 
362
367
  function openCreatePanel(): void {
363
368
  createOpen.value = true;
@@ -446,8 +451,7 @@ async function submitEdit(): Promise<void> {
446
451
 
447
452
  async function confirmDelete(bundle: BundleRow): Promise<void> {
448
453
  const ok = window.confirm(
449
- `Bundle '${bundle.bundleKey}' wirklich soft-deleten? Bestand bleibt durch ` +
450
- `published BundleVersions geschützt.`,
454
+ formatMessage(msg.value.page.confirmSoftDelete, { bundleKey: bundle.bundleKey }),
451
455
  );
452
456
  if (!ok) return;
453
457
  await props.softDelete(bundle.id);
@@ -466,6 +470,13 @@ watch(
466
470
  // ─── Strict mode warnings ───
467
471
  const lastWarnings = ref<StrictModeWarning[]>([]);
468
472
 
473
+ const loadErrorText = computed(() =>
474
+ formatMessage(msg.value.page.loadError, { message: props.error?.message ?? '' }),
475
+ );
476
+ const strictWarningsText = computed(() =>
477
+ formatMessage(msg.value.page.strictWarnings, { count: lastWarnings.value.length }),
478
+ );
479
+
469
480
  // ─── Inline editor (selected version per bundle) ───
470
481
  const selectedVersionIdByBundle = ref<Record<string, string | null>>({});
471
482
  const inlineEditorSaving = ref(false);
@@ -565,10 +576,10 @@ async function onInlineSave(
565
576
  async function onDiscardVersion(bundleId: string, versionId: string): Promise<void> {
566
577
  if (!props.discardDraft) {
567
578
  inlineEditorError.value =
568
- 'Discard ist im Wrapper nicht verdrahtetbitte `discardDraft`-Prop ergänzen.';
579
+ 'Discard is not wired up in the wrapper add the `discardDraft` prop.';
569
580
  return;
570
581
  }
571
- const ok = window.confirm('Diese Draft-Version verwerfen? Der Inhalt geht verloren.');
582
+ const ok = window.confirm(msg.value.page.confirmDiscardVersion);
572
583
  if (!ok) return;
573
584
  inlineEditorSaving.value = true;
574
585
  inlineEditorError.value = null;
@@ -614,7 +625,7 @@ async function onPublishSubmit(opts: {
614
625
  validUntil?: string | null;
615
626
  }): Promise<BundleVersionMutationResult> {
616
627
  if (!publishDraft.value) {
617
- throw new Error('BundlesPage: publish submit ohne Draft-Kontext');
628
+ throw new Error('BundlesPage: publish submit without draft context');
618
629
  }
619
630
  return props.publish(publishDraft.value.id, opts);
620
631
  }
@@ -2,7 +2,7 @@
2
2
  <div class="sa-dashboard">
3
3
  <header class="sa-page-head sa-dashboard__head">
4
4
  <div>
5
- <h1 class="sa-page-head__title">Dashboard</h1>
5
+ <h1 class="sa-page-head__title">{{ msg.title }}</h1>
6
6
  <p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
7
7
  </div>
8
8
  <div class="sa-page-head__actions">
@@ -11,22 +11,22 @@
11
11
  dense
12
12
  icon="refresh"
13
13
  :loading="loading"
14
- aria-label="Neu laden"
14
+ :aria-label="common.reload"
15
15
  @click="reload"
16
16
  />
17
17
  </div>
18
18
  </header>
19
19
 
20
20
  <q-banner v-if="error" class="bg-red-1 text-red-9 q-mb-md" rounded>
21
- <strong>Fehler:</strong> {{ error.message }}
21
+ <strong>{{ common.error }}:</strong> {{ error.message }}
22
22
  </q-banner>
23
23
 
24
24
  <div v-if="loading && !cards.length" class="sa-dashboard__loading">
25
- <q-spinner size="32px" /> Daten werden geladen…
25
+ <q-spinner size="32px" /> {{ common.loadingData }}
26
26
  </div>
27
27
 
28
28
  <div v-else-if="!cards.length" class="sa-dashboard__empty">
29
- Keine KPI-Cards im Manifest deklariert.
29
+ {{ msg.emptyKpiCards }}
30
30
  </div>
31
31
 
32
32
  <div v-else class="sa-dashboard__strip">
@@ -83,7 +83,7 @@
83
83
  class="sa-dashboard__card sa-dashboard__shortcuts"
84
84
  >
85
85
  <header class="sa-dashboard__row-head">
86
- <h2>Shortcuts</h2>
86
+ <h2>{{ msg.shortcutsTitle }}</h2>
87
87
  </header>
88
88
  <div class="sa-dashboard__shortcut-grid">
89
89
  <a
@@ -106,10 +106,14 @@
106
106
  </template>
107
107
 
108
108
  <script setup lang="ts">
109
- import { computed, onMounted, reactive, ref, watch } from 'vue';
109
+ import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
110
110
  import type { AdminManifest, KpiCardDef } from '@saasicat/types';
111
111
  import type { HttpClient } from '../client/types.js';
112
112
  import { buildRoutes } from '../client/nav-builder.js';
113
+ import { formatMessage } from '../client/i18n/format.js';
114
+ import { SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
115
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
116
+ import { useSuperAdminHttp } from '../vue/use-super-admin-context.js';
113
117
 
114
118
  // Platform standard page: Dashboard.
115
119
  //
@@ -204,7 +208,18 @@ interface KpiCardState {
204
208
  value: string | number | null;
205
209
  }
206
210
 
207
- const manifestRef = ref<AdminManifest | null>(props.manifest ?? null);
211
+ const msg = useSaMessages('dashboard');
212
+ const common = useSaMessages('common');
213
+ const { locale, intlLocale } = useSuperAdminI18n();
214
+
215
+ // Provided by createSuperAdminApp({ manifestGuard: { getManifest } }) — lets
216
+ // the page work as a plain route component without a wrapper.
217
+ const injectedManifest = inject(SUPER_ADMIN_MANIFEST_KEY, null);
218
+ // The KPI endpoints need the app's auth. Without this the page would fall back
219
+ // to a bare fetch() and every card would render as an em dash.
220
+ const shellHttp = useSuperAdminHttp();
221
+
222
+ const manifestRef = ref<AdminManifest | null>(props.manifest ?? injectedManifest?.() ?? null);
208
223
  const cards = reactive<KpiCardState[]>([]);
209
224
  const loading = ref(false);
210
225
  const error = ref<Error | null>(null);
@@ -238,7 +253,7 @@ const resolvedShortcuts = computed<ShortcutDef[]>(() => {
238
253
  } else if (!manifestRef.value) {
239
254
  list = [];
240
255
  } else {
241
- const routes = buildRoutes(manifestRef.value);
256
+ const routes = buildRoutes(manifestRef.value, { locale: locale.value });
242
257
  list = routes.map((r) => ({
243
258
  id: r.id,
244
259
  label: r.label,
@@ -325,9 +340,8 @@ function defaultFormat(card: KpiCardDef, body: unknown): KpiFormatted {
325
340
  }
326
341
 
327
342
  async function callHttp(url: string): Promise<{ status: number; json: () => Promise<unknown> }> {
328
- if (props.http) {
329
- return props.http(url, { method: 'GET' });
330
- }
343
+ const http = props.http ?? shellHttp;
344
+ if (http) return http(url, { method: 'GET' });
331
345
  const headers: Record<string, string> = {};
332
346
  const token = props.getAuthToken?.();
333
347
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -354,7 +368,7 @@ function extractSub(
354
368
  }
355
369
  if (hintType === 'value+delta' && typeof body.delta === 'number') {
356
370
  const sign = body.delta > 0 ? '+' : '';
357
- return `${sign}${body.delta} ggü. Vorperiode`;
371
+ return formatMessage(msg.value.deltaVsPreviousPeriod, { delta: `${sign}${body.delta}` });
358
372
  }
359
373
  if (typeof body.sub === 'string') return body.sub;
360
374
  return undefined;
@@ -363,14 +377,14 @@ function extractSub(
363
377
  function formatValue(card: KpiCardState): string {
364
378
  const v = card.value;
365
379
  if (v === null || v === undefined || v === '') return '—';
366
- if (typeof v === 'number') return v.toLocaleString('de-DE');
380
+ if (typeof v === 'number') return v.toLocaleString(intlLocale.value);
367
381
  if (typeof v === 'string') return v;
368
382
  return String(v);
369
383
  }
370
384
 
371
385
  function formatTimestamp(iso: string): string {
372
386
  try {
373
- return new Date(iso).toLocaleString('de-DE', {
387
+ return new Date(iso).toLocaleString(intlLocale.value, {
374
388
  day: '2-digit',
375
389
  month: 'short',
376
390
  year: 'numeric',