@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
@@ -6,13 +6,13 @@
6
6
  </template>
7
7
  {{ error.message }}
8
8
  <template #action>
9
- <q-btn flat label="Erneut laden" @click="load" />
9
+ <q-btn flat :label="common.reload" @click="load" />
10
10
  </template>
11
11
  </q-banner>
12
12
 
13
13
  <div v-if="loading && plans.length === 0" class="sa-plans__loading">
14
14
  <q-spinner size="32px" />
15
- <span>Pläne werden geladen…</span>
15
+ <span>{{ msg.page.loading }}</span>
16
16
  </div>
17
17
 
18
18
  <template v-else>
@@ -177,8 +177,11 @@ import {
177
177
  usePlanVersions,
178
178
  type UsePlansResult,
179
179
  type UsePlanVersionsResult,
180
- } from '../use-plans.js';
181
- import type { HttpClient } from '../types.js';
180
+ } from '../vue/use-plans.js';
181
+ import type { HttpClient } from '../client/types.js';
182
+ import type { SaMessages } from '../client/i18n/index.js';
183
+ import { formatMessage } from '../client/i18n/format.js';
184
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
182
185
  import PlanVersionEditor from '../components/plan-version-editor/PlanVersionEditor.vue';
183
186
  import PlanMatrix from '../components/plan-matrix/PlanMatrix.vue';
184
187
  import PlanDetail from '../components/plan-detail/PlanDetail.vue';
@@ -247,6 +250,9 @@ const props = defineProps<{
247
250
  loadPlanAudit?: (planId: string) => Promise<AuditRow[]>;
248
251
  }>();
249
252
 
253
+ const msg = useSaMessages('plans');
254
+ const common = useSaMessages('common');
255
+
250
256
  const composable: UsePlansResult = usePlans({
251
257
  adminEndpoint: props.adminEndpoint,
252
258
  projectKey: props.projectKey,
@@ -390,7 +396,7 @@ async function onCreateSubmit(payload: PlanCreateSubmit): Promise<void> {
390
396
 
391
397
  await reloadAllVersions();
392
398
  flashHighlight(created.planKey);
393
- flashToast(`Plan ${created.planKey} angelegt — jetzt die erste Version zusammenstellen.`);
399
+ flashToast(formatMessage(msg.value.page.toastPlanCreated, { planKey: created.planKey }));
394
400
  await onOpenPlan(created);
395
401
  openCreateDraftWithPrefill({
396
402
  features: payload.initialFeatures,
@@ -550,7 +556,7 @@ async function onUpdatePlanFromDetail(patch: { label: string }): Promise<void> {
550
556
  selectedPlan.value = updated;
551
557
  } catch (err: unknown) {
552
558
  // eslint-disable-next-line no-console
553
- console.error('[PlansPage] Plan-Rename fehlgeschlagen', err);
559
+ console.error('[PlansPage] Plan rename failed', err);
554
560
  }
555
561
  }
556
562
 
@@ -564,7 +570,10 @@ const availableBundles = ref<BundleEntry[]>([]);
564
570
  // all consumer admin UIs label things identically.
565
571
  const featureCatalogEntries = ref<FeatureCatalogEntryRow[]>([]);
566
572
  const featureRegistry = computed<Record<string, { label?: string; group?: string }>>(() => ({
567
- ...buildFeatureRegistry(featureCatalogEntries.value, props.displayLocale ?? CATALOG_DEFAULT_LOCALE),
573
+ ...buildFeatureRegistry(
574
+ featureCatalogEntries.value,
575
+ props.displayLocale ?? CATALOG_DEFAULT_LOCALE,
576
+ ),
568
577
  ...props.featureRegistry,
569
578
  }));
570
579
 
@@ -659,28 +668,32 @@ function describeDraftSaveError(err: unknown): string {
659
668
  const body = e?.body;
660
669
  const code = body?.code;
661
670
  if (status === 401 || status === 403) {
662
- return 'Sitzung abgelaufen — bitte neu anmelden und erneut speichern.';
671
+ return msg.value.page.errorSessionExpiredSave;
663
672
  }
664
673
  if (status === 422 && code === 'STRICT_MODE_VIOLATIONS') {
665
674
  const list = (body?.warnings ?? [])
666
675
  .map((w) => w.message)
667
676
  .filter(Boolean)
668
677
  .join(' · ');
669
- return `Strict-Mode-Check: ${list || 'Drift gegen den Discovery-Snapshot.'}`;
678
+ return formatMessage(msg.value.page.errorStrictMode, {
679
+ details: list || msg.value.page.errorStrictModeFallback,
680
+ });
670
681
  }
671
682
  if (status === 422 && code === 'PLAN_VERSION_REGRESSION') {
672
- return 'Diese Version ist regressiv (Feature entfernt / Quota gesenkt / Preis erhöht). Publish erfordert Force-Regressive — beim Speichern als Draft sollte das nicht auftreten.';
683
+ return msg.value.page.errorDraftRegression;
673
684
  }
674
685
  if (status === 422 && body?.message) {
675
686
  // e.g. "Plan 'BASIC' hat bereits eine Draft-Version v4 …"
676
687
  return body.message;
677
688
  }
678
689
  if (status !== undefined) {
679
- return body?.message ?? `Speichern fehlgeschlagen (HTTP ${status}).`;
690
+ return (
691
+ body?.message ?? formatMessage(msg.value.page.errorSaveFailedHttp, { status: status })
692
+ );
680
693
  }
681
694
  return err instanceof Error
682
- ? `Speichern fehlgeschlagen: ${err.message}`
683
- : 'Speichern fehlgeschlagen — Details siehe Browser-Konsole.';
695
+ ? formatMessage(msg.value.page.errorSaveFailedDetail, { message: err.message })
696
+ : msg.value.page.errorSaveFailedConsole;
684
697
  }
685
698
  const draftEditing = ref<{
686
699
  editingId: string | null;
@@ -903,13 +916,18 @@ async function onReviewSaveExit(): Promise<void> {
903
916
  if (!saved) return;
904
917
  const planKey = selectedPlan.value?.planKey ?? '';
905
918
  flashHighlight(planKey);
906
- flashToast(`Draft v${saved.version} von ${planKey} gespeichert.`);
919
+ flashToast(
920
+ formatMessage(msg.value.page.toastDraftSaved, {
921
+ version: saved.version,
922
+ planKey,
923
+ }),
924
+ );
907
925
  reviewDraft.value = null;
908
926
  draftEditing.value = null;
909
927
  mode.value = selectedPlan.value ? 'cockpit' : 'list';
910
928
  } catch (err: unknown) {
911
929
  // eslint-disable-next-line no-console
912
- console.error('[PlansPage] Draft speichern fehlgeschlagen', err);
930
+ console.error('[PlansPage] Saving the draft failed', err);
913
931
  reviewError.value = describeDraftSaveError(err);
914
932
  } finally {
915
933
  draftSaving.value = false;
@@ -936,14 +954,17 @@ async function onReviewPublish(payload: {
936
954
  const planKey = selectedPlan.value.planKey;
937
955
  flashHighlight(planKey);
938
956
  flashToast(
939
- `Plan ${planKey} wurde als v${result.planVersion.version} veröffentlicht — sichtbar im Katalog.`,
957
+ formatMessage(msg.value.page.toastPlanPublished, {
958
+ planKey,
959
+ version: result.planVersion.version,
960
+ }),
940
961
  );
941
962
  reviewDraft.value = null;
942
963
  draftEditing.value = null;
943
964
  mode.value = 'cockpit';
944
965
  } catch (err: unknown) {
945
966
  // eslint-disable-next-line no-console
946
- console.error('[PlansPage] Publish fehlgeschlagen', err);
967
+ console.error('[PlansPage] Publishing failed', err);
947
968
  reviewError.value = describePublishError(err);
948
969
  } finally {
949
970
  publishing.value = false;
@@ -974,31 +995,29 @@ async function executeArchive(): Promise<void> {
974
995
  await hardDelete(plan.id);
975
996
  await reloadAllVersions();
976
997
  archiveOpen.value = false;
977
- flashToast(`Plan ${plan.planKey} komplett aus der DB gelöscht.`);
998
+ flashToast(formatMessage(msg.value.page.toastPlanDeleted, { planKey: plan.planKey }));
978
999
  } catch (err: unknown) {
979
1000
  // eslint-disable-next-line no-console
980
- console.error('[PlansPage] Archive/Purge fehlgeschlagen', err);
1001
+ console.error('[PlansPage] Archive/purge failed', err);
981
1002
  const status = (err as { status?: number })?.status;
982
1003
  const body = (err as { body?: { code?: string; message?: string } })?.body;
983
1004
  const errMessage = err instanceof Error ? err.message : String(err);
984
1005
  if (status === 422 && body?.code === 'PLAN_HAS_PUBLISHED_VERSIONS') {
985
- archiveError.value =
986
- body.message ??
987
- 'Plan hat published Versionen — kann nicht gelöscht werden (Vertragsschutz P1).';
1006
+ archiveError.value = body.message ?? msg.value.page.errorPlanHasPublishedVersions;
988
1007
  } else if (status === 422 && body?.code === 'PLAN_HAS_DRAFTS') {
989
- archiveError.value =
990
- body.message ??
991
- 'Plan hat noch einen offenen Draft — bitte erst über das Mülleimer-Icon verwerfen.';
1008
+ archiveError.value = body.message ?? msg.value.page.errorPlanHasDrafts;
992
1009
  } else if (status === 422 && body?.code === 'PLAN_HARD_DELETE_NOT_IMPLEMENTED') {
993
- archiveError.value =
994
- 'Backend unterstützt Hard-Delete noch nicht — API-Server neu bauen + starten.';
1010
+ archiveError.value = msg.value.page.errorHardDeleteNotImplemented;
995
1011
  } else if (status === 404) {
996
- archiveError.value = 'Plan wurde bereits entfernt (404). Liste wird neu geladen.';
1012
+ archiveError.value = msg.value.page.errorPlanAlreadyRemoved;
997
1013
  await reloadAllVersions();
998
1014
  } else if (status !== undefined) {
999
- archiveError.value = body?.message ?? `Vorgang fehlgeschlagen (HTTP ${status}).`;
1015
+ archiveError.value =
1016
+ body?.message ?? formatMessage(msg.value.page.errorOperationFailedHttp, { status });
1000
1017
  } else {
1001
- archiveError.value = `Vorgang fehlgeschlagen: ${errMessage}. Details siehe Browser-Konsole.`;
1018
+ archiveError.value = formatMessage(msg.value.page.errorOperationFailedDetail, {
1019
+ message: errMessage,
1020
+ });
1002
1021
  }
1003
1022
  } finally {
1004
1023
  archiving.value = false;
@@ -1037,35 +1056,38 @@ async function executeDiscard(): Promise<void> {
1037
1056
  await reloadAllVersions();
1038
1057
  discardOpen.value = false;
1039
1058
  flashToast(
1040
- `Draft v${draft.version} von ${plan.planKey} verworfen — Live-Version unverändert.`,
1059
+ formatMessage(msg.value.page.toastDraftDiscarded, {
1060
+ version: draft.version,
1061
+ planKey: plan.planKey,
1062
+ }),
1041
1063
  );
1042
1064
  } catch (err: unknown) {
1043
1065
  // Full error object to the console — in the diagnostic case you can see
1044
1066
  // e.g. CORS preflight errors or the real network stack there.
1045
1067
  // eslint-disable-next-line no-console
1046
- console.error('[PlansPage] Discard fehlgeschlagen', err);
1068
+ console.error('[PlansPage] Discard failed', err);
1047
1069
 
1048
1070
  const status = (err as { status?: number })?.status;
1049
1071
  const body = (err as { body?: { code?: string; message?: string } })?.body;
1050
1072
  const errMessage = err instanceof Error ? err.message : String(err);
1051
1073
 
1052
1074
  if (status === 422 && body?.code === 'PLAN_VERSION_ALREADY_PUBLISHED') {
1053
- discardError.value =
1054
- 'Diese Version ist bereits published und kann nicht verworfen werden.';
1075
+ discardError.value = msg.value.page.errorVersionAlreadyPublished;
1055
1076
  } else if (status === 422 && body?.code === 'PLAN_VERSION_DISCARD_NOT_IMPLEMENTED') {
1056
- discardError.value =
1057
- 'Backend unterstützt das Verwerfen noch nicht — bitte API-Server neu bauen + starten.';
1077
+ discardError.value = msg.value.page.errorDiscardNotImplemented;
1058
1078
  } else if (status === 404) {
1059
- discardError.value =
1060
- 'Draft wurde bereits entfernt (404) — Liste wird gleich neu geladen.';
1079
+ discardError.value = msg.value.page.errorDraftAlreadyRemoved;
1061
1080
  await reloadAllVersions();
1062
1081
  } else if (status !== undefined) {
1063
- discardError.value = body?.message ?? `Verwerfen fehlgeschlagen (HTTP ${status}).`;
1082
+ discardError.value =
1083
+ body?.message ?? formatMessage(msg.value.page.errorDiscardFailedHttp, { status });
1064
1084
  } else {
1065
1085
  // No HTTP status → usually a network error / CORS preflight / failed
1066
1086
  // caller-side (e.g. usePlanVersions construction). Show the real
1067
1087
  // error message so the diagnostic path doesn't end at "HTTP ?".
1068
- discardError.value = `Verwerfen fehlgeschlagen: ${errMessage}. Details siehe Browser-Konsole (oft: API-Server alt gebaut oder CORS blockiert DELETE).`;
1088
+ discardError.value = formatMessage(msg.value.page.errorDiscardFailedDetail, {
1089
+ message: errMessage,
1090
+ });
1069
1091
  }
1070
1092
  } finally {
1071
1093
  discarding.value = false;
@@ -1078,13 +1100,15 @@ async function executeDiscard(): Promise<void> {
1078
1100
  // consistent after the call.
1079
1101
  async function onSubmitTerminate(versionId: string, endsAt: string): Promise<void> {
1080
1102
  if (!planVersions.value) {
1081
- throw new Error('PlanVersions-Composable nicht initialisiert');
1103
+ throw new Error('PlanVersions composable is not initialized');
1082
1104
  }
1083
1105
  await planVersions.value.terminateVersion(versionId, endsAt);
1084
1106
  await reloadCockpitVersions();
1085
1107
  if (selectedPlan.value) {
1086
1108
  flashToast(
1087
- `v${versions.value.find((v) => v.id === versionId)?.version ?? '?'} terminiert.`,
1109
+ formatMessage(msg.value.page.toastVersionTerminated, {
1110
+ version: versions.value.find((v) => v.id === versionId)?.version ?? '?',
1111
+ }),
1088
1112
  );
1089
1113
  }
1090
1114
  }
@@ -1122,7 +1146,10 @@ async function executePublish(): Promise<void> {
1122
1146
  if (selectedPlan.value) {
1123
1147
  flashHighlight(selectedPlan.value.planKey);
1124
1148
  flashToast(
1125
- `Plan ${selectedPlan.value.planKey} wurde als v${result.planVersion.version} veröffentlicht — sichtbar im Katalog.`,
1149
+ formatMessage(msg.value.page.toastPlanPublished, {
1150
+ planKey: selectedPlan.value.planKey,
1151
+ version: result.planVersion.version,
1152
+ }),
1126
1153
  );
1127
1154
  }
1128
1155
  } catch (err: unknown) {
@@ -1140,19 +1167,21 @@ async function executePublish(): Promise<void> {
1140
1167
  }
1141
1168
  }
1142
1169
 
1143
- const REGRESSION_FIELD_LABELS: Record<string, string> = {
1144
- monthlyNet: 'Preis (monatlich, netto)',
1145
- yearlyNet: 'Preis (jährlich, netto)',
1146
- 'features.removed': 'Entfernte Features',
1147
- 'features.added': 'Hinzugefügte Features',
1148
- 'quotas.lowered': 'Gesenkte Quotas',
1149
- 'quotas.raised': 'Erhöhte Quotas',
1150
- 'bundles.removed': 'Entfernte Bundles',
1151
- 'bundles.added': 'Hinzugefügte Bundles',
1170
+ // API field path of a regression change → catalog key of its label.
1171
+ const REGRESSION_FIELD_KEYS: Record<string, keyof SaMessages['plans']['regressionFields']> = {
1172
+ monthlyNet: 'monthlyNet',
1173
+ yearlyNet: 'yearlyNet',
1174
+ 'features.removed': 'featuresRemoved',
1175
+ 'features.added': 'featuresAdded',
1176
+ 'quotas.lowered': 'quotasLowered',
1177
+ 'quotas.raised': 'quotasRaised',
1178
+ 'bundles.removed': 'bundlesRemoved',
1179
+ 'bundles.added': 'bundlesAdded',
1152
1180
  };
1153
1181
 
1154
1182
  function fieldLabel(field: string): string {
1155
- return REGRESSION_FIELD_LABELS[field] ?? field;
1183
+ const key = REGRESSION_FIELD_KEYS[field];
1184
+ return key ? msg.value.regressionFields[key] : field;
1156
1185
  }
1157
1186
 
1158
1187
  function formatChangeValue(value: unknown): string {
@@ -1175,21 +1204,24 @@ function describePublishError(err: unknown): string {
1175
1204
  const status = (err as { status?: number })?.status;
1176
1205
  const body = (err as { body?: { code?: string; message?: string } })?.body;
1177
1206
  if (status === 401 || status === 403) {
1178
- return 'Sitzung abgelaufen — bitte neu anmelden und erneut versuchen.';
1207
+ return msg.value.page.errorSessionExpiredPublish;
1179
1208
  }
1180
1209
  if (status === 422 && body?.code === 'PLAN_VERSION_REGRESSION') {
1181
- return 'Diese Version ist regressiv. Aktiviere "Force-Publish", wenn der Schritt absichtlich ist.';
1210
+ return msg.value.page.errorPublishRegression;
1182
1211
  }
1183
1212
  if (status === 422 && body?.code === 'PLAN_VERSION_VALID_FROM_REQUIRED') {
1184
- return 'Beim Publish muss "Gültig ab" gesetzt sein. Bitte den Draft öffnen und das Datum eintragen.';
1213
+ return msg.value.page.errorValidFromRequired;
1185
1214
  }
1186
1215
  if (status === 422 && body?.code === 'PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS') {
1187
- return '"Gültig ab" muss strikt nach dem "Gültig ab" der Vorgänger-Version liegen. Bitte ein späteres Datum wählen.';
1216
+ return msg.value.page.errorValidFromNotAfterPrevious;
1188
1217
  }
1189
1218
  if (status === 422 && body?.code === 'PLAN_VERSION_ZERO_PRICE') {
1190
- return 'Diese Version hat Preis 0,00 (Schutz gegen Seed-Platzhalter). Aktiviere "Preis 0,00 bewusst zulassen", wenn das ein gewollter kostenloser Sondervertrag ist.';
1219
+ return msg.value.page.errorZeroPrice;
1191
1220
  }
1192
- return body?.message ?? `Publish fehlgeschlagen (HTTP ${status ?? '?'})`;
1221
+ return (
1222
+ body?.message ??
1223
+ formatMessage(msg.value.page.errorPublishFailedHttp, { status: status ?? '?' })
1224
+ );
1193
1225
  }
1194
1226
 
1195
1227
  watch(plans, async () => {