@saasicat/ui-vue 0.4.0 → 0.6.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 (214) hide show
  1. package/README.md +64 -4
  2. package/dist/chunk-A3W2N26K.js +714 -0
  3. package/dist/chunk-QZOSDCNP.js +98 -0
  4. package/dist/chunk-X5SQ5HMB.js +3231 -0
  5. package/dist/client/index.cjs +3988 -0
  6. package/dist/client/index.d.cts +321 -0
  7. package/dist/client/index.d.ts +321 -0
  8. package/dist/client/index.js +70 -0
  9. package/dist/index.cjs +4446 -1249
  10. package/dist/index.d.cts +297 -888
  11. package/dist/index.d.ts +297 -888
  12. package/dist/index.js +609 -1348
  13. package/dist/messages-7b0P8W75.d.cts +1652 -0
  14. package/dist/messages-7b0P8W75.d.ts +1652 -0
  15. package/dist/quasar/index.cjs +3340 -0
  16. package/dist/quasar/index.d.cts +95 -0
  17. package/dist/quasar/index.d.ts +95 -0
  18. package/dist/quasar/index.js +96 -0
  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/use-super-admin-i18n-B3v3-SWZ.d.ts +210 -0
  22. package/dist/use-super-admin-i18n-DMktRVEt.d.cts +210 -0
  23. package/package.json +23 -3
  24. package/src/{action-registry.ts → client/action-registry.ts} +4 -4
  25. package/src/{batch-column-fetcher.ts → client/batch-column-fetcher.ts} +4 -4
  26. package/src/{boot-loader.ts → 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 +27 -0
  51. package/src/{manifest-loader.ts → client/manifest-loader.ts} +4 -4
  52. package/src/{nav-builder.ts → client/nav-builder.ts} +44 -51
  53. package/src/{plan-versions-catalog.ts → client/plan-versions-catalog.ts} +42 -30
  54. package/src/components/BundleVersionPublishDialog.vue +35 -24
  55. package/src/components/FeatureGate.vue +1 -1
  56. package/src/components/MarketingPromotionsTab.vue +112 -59
  57. package/src/components/MfaPromptDialog.vue +11 -11
  58. package/src/components/TenantActionConfirmDialog.vue +19 -17
  59. package/src/components/VersionDiffPreview.vue +72 -43
  60. package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
  61. package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
  62. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
  63. package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
  64. package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
  65. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
  66. package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
  67. package/src/components/bundle-editor/bundle-version-status.ts +29 -29
  68. package/src/components/dialogs/PilotCreateDialog.vue +58 -63
  69. package/src/components/dialogs/PilotEditDialog.vue +40 -34
  70. package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
  71. package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
  72. package/src/components/dialogs/types.ts +3 -13
  73. package/src/components/plan/PlanCycleToggle.vue +21 -10
  74. package/src/components/plan/PlanGrid.vue +2 -2
  75. package/src/components/plan/PriceSummary.vue +1 -1
  76. package/src/components/plan/PromoCodeInput.vue +1 -1
  77. package/src/components/plan/PublicBundleGrid.vue +1 -1
  78. package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
  79. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
  80. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
  81. package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
  82. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
  83. package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
  84. package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
  85. package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
  86. package/src/components/plan-detail/PlanAuditLog.vue +11 -6
  87. package/src/components/plan-detail/PlanDetail.vue +21 -23
  88. package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
  89. package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
  90. package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
  91. package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
  92. package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
  93. package/src/components/plan-list/PlanList.vue +99 -57
  94. package/src/components/plan-matrix/PlanMatrix.vue +85 -38
  95. package/src/components/plan-review/PlanReview.vue +105 -80
  96. package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
  97. package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
  98. package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
  99. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
  100. package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
  101. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
  102. package/src/index.ts +82 -79
  103. package/src/pages-standard/AdminLayout.vue +43 -18
  104. package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
  105. package/src/pages-standard/AuditPage.vue +58 -19
  106. package/src/pages-standard/BundlesPage.vue +29 -18
  107. package/src/pages-standard/DashboardPage.vue +31 -17
  108. package/src/pages-standard/DiscoveryPage.vue +40 -28
  109. package/src/pages-standard/EmailHistoryPage.vue +98 -69
  110. package/src/pages-standard/MarketingCatalogPage.vue +40 -21
  111. package/src/pages-standard/PilotsPage.vue +64 -52
  112. package/src/pages-standard/PlanVersionsPage.vue +9 -5
  113. package/src/pages-standard/PlansPage.vue +91 -59
  114. package/src/pages-standard/PlatformEmailPage.vue +120 -53
  115. package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
  116. package/src/pages-standard/PromoCodesPage.vue +51 -54
  117. package/src/pages-standard/SubscriptionsPage.vue +14 -9
  118. package/src/pages-standard/SuperAdminLoginPage.vue +19 -16
  119. package/src/pages-standard/SuperAdminSetupWizard.vue +53 -49
  120. package/src/pages-standard/TenantDetailPage.vue +54 -40
  121. package/src/pages-standard/TenantsPage.vue +57 -44
  122. package/src/pages-standard/UsersPage.vue +70 -52
  123. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
  124. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
  125. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
  126. package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
  127. package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
  128. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
  129. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
  130. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
  131. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
  132. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
  133. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
  134. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
  135. package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
  136. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
  137. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
  138. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
  139. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
  140. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
  141. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +42 -26
  142. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +46 -27
  143. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +8 -6
  144. package/src/pages-standard/plan-versions/PlanVersionsList.vue +40 -17
  145. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +1 -1
  146. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +25 -13
  147. package/src/pages-standard/plan-versions/format.ts +33 -17
  148. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
  149. package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
  150. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
  151. package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
  152. package/src/pages-standard/tenants/format.ts +3 -2
  153. package/src/pages-tenant/MySubscriptionBundlesPage.vue +73 -38
  154. package/src/pages-tenant/OnboardingConfigurator.vue +3 -3
  155. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  156. package/src/pages-tenant/PlanChangeWizard.vue +10 -6
  157. package/src/pages-tenant/TenantPlanSection.vue +20 -16
  158. package/src/pages-tenant/default-i18n.ts +217 -1
  159. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +9 -7
  160. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +11 -12
  161. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
  162. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +1 -1
  163. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +1 -1
  164. package/src/quasar/create-super-admin-app.ts +192 -0
  165. package/src/quasar/index.ts +11 -0
  166. package/src/quasar/notify.ts +30 -0
  167. package/src/testing-e2e/admin-pages-suite.ts +14 -11
  168. package/src/{manifest-store-factory.ts → vue/manifest-store-factory.ts} +1 -1
  169. package/src/{platform-loaders.ts → vue/platform-loaders.ts} +4 -4
  170. package/src/{project-page-host.ts → vue/project-page-host.ts} +6 -4
  171. package/src/{create-super-admin-app.ts → vue/super-admin-context.ts} +20 -139
  172. package/src/vue/ui-notify.ts +30 -0
  173. package/src/{use-actions.ts → vue/use-actions.ts} +1 -1
  174. package/src/{use-api-list.ts → vue/use-api-list.ts} +1 -1
  175. package/src/{use-audit-entries.ts → vue/use-audit-entries.ts} +3 -3
  176. package/src/{use-batch-columns.ts → vue/use-batch-columns.ts} +1 -1
  177. package/src/{use-bulk-publish.ts → vue/use-bulk-publish.ts} +7 -5
  178. package/src/{use-bundle-versions-map.ts → vue/use-bundle-versions-map.ts} +7 -5
  179. package/src/{use-bundles.ts → vue/use-bundles.ts} +14 -14
  180. package/src/{use-catalog-entries.ts → vue/use-catalog-entries.ts} +14 -11
  181. package/src/{use-discovery.ts → vue/use-discovery.ts} +9 -6
  182. package/src/{use-entitlement.ts → vue/use-entitlement.ts} +4 -4
  183. package/src/{use-live-plan-versions.ts → vue/use-live-plan-versions.ts} +5 -5
  184. package/src/{use-manifest.ts → vue/use-manifest.ts} +1 -1
  185. package/src/{use-marketing-projections.ts → vue/use-marketing-projections.ts} +10 -7
  186. package/src/{use-nav.ts → vue/use-nav.ts} +9 -3
  187. package/src/{use-plan-editor.ts → vue/use-plan-editor.ts} +2 -2
  188. package/src/{use-plan-versions.ts → vue/use-plan-versions.ts} +3 -3
  189. package/src/{use-plans.ts → vue/use-plans.ts} +13 -13
  190. package/src/{use-platform-tenant-actions.ts → vue/use-platform-tenant-actions.ts} +9 -6
  191. package/src/{use-promotions.ts → vue/use-promotions.ts} +10 -11
  192. package/src/{use-public-boot.ts → vue/use-public-boot.ts} +1 -1
  193. package/src/{use-subscription-draft.ts → vue/use-subscription-draft.ts} +2 -3
  194. package/src/{use-super-admin-context.ts → vue/use-super-admin-context.ts} +5 -5
  195. package/src/vue/use-super-admin-i18n.ts +82 -0
  196. package/src/{use-tenant-action-flow.ts → vue/use-tenant-action-flow.ts} +12 -7
  197. package/src/{use-tenant-billing-catalog.ts → vue/use-tenant-billing-catalog.ts} +2 -2
  198. package/src/{use-tenant-billing.ts → vue/use-tenant-billing.ts} +2 -2
  199. package/src/{use-tenant-manifest.ts → vue/use-tenant-manifest.ts} +2 -4
  200. package/src/{use-tenant-subscription-bundles.ts → vue/use-tenant-subscription-bundles.ts} +5 -5
  201. package/src/{use-tenants.ts → vue/use-tenants.ts} +3 -3
  202. package/dist/testing-e2e/admin-pages-suite.d.cts +0 -37
  203. package/dist/testing-e2e/admin-pages-suite.d.ts +0 -37
  204. package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
  205. package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
  206. package/src/pages-standard/BusinessTypesPage.vue +0 -662
  207. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
  208. package/src/use-business-types.ts +0 -269
  209. /package/src/{http-json.ts → client/http-json.ts} +0 -0
  210. /package/src/{types.ts → client/types.ts} +0 -0
  211. /package/src/{version.ts → client/version.ts} +0 -0
  212. /package/src/{create-admin-routes.ts → vue/create-admin-routes.ts} +0 -0
  213. /package/src/{entitlement-provider.ts → vue/entitlement-provider.ts} +0 -0
  214. /package/src/{feature-router-guard.ts → vue/feature-router-guard.ts} +0 -0
@@ -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
- import type { HttpClient } from '../types.js';
112
- import { buildRoutes } from '../nav-builder.js';
111
+ import type { HttpClient } from '../client/types.js';
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',
@@ -11,7 +11,7 @@
11
11
 
12
12
  <q-banner v-if="error" class="sa-discovery__error" rounded>
13
13
  <template #avatar><q-icon name="warning" color="negative" /></template>
14
- Fehler: {{ error.message }}
14
+ {{ common.error }}: {{ error.message }}
15
15
  </q-banner>
16
16
 
17
17
  <DiscoveryKpis
@@ -25,8 +25,8 @@
25
25
  />
26
26
 
27
27
  <q-tabs v-model="activeTab" align="left" dense class="sa-discovery__tabs">
28
- <q-tab name="features" :label="`Features (${features.length})`" />
29
- <q-tab name="quotas" :label="`Quotas (${quotas.length})`" />
28
+ <q-tab name="features" :label="featuresTabLabel" />
29
+ <q-tab name="quotas" :label="quotasTabLabel" />
30
30
  </q-tabs>
31
31
 
32
32
  <q-tab-panels v-model="activeTab" animated class="sa-discovery__panels">
@@ -37,7 +37,7 @@
37
37
  dense
38
38
  outlined
39
39
  clearable
40
- placeholder="Feature-Key, Label oder Capability suchen …"
40
+ :placeholder="msg.searchPlaceholder"
41
41
  class="sa-discovery__search"
42
42
  >
43
43
  <template #prepend><q-icon name="search" /></template>
@@ -48,7 +48,7 @@
48
48
  outlined
49
49
  emit-value
50
50
  map-options
51
- :options="STATUS_FILTER_OPTIONS"
51
+ :options="statusFilterOptions"
52
52
  class="sa-discovery__filter"
53
53
  />
54
54
  </div>
@@ -76,21 +76,20 @@
76
76
  </div>
77
77
  </div>
78
78
  <div v-if="filteredFeatures.length === 0" class="sa-discovery__empty-row">
79
- Keine Features entsprechen den Filtern.
79
+ {{ msg.noFeaturesMatchFilters }}
80
80
  </div>
81
81
 
82
82
  <div v-if="orphanCaps.length" class="sa-discovery__group">
83
83
  <div class="sa-discovery__group-head">
84
84
  <span class="sa-discovery__group-title sa-discovery__group-title--orphan">
85
85
  <q-icon name="warning" size="14px" />
86
- Capabilities ohne Feature
86
+ {{ msg.orphansTitle }}
87
87
  </span>
88
88
  <span class="sa-discovery__group-count">{{ orphanCaps.length }}</span>
89
89
  </div>
90
90
  <p class="sa-discovery__orphan-hint">
91
- Diesen Capabilities fehlt der <code>feature:</code>-Tag im
92
- <code>@ImplementsCapability</code>-Decorator Feature-Zuordnung im Code
93
- nachziehen, sonst sind sie nicht verkaufbar.
91
+ {{ msg.orphanHint.before }} <code>feature:</code>{{ msg.orphanHint.middle }}
92
+ <code>@ImplementsCapability</code>{{ msg.orphanHint.after }}
94
93
  </p>
95
94
  <DiscoveryCapList
96
95
  :capabilities="orphanCaps"
@@ -113,7 +112,7 @@
113
112
  @quota-locale="onQuotaLocale"
114
113
  />
115
114
  <div v-if="quotas.length === 0" class="sa-discovery__empty-row">
116
- Keine Quotas im Code deklariert.
115
+ {{ msg.noQuotasDeclared }}
117
116
  </div>
118
117
  </div>
119
118
  </q-tab-panel>
@@ -140,7 +139,9 @@ import DiscoveryHeader from './discovery-page/DiscoveryHeader.vue';
140
139
  import DiscoveryKpis from './discovery-page/DiscoveryKpis.vue';
141
140
  import DiscoveryMetaBanner from './discovery-page/DiscoveryMetaBanner.vue';
142
141
  import DiscoveryQuotaCard from './discovery-page/DiscoveryQuotaCard.vue';
143
- import { STATUS_META } from './discovery-page/discovery-ui.js';
142
+ import { formatMessage } from '../client/i18n/format.js';
143
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
144
+ import { statusLabel } from './discovery-page/discovery-ui.js';
144
145
 
145
146
  // Platform standard page: Discovery review, feature-centric (#20).
146
147
  // Two tabs (Features + Quotas); each entry is an expandable card with
@@ -174,18 +175,31 @@ const props = defineProps<{
174
175
  }>();
175
176
 
176
177
  const activeTab = ref<'features' | 'quotas'>('features');
178
+
179
+ const featuresTabLabel = computed(() =>
180
+ formatMessage(msg.value.tabFeatures, { count: props.features.length }),
181
+ );
182
+ const quotasTabLabel = computed(() =>
183
+ formatMessage(msg.value.tabQuotas, { count: props.quotas.length }),
184
+ );
177
185
  const featureQuery = ref('');
178
186
  const statusFilter = ref<DiscoveryStatus | 'all'>('all');
179
187
  const expandedFeature = ref<string | null>(null);
180
188
  const expandedQuota = ref<string | null>(null);
181
189
 
182
- const STATUS_FILTER_OPTIONS: Array<{ label: string; value: DiscoveryStatus | 'all' }> = [
183
- { label: 'Alle Status', value: 'all' },
184
- ...(['pending', 'approved', 'outdated', 'obsolete'] as const).map((status) => ({
185
- label: STATUS_META[status].label,
186
- value: status,
187
- })),
188
- ];
190
+ const msg = useSaMessages('discovery');
191
+ const common = useSaMessages('common');
192
+ const { locale, intlLocale } = useSuperAdminI18n();
193
+
194
+ const statusFilterOptions = computed<Array<{ label: string; value: DiscoveryStatus | 'all' }>>(
195
+ () => [
196
+ { label: msg.value.statusFilterAll, value: 'all' },
197
+ ...(['pending', 'approved', 'outdated', 'obsolete'] as const).map((status) => ({
198
+ label: statusLabel(status, locale.value),
199
+ value: status,
200
+ })),
201
+ ],
202
+ );
189
203
 
190
204
  const appKey = computed(() => props.snapshot?.app.key ?? '—');
191
205
  const appLabel = computed(() => {
@@ -194,9 +208,9 @@ const appLabel = computed(() => {
194
208
  });
195
209
  const appVersion = computed(() => props.snapshot?.app.version ?? '0.0.0');
196
210
  const scanLabel = computed(() => {
197
- if (!props.snapshot?.scannedAt) return 'noch nicht gescannt';
211
+ if (!props.snapshot?.scannedAt) return msg.value.notScannedYet;
198
212
  try {
199
- return new Date(props.snapshot.scannedAt).toLocaleString('de-DE');
213
+ return new Date(props.snapshot.scannedAt).toLocaleString(intlLocale.value);
200
214
  } catch {
201
215
  return props.snapshot.scannedAt;
202
216
  }
@@ -277,21 +291,19 @@ const filteredFeatures = computed(() => {
277
291
  });
278
292
  });
279
293
 
280
- const NO_OWNER_LABEL = 'Ohne Owner';
281
-
282
- /** Grouping by primary owner (#14); "Ohne Owner" last. */
294
+ /** Grouping by primary owner (#14); the ownerless group goes last. */
283
295
  const featureGroups = computed<Array<{ label: string; features: FeatureCatalogEntryRow[] }>>(() => {
284
296
  const groups = new Map<string, FeatureCatalogEntryRow[]>();
285
297
  for (const f of filteredFeatures.value) {
286
- const owner = ownersByFeature.value.get(f.featureKey)?.[0] ?? NO_OWNER_LABEL;
298
+ const owner = ownersByFeature.value.get(f.featureKey)?.[0] ?? msg.value.noOwner;
287
299
  const list = groups.get(owner);
288
300
  if (list) list.push(f);
289
301
  else groups.set(owner, [f]);
290
302
  }
291
303
  return [...groups.entries()]
292
304
  .sort((a, b) => {
293
- if (a[0] === NO_OWNER_LABEL) return 1;
294
- if (b[0] === NO_OWNER_LABEL) return -1;
305
+ if (a[0] === msg.value.noOwner) return 1;
306
+ if (b[0] === msg.value.noOwner) return -1;
295
307
  return a[0].localeCompare(b[0]);
296
308
  })
297
309
  .map(([label, features]) => ({ label, features }));
@@ -340,7 +352,7 @@ function persist(p: Promise<unknown>): void {
340
352
  p.catch((err) => {
341
353
  // Auth renewal/redirect is handled by the HTTP client; here just log
342
354
  // so a failed save stays visible.
343
- console.error('Catalog-Entry konnte nicht gespeichert werden', err);
355
+ console.error('Failed to save catalog entry', err);
344
356
  });
345
357
  }
346
358