@saasicat/ui-vue 0.5.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 (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
@@ -1,23 +1,29 @@
1
1
  <template>
2
2
  <section class="sa-bundles">
3
3
  <div class="sa-bundles__head">
4
- <h2 class="sa-bundles__title">Bundles</h2>
4
+ <h2 class="sa-bundles__title">{{ msg.bundleOverview.title }}</h2>
5
5
  <span class="sa-bundles__count">{{ bundles.length }}</span>
6
6
  </div>
7
7
 
8
8
  <div v-if="bundles.length === 0" class="sa-bundles__empty">
9
- Keine Bundles vorhanden.
9
+ {{ msg.bundleOverview.empty }}
10
10
  </div>
11
11
 
12
12
  <div v-else class="sa-bundles__grid">
13
- <q-card v-for="bundle in bundles" :key="bundle.bundleKey" flat bordered class="sa-bundle">
13
+ <q-card
14
+ v-for="bundle in bundles"
15
+ :key="bundle.bundleKey"
16
+ flat
17
+ bordered
18
+ class="sa-bundle"
19
+ >
14
20
  <q-card-section class="sa-bundle__header">
15
21
  <div class="sa-bundle__label">{{ bundle.label ?? bundle.bundleKey }}</div>
16
22
  <div class="sa-bundle__key">{{ bundle.bundleKey }}</div>
17
23
  </q-card-section>
18
24
  <q-separator />
19
25
  <q-card-section>
20
- <div class="sa-bundle__caption">Enthält</div>
26
+ <div class="sa-bundle__caption">{{ msg.bundleOverview.contains }}</div>
21
27
  <div class="sa-bundle__chips">
22
28
  <q-chip
23
29
  v-for="feature in bundle.features"
@@ -33,7 +39,7 @@
33
39
  </div>
34
40
  </q-card-section>
35
41
  <q-card-section class="sa-bundle__compat">
36
- <div class="sa-bundle__caption">Kompatibel mit</div>
42
+ <div class="sa-bundle__caption">{{ msg.bundleOverview.compatibleWith }}</div>
37
43
  <div class="sa-bundle__plans">{{ compatLabel(bundle) }}</div>
38
44
  </q-card-section>
39
45
  </q-card>
@@ -43,6 +49,7 @@
43
49
 
44
50
  <script setup lang="ts">
45
51
  import type { PlanRow } from '@saasicat/types';
52
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
46
53
 
47
54
  interface BundleEntry {
48
55
  bundleKey: string;
@@ -57,6 +64,8 @@ const props = defineProps<{
57
64
  featureRegistry: Record<string, { label?: string; group?: string }>;
58
65
  }>();
59
66
 
67
+ const msg = useSaMessages('plans');
68
+
60
69
  function featureLabel(featureKey: string): string {
61
70
  return props.featureRegistry[featureKey]?.label ?? featureKey;
62
71
  }
@@ -68,7 +77,7 @@ function planLabel(planKey: string): string {
68
77
  // Empty compatibility list = bundle applies to all plans (cf. PlanMatrix.hasBundle).
69
78
  function compatLabel(bundle: BundleEntry): string {
70
79
  const keys = bundle.compatiblePlanKeys ?? [];
71
- if (keys.length === 0) return 'Alle Pläne';
80
+ if (keys.length === 0) return msg.value.bundleOverview.allPlans;
72
81
  return keys.map(planLabel).join(', ');
73
82
  }
74
83
  </script>
@@ -6,21 +6,20 @@
6
6
  >
7
7
  <q-card style="min-width: 400px">
8
8
  <q-card-section>
9
- <div class="text-h6">Draft verwerfen?</div>
9
+ <div class="text-h6">{{ msg.discardDialog.title }}</div>
10
10
  <div class="text-body2 q-mt-sm">
11
- Draft v{{ target?.draft.version }} von
12
- <code>{{ target?.plan.planKey }}</code> wird unwiderruflich gelöscht. Published
13
- Versions bleiben unverändert.
11
+ {{ bodyLead }}
12
+ <code>{{ target?.plan.planKey }}</code> {{ msg.discardDialog.bodyTail }}
14
13
  </div>
15
14
  </q-card-section>
16
15
  <q-banner v-if="error" class="bg-warning q-mx-md q-mb-md" rounded>
17
16
  {{ error }}
18
17
  </q-banner>
19
18
  <q-card-actions align="right">
20
- <q-btn flat label="Abbrechen" @click="$emit('update:modelValue', false)" />
19
+ <q-btn flat :label="common.cancel" @click="$emit('update:modelValue', false)" />
21
20
  <q-btn
22
21
  color="negative"
23
- label="Verwerfen"
22
+ :label="common.discard"
24
23
  :loading="discarding"
25
24
  @click="$emit('execute')"
26
25
  />
@@ -30,15 +29,27 @@
30
29
  </template>
31
30
 
32
31
  <script setup lang="ts">
32
+ import { computed } from 'vue';
33
+ import { formatMessage } from '../../client/i18n/format.js';
34
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
33
35
  import type { PlanDiscardTarget } from './types.js';
34
36
 
35
- defineProps<{
37
+ const props = defineProps<{
36
38
  modelValue: boolean;
37
39
  target: PlanDiscardTarget | null;
38
40
  error: string | null;
39
41
  discarding: boolean;
40
42
  }>();
41
43
 
44
+ const msg = useSaMessages('plans');
45
+ const common = useSaMessages('common');
46
+
47
+ const bodyLead = computed(() =>
48
+ formatMessage(msg.value.discardDialog.bodyLead, {
49
+ version: props.target?.draft.version ?? '',
50
+ }),
51
+ );
52
+
42
53
  defineEmits<{
43
54
  (e: 'update:modelValue', value: boolean): void;
44
55
  (e: 'execute'): void;
@@ -6,25 +6,24 @@
6
6
  >
7
7
  <q-card style="min-width: 420px">
8
8
  <q-card-section>
9
- <div class="text-h6">Version publishen?</div>
9
+ <div class="text-h6">{{ msg.publishDialog.title }}</div>
10
10
  <div class="text-body2 q-mt-sm">
11
- Plan <code>{{ selectedPlan?.planKey }}</code
12
- >, Draft v{{ publishTarget?.version }}.
11
+ {{ msg.publishDialog.bodyLead }} <code>{{ selectedPlan?.planKey }}</code
12
+ >{{ bodyTail }}
13
13
  </div>
14
14
  <div class="text-caption text-grey-7 q-mt-sm">
15
- Eine ggf. zuvor live Version wird auf <code>supersededAt</code> gesetzt
16
- (Vertragsschutz P1: Bestand-Subscriptions bleiben auf der alten Version bis zum
17
- nächsten Renewal).
15
+ {{ msg.publishDialog.supersededNoteLead }} <code>supersededAt</code>
16
+ {{ msg.publishDialog.supersededNoteTail }}
18
17
  </div>
19
18
  <q-checkbox
20
19
  :model-value="forceRegressive"
21
- label="Force-Publish auch bei Regression (Feature entfernt / Quota gesenkt / Preis erhöht)"
20
+ :label="msg.publishDialog.forceRegressive"
22
21
  class="q-mt-md"
23
22
  @update:model-value="$emit('update:forceRegressive', Boolean($event))"
24
23
  />
25
24
  <q-checkbox
26
25
  :model-value="allowZeroPrice"
27
- label="Preis 0,00 bewusst zulassen (kostenloser Sondervertrag)"
26
+ :label="msg.publishDialog.allowZeroPrice"
28
27
  class="q-mt-sm"
29
28
  @update:model-value="$emit('update:allowZeroPrice', Boolean($event))"
30
29
  />
@@ -33,7 +32,7 @@
33
32
  {{ publishError }}
34
33
  </q-banner>
35
34
  <q-card-section v-if="regressionChanges.length > 0" class="q-pt-none">
36
- <div class="text-subtitle2 q-mb-xs">Regressive Änderungen:</div>
35
+ <div class="text-subtitle2 q-mb-xs">{{ msg.publishDialog.regressiveChanges }}</div>
37
36
  <ul class="sa-publish-regression">
38
37
  <li v-for="c in regressionChanges" :key="c.field">
39
38
  <strong>{{ fieldLabel(c.field) }}:</strong>
@@ -48,10 +47,10 @@
48
47
  </ul>
49
48
  </q-card-section>
50
49
  <q-card-actions align="right">
51
- <q-btn flat label="Abbrechen" @click="$emit('update:modelValue', false)" />
50
+ <q-btn flat :label="common.cancel" @click="$emit('update:modelValue', false)" />
52
51
  <q-btn
53
52
  color="primary"
54
- label="Publish"
53
+ :label="msg.publishDialog.confirm"
55
54
  :loading="publishing"
56
55
  @click="$emit('execute')"
57
56
  />
@@ -61,10 +60,13 @@
61
60
  </template>
62
61
 
63
62
  <script setup lang="ts">
63
+ import { computed } from 'vue';
64
64
  import type { PlanRow, PlanVersionRow } from '@saasicat/types';
65
+ import { formatMessage } from '../../client/i18n/format.js';
66
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
65
67
  import type { RegressionChange } from './types.js';
66
68
 
67
- defineProps<{
69
+ const props = defineProps<{
68
70
  modelValue: boolean;
69
71
  selectedPlan: PlanRow | null;
70
72
  publishTarget: PlanVersionRow | null;
@@ -77,6 +79,15 @@ defineProps<{
77
79
  formatChangeValue: (value: unknown) => string;
78
80
  }>();
79
81
 
82
+ const msg = useSaMessages('plans');
83
+ const common = useSaMessages('common');
84
+
85
+ const bodyTail = computed(() =>
86
+ formatMessage(msg.value.publishDialog.bodyTail, {
87
+ version: props.publishTarget?.version ?? '',
88
+ }),
89
+ );
90
+
80
91
  defineEmits<{
81
92
  (e: 'update:modelValue', value: boolean): void;
82
93
  (e: 'update:forceRegressive', value: boolean): void;
@@ -28,9 +28,10 @@ export function planAccent(
28
28
  return accents[planId] ?? '#3f6bff';
29
29
  }
30
30
 
31
- export function formatDateDe(iso: string | null | undefined): string {
31
+ /** `locale` is a BCP-47 tag pages pass `intlLocale.value` of the active UI locale. */
32
+ export function formatDate(iso: string | null | undefined, locale: string): string {
32
33
  if (!iso) return '—';
33
- return new Date(iso).toLocaleDateString('de-DE', {
34
+ return new Date(iso).toLocaleDateString(locale, {
34
35
  day: '2-digit',
35
36
  month: 'short',
36
37
  year: 'numeric',
@@ -2,11 +2,8 @@
2
2
  <div class="msb">
3
3
  <header class="msb-head">
4
4
  <div>
5
- <h2 class="msb-title">Meine Bundles</h2>
6
- <p class="msb-sub">
7
- Eigenständig gebuchte Add-On-Pakete zu deinem Plan. Mindestlaufzeit +
8
- Kündigungs- Termin pro Bundle.
9
- </p>
5
+ <h2 class="msb-title">{{ effectiveI18n.myBundlesTitle }}</h2>
6
+ <p class="msb-sub">{{ effectiveI18n.myBundlesSubtitle }}</p>
10
7
  </div>
11
8
  <button class="msb-btn msb-btn--primary" type="button" @click="openAddDialog">
12
9
  <svg
@@ -19,22 +16,24 @@
19
16
  >
20
17
  <path d="M12 5v14M5 12h14" />
21
18
  </svg>
22
- <span>Bundle buchen</span>
19
+ <span>{{ effectiveI18n.bundleBookAction }}</span>
23
20
  </button>
24
21
  </header>
25
22
 
26
23
  <div v-if="error" class="msb-error" role="alert">
27
- <strong>Fehler:</strong> {{ error.message }}
24
+ <strong>{{ effectiveI18n.errorLabel }}:</strong> {{ error.message }}
28
25
  </div>
29
26
 
30
- <div v-if="loading && bundles.length === 0" class="msb-loading">Lade …</div>
27
+ <div v-if="loading && bundles.length === 0" class="msb-loading">
28
+ {{ effectiveI18n.loading }}
29
+ </div>
31
30
 
32
31
  <div v-else-if="bundles.length === 0" class="msb-empty">
33
- Du hast noch kein Bundle gebucht. Über
32
+ {{ effectiveI18n.myBundlesEmptyPrefix }}
34
33
  <button type="button" class="msb-empty-link" @click="openAddDialog">
35
- Bundle buchen
34
+ {{ effectiveI18n.bundleBookAction }}
36
35
  </button>
37
- kannst du dein Paket um zusätzliche Features &amp; Quotas erweitern.
36
+ {{ effectiveI18n.myBundlesEmptySuffix }}
38
37
  </div>
39
38
 
40
39
  <div v-else class="msb-list">
@@ -48,9 +47,10 @@
48
47
  </span>
49
48
  </div>
50
49
  <div class="msb-card-meta">
51
- Gebucht seit {{ formatDate(b.startedAt) }}
50
+ {{ effectiveI18n.myBundlesBookedSince }} {{ formatDate(b.startedAt) }}
52
51
  <template v-if="b.minimumTermEndsAt">
53
- · Mindestlaufzeit bis {{ formatDate(b.minimumTermEndsAt) }}
52
+ · {{ effectiveI18n.bundleMinimumTermUntil }}
53
+ {{ formatDate(b.minimumTermEndsAt) }}
54
54
  </template>
55
55
  </div>
56
56
  </div>
@@ -61,11 +61,16 @@
61
61
  :disabled="cancellingId === b.id"
62
62
  @click="onCancel(b)"
63
63
  >
64
- {{ cancellingId === b.id ? 'Kündige …' : 'Kündigen' }}
64
+ {{
65
+ cancellingId === b.id
66
+ ? effectiveI18n.myBundlesCancelInProgress
67
+ : effectiveI18n.bundleCancelAction
68
+ }}
65
69
  </button>
66
70
  </header>
67
71
  <div v-if="b.canceledAt !== null" class="msb-cancel-info">
68
- Gekündigt am {{ formatDate(b.canceledAt) }} · läuft bis
72
+ {{ effectiveI18n.myBundlesCanceledAt }} {{ formatDate(b.canceledAt) }} ·
73
+ {{ effectiveI18n.myBundlesRunsUntil }}
69
74
  <strong>{{ formatDate(b.canceledEffectiveAt) }}</strong>
70
75
  </div>
71
76
  </article>
@@ -75,11 +80,11 @@
75
80
  <div v-if="addOpen" class="msb-modal-bg" @click="closeAddDialog">
76
81
  <div class="msb-modal" @click.stop>
77
82
  <div class="msb-modal-head">
78
- <div class="msb-modal-title">Bundle buchen</div>
83
+ <div class="msb-modal-title">{{ effectiveI18n.bundleBookAction }}</div>
79
84
  <button
80
85
  class="msb-modal-x"
81
86
  type="button"
82
- aria-label="Schließen"
87
+ :aria-label="effectiveI18n.wizardClose"
83
88
  @click="closeAddDialog"
84
89
  >
85
90
  <svg
@@ -98,9 +103,13 @@
98
103
  <!-- Selection list via public catalog, otherwise fallback to direct UUID input. -->
99
104
  <template v-if="bookable.length > 0">
100
105
  <label class="msb-field">
101
- <span class="msb-field-label">Bundle</span>
106
+ <span class="msb-field-label">{{
107
+ effectiveI18n.myBundlesAddBundleLabel
108
+ }}</span>
102
109
  <select v-model="addForm.bundleVersionId" class="msb-input">
103
- <option value="">— bitte wählen —</option>
110
+ <option value="">
111
+ {{ effectiveI18n.myBundlesAddSelectPlaceholder }}
112
+ </option>
104
113
  <option
105
114
  v-for="b in bookable"
106
115
  :key="b.bundleVersionId"
@@ -108,54 +117,67 @@
108
117
  >
109
118
  {{ b.label }} ({{ b.bundleKey }})
110
119
  <template v-if="b.monthlyNet !== null">
111
- — {{ b.monthlyNet }} € / Mo
120
+ — {{ b.monthlyNet }}
121
+ {{ effectiveI18n.myBundlesPricePerMonthShort }}
112
122
  </template>
113
123
  </option>
114
124
  </select>
115
125
  <span v-if="hiddenBecauseIncompatible > 0" class="msb-field-hint">
116
- {{ hiddenBecauseIncompatible }} weitere Bundle(s) ausgeblendet —
117
- nicht kompatibel mit Plan <code>{{ currentPlanKey }}</code
126
+ {{ hiddenBecauseIncompatible }}
127
+ {{ effectiveI18n.myBundlesHiddenIncompatible }}
128
+ <code>{{ currentPlanKey }}</code
118
129
  >.
119
130
  </span>
120
131
  </label>
121
132
  </template>
122
133
  <template v-else>
123
134
  <label class="msb-field">
124
- <span class="msb-field-label">BundleVersion-ID</span>
135
+ <span class="msb-field-label">
136
+ {{ effectiveI18n.myBundlesBundleVersionIdLabel }}
137
+ </span>
125
138
  <input
126
139
  v-model="addForm.bundleVersionId"
127
140
  class="msb-input"
128
- placeholder="UUID der gewünschten Bundle-Version"
141
+ :placeholder="effectiveI18n.myBundlesBundleVersionIdPlaceholder"
129
142
  />
143
+ <!-- Integration hint for the embedding app, not for tenants. -->
130
144
  <span class="msb-field-hint">
131
- Aus dem öffentlichen Marketing-Catalog. Konsument kann
145
+ From the public marketing catalog. Pass
132
146
  <code>availableBundles</code>
133
- als Prop reichen, dann erscheint hier ein Dropdown.
147
+ as a prop to get a dropdown here.
134
148
  </span>
135
149
  </label>
136
150
  </template>
137
151
  <label class="msb-field">
138
- <span class="msb-field-label">Mindestlaufzeit (Monate, optional)</span>
152
+ <span class="msb-field-label">{{
153
+ effectiveI18n.myBundlesMinimumTermLabel
154
+ }}</span>
139
155
  <input
140
156
  v-model.number="addForm.minimumTermMonths"
141
157
  type="number"
142
158
  min="0"
143
159
  max="120"
144
160
  class="msb-input"
145
- placeholder="Default: 12"
161
+ :placeholder="effectiveI18n.myBundlesMinimumTermPlaceholder"
146
162
  />
147
163
  </label>
148
164
  <div v-if="addError" class="msb-error">{{ addError }}</div>
149
165
  </div>
150
166
  <div class="msb-modal-foot">
151
- <button class="msb-btn" type="button" @click="closeAddDialog">Abbrechen</button>
167
+ <button class="msb-btn" type="button" @click="closeAddDialog">
168
+ {{ effectiveI18n.bundlePreviewClose }}
169
+ </button>
152
170
  <button
153
171
  class="msb-btn msb-btn--primary"
154
172
  type="button"
155
173
  :disabled="!canSubmit || adding"
156
174
  @click="submitAdd"
157
175
  >
158
- {{ adding ? 'Buche …' : 'Bundle buchen' }}
176
+ {{
177
+ adding
178
+ ? effectiveI18n.myBundlesBookInProgress
179
+ : effectiveI18n.bundleBookAction
180
+ }}
159
181
  </button>
160
182
  </div>
161
183
  </div>
@@ -167,7 +189,9 @@
167
189
  import { computed, onMounted, reactive, ref } from 'vue';
168
190
  import type { SubscriptionBundleRecord } from '@saasicat/types';
169
191
 
192
+ import { useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
170
193
  import { useTenantSubscriptionBundles } from '../vue/use-tenant-subscription-bundles.js';
194
+ import { defaultTenantPlanSectionI18n, type TenantPlanSectionI18n } from './default-i18n.js';
171
195
 
172
196
  // MySubscriptionBundlesPage — tenant self-service page "Meine Bundles".
173
197
  // The hosting app embeds the page
@@ -208,6 +232,8 @@ const props = withDefaults(
208
232
  currentPlanKey?: string | null;
209
233
  /** Optional: auth token provider (for the Bearer header). */
210
234
  getAuthToken?: () => string | null;
235
+ /** i18n overrides — missing keys fall back to the active locale's map. */
236
+ i18n?: Partial<TenantPlanSectionI18n>;
211
237
  }>(),
212
238
  {
213
239
  bundleLabels: () => ({}),
@@ -217,6 +243,13 @@ const props = withDefaults(
217
243
  },
218
244
  );
219
245
 
246
+ const { locale, intlLocale } = useSuperAdminI18n();
247
+
248
+ const effectiveI18n = computed<TenantPlanSectionI18n>(() => ({
249
+ ...defaultTenantPlanSectionI18n(locale.value),
250
+ ...(props.i18n ?? {}),
251
+ }));
252
+
220
253
  const { bundles, loading, error, load, add, cancel } = useTenantSubscriptionBundles({
221
254
  billingEndpoint: props.billingEndpoint,
222
255
  getAuthToken: props.getAuthToken,
@@ -308,9 +341,7 @@ async function submitAdd(): Promise<void> {
308
341
  const cancellingId = ref<string | null>(null);
309
342
 
310
343
  async function onCancel(b: SubscriptionBundleRecord): Promise<void> {
311
- const ok = window.confirm(
312
- `Bundle wirklich kündigen? Die Kündigung wird zum nächstmöglichen Termin wirksam.`,
313
- );
344
+ const ok = window.confirm(effectiveI18n.value.myBundlesCancelConfirm);
314
345
  if (!ok) return;
315
346
  cancellingId.value = b.id;
316
347
  try {
@@ -336,9 +367,9 @@ function statusOf(b: SubscriptionBundleRecord): 'active' | 'canceled-pending' |
336
367
 
337
368
  function statusLabel(b: SubscriptionBundleRecord): string {
338
369
  const s = statusOf(b);
339
- if (s === 'active') return 'Aktiv';
340
- if (s === 'canceled-pending') return 'Kündigung wirksam ab …';
341
- return 'Beendet';
370
+ if (s === 'active') return effectiveI18n.value.statusActive;
371
+ if (s === 'canceled-pending') return effectiveI18n.value.myBundlesStatusCanceledPending;
372
+ return effectiveI18n.value.myBundlesStatusEnded;
342
373
  }
343
374
 
344
375
  function cardStatusClass(b: SubscriptionBundleRecord): string {
@@ -349,7 +380,11 @@ function formatDate(date: Date | string | null | undefined): string {
349
380
  if (!date) return '—';
350
381
  const d = typeof date === 'string' ? new Date(date) : date;
351
382
  if (Number.isNaN(d.getTime())) return '—';
352
- return d.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
383
+ return d.toLocaleDateString(intlLocale.value, {
384
+ year: 'numeric',
385
+ month: '2-digit',
386
+ day: '2-digit',
387
+ });
353
388
  }
354
389
  </script>
355
390
 
@@ -220,6 +220,7 @@ import { computed, ref, watch } from 'vue';
220
220
  import LimitsRow from './LimitsRow.vue';
221
221
  import PlanCycleToggle from '../components/plan/PlanCycleToggle.vue';
222
222
  import PlanGrid from '../components/plan/PlanGrid.vue';
223
+ import { useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
223
224
  import type { BillingCycleStr, PlanChangePreviewShape } from '../vue/use-tenant-billing.js';
224
225
  import type { CatalogPlan } from '../vue/use-tenant-billing-catalog.js';
225
226
 
@@ -239,6 +240,7 @@ interface I18nStrings {
239
240
  badgeCurrent: string;
240
241
  badgePopular: string;
241
242
  priceUnitMonthly: string;
243
+ priceUnitYearly: string;
242
244
  priceOnRequest: string;
243
245
  stepChoose: string;
244
246
  stepChooseIntro: string;
@@ -302,8 +304,8 @@ interface Props {
302
304
  formatQuotaLabel?: (key: string, value: number) => string;
303
305
  /**
304
306
  * Plain value per quota key, e.g. "200" or "10 GB" (for PlanGrid).
305
- * Optional — default: takes `value.toLocaleString('de-DE')`, appends a
306
- * `GB` suffix for `storage` keys, replaces -1 with ∞.
307
+ * Optional — default: takes `value.toLocaleString()` in the active UI
308
+ * locale, appends a `GB` suffix for `storage` keys, replaces -1 with ∞.
307
309
  */
308
310
  formatQuotaValue?: (key: string, value: number) => string;
309
311
  quotaLabel: (key: string) => string;
@@ -321,6 +323,8 @@ const emit = defineEmits<{
321
323
  submitted: [];
322
324
  }>();
323
325
 
326
+ const { intlLocale } = useSuperAdminI18n();
327
+
324
328
  const model = computed({
325
329
  get: () => props.modelValue,
326
330
  set: (v) => emit('update:modelValue', v),
@@ -430,7 +434,7 @@ function formatQuotaValueResolved(key: string, value: number | null | undefined)
430
434
  if (value === null || value === undefined || Number.isNaN(value)) return '–';
431
435
  if (value < 0) return '∞';
432
436
  if (key.toLowerCase().includes('storage')) return `${value} GB`;
433
- return value.toLocaleString('de-DE');
437
+ return value.toLocaleString(intlLocale.value);
434
438
  }
435
439
 
436
440
  const cycleI18n = computed(() => ({
@@ -443,7 +447,7 @@ const planGridI18n = computed(() => ({
443
447
  popular: props.i18n.badgePopular,
444
448
  current: props.i18n.badgeCurrent,
445
449
  perMonth: props.i18n.priceUnitMonthly,
446
- perYear: props.i18n.priceUnitMonthly,
450
+ perYear: props.i18n.priceUnitYearly,
447
451
  priceOnRequest: props.i18n.priceOnRequest,
448
452
  }));
449
453
 
@@ -122,7 +122,11 @@
122
122
  <!-- Reactivate confirmation (analogous to cancellation): deliberate action -->
123
123
  <q-dialog
124
124
  :model-value="reactivateConfirmId !== null"
125
- @update:model-value="(v) => { if (!v) closeReactivateConfirm(); }"
125
+ @update:model-value="
126
+ (v) => {
127
+ if (!v) closeReactivateConfirm();
128
+ }
129
+ "
126
130
  >
127
131
  <q-card style="min-width: 360px; max-width: 480px">
128
132
  <q-card-section>
@@ -194,7 +198,7 @@ import { computed, ref } from 'vue';
194
198
  import PackageSnapshotPanel from './PackageSnapshotPanel.vue';
195
199
  import PendingVersionBanner from './PendingVersionBanner.vue';
196
200
  import PlanChangeWizard from './PlanChangeWizard.vue';
197
- import { DEFAULT_I18N_DE, type TenantPlanSectionI18n } from './default-i18n.js';
201
+ import { defaultTenantPlanSectionI18n, type TenantPlanSectionI18n } from './default-i18n.js';
198
202
  import BundlePreviewDialog from './tenant-plan-section/BundlePreviewDialog.vue';
199
203
  import TenantBundleStore from './tenant-plan-section/TenantBundleStore.vue';
200
204
  import TenantFeatureMatrix from './tenant-plan-section/TenantFeatureMatrix.vue';
@@ -210,6 +214,7 @@ import {
210
214
  type CatalogBundle,
211
215
  type CatalogPlan,
212
216
  } from '../vue/use-tenant-billing-catalog.js';
217
+ import { useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
213
218
  import type { HttpClient } from '../client/types.js';
214
219
 
215
220
  // TenantPlanSection — main component for the tenant plan/bundle self-service
@@ -234,7 +239,8 @@ interface Props {
234
239
  * Plain value per quota key (e.g. "200" or "10 GB") — separate from the
235
240
  * label so the wizard's `<PlanGrid>` can render value + label separately.
236
241
  * Optional; without an override the wizard default applies
237
- * (`value.toLocaleString('de-DE')` + `GB` for storage keys, ∞ for -1).
242
+ * (`value.toLocaleString()` in the active UI locale + `GB` for storage
243
+ * keys, ∞ for -1).
238
244
  */
239
245
  formatQuotaValue?: (key: string, value: number) => string;
240
246
 
@@ -245,7 +251,7 @@ interface Props {
245
251
  /** Default: storage quotas (float), all others integer. */
246
252
  isFractionalQuota?: (key: string) => boolean;
247
253
 
248
- /** i18n overrides — missing keys fall back to DEFAULT_I18N_DE. */
254
+ /** i18n overrides — missing keys fall back to the active locale's map. */
249
255
  i18n?: Partial<TenantPlanSectionI18n>;
250
256
 
251
257
  /**
@@ -264,6 +270,8 @@ interface Props {
264
270
 
265
271
  const props = defineProps<Props>();
266
272
 
273
+ const { locale } = useSuperAdminI18n();
274
+
267
275
  const billing = useTenantBilling({
268
276
  http: props.http,
269
277
  getAuthToken: props.getAuthToken,
@@ -287,8 +295,7 @@ const bundleError = ref<string | null>(null);
287
295
 
288
296
  // Bundle preview dialog state (#37/#61)
289
297
  type PendingBundleAction =
290
- | { kind: 'add'; bundleVersionId: string }
291
- | { kind: 'cancel'; subscriptionBundleId: string };
298
+ { kind: 'add'; bundleVersionId: string } | { kind: 'cancel'; subscriptionBundleId: string };
292
299
  const bundlePreviewOpen = ref(false);
293
300
  const bundlePreview = ref<BundlePreviewShape | null>(null);
294
301
  const bundlePreviewLoading = ref(false);
@@ -301,7 +308,7 @@ const loading = computed(() => billing.loading.value || catalog.loading.value);
301
308
  const error = computed(() => billing.error.value ?? catalog.error.value);
302
309
 
303
310
  const effectiveI18n = computed<TenantPlanSectionI18n>(() => ({
304
- ...DEFAULT_I18N_DE,
311
+ ...defaultTenantPlanSectionI18n(locale.value),
305
312
  ...(props.i18n ?? {}),
306
313
  }));
307
314
 
@@ -331,10 +338,7 @@ const availableBundles = computed<CatalogBundle[]>(() => catalog.bundles.value ?
331
338
  // no longer active and disappears from the list.
332
339
  function isSubscriptionBundleActive(b: SubscriptionBundleShape): boolean {
333
340
  if (b.canceledAt === null) return true;
334
- return (
335
- b.canceledEffectiveAt !== null &&
336
- new Date(b.canceledEffectiveAt).getTime() > Date.now()
337
- );
341
+ return b.canceledEffectiveAt !== null && new Date(b.canceledEffectiveAt).getTime() > Date.now();
338
342
  }
339
343
 
340
344
  const bookedBundles = computed<SubscriptionBundleShape[]>(() =>
@@ -348,8 +352,7 @@ const hasBundleStore = computed(
348
352
  const featureRegistry = computed(() => catalog.featureRegistry.value);
349
353
  const activeFeatures = computed<string[]>(() => usage.value?.limits.features ?? []);
350
354
  const hasFeatureOverview = computed(
351
- () =>
352
- Object.keys(featureRegistry.value ?? {}).length > 0 || activeFeatures.value.length > 0,
355
+ () => Object.keys(featureRegistry.value ?? {}).length > 0 || activeFeatures.value.length > 0,
353
356
  );
354
357
 
355
358
  const currentPlanName = computed(() => {
@@ -435,6 +438,7 @@ const wizardI18n = computed(() => ({
435
438
  badgeCurrent: effectiveI18n.value.wizardBadgeCurrent,
436
439
  badgePopular: effectiveI18n.value.wizardBadgePopular,
437
440
  priceUnitMonthly: effectiveI18n.value.wizardPriceUnitMonthly,
441
+ priceUnitYearly: effectiveI18n.value.wizardPriceUnitYearly,
438
442
  priceOnRequest: effectiveI18n.value.wizardPriceOnRequest,
439
443
  stepChoose: effectiveI18n.value.wizardStepChoose,
440
444
  stepChooseIntro: effectiveI18n.value.wizardStepChooseIntro,