@saasicat/ui-vue 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +17 -0
  2. package/dist/{chunk-EH7TOD4H.js → chunk-A3W2N26K.js} +73 -77
  3. package/dist/{chunk-3YP2ZAOD.js → chunk-QZOSDCNP.js} +36 -1
  4. package/dist/chunk-X5SQ5HMB.js +3231 -0
  5. package/dist/client/index.cjs +3279 -76
  6. package/dist/client/index.d.cts +31 -5
  7. package/dist/client/index.d.ts +31 -5
  8. package/dist/client/index.js +23 -5
  9. package/dist/index.cjs +3709 -465
  10. package/dist/index.d.cts +37 -54
  11. package/dist/index.d.ts +37 -54
  12. package/dist/index.js +347 -311
  13. package/dist/messages-7b0P8W75.d.cts +1652 -0
  14. package/dist/messages-7b0P8W75.d.ts +1652 -0
  15. package/dist/quasar/index.cjs +3185 -4
  16. package/dist/quasar/index.d.cts +11 -2
  17. package/dist/quasar/index.d.ts +11 -2
  18. package/dist/quasar/index.js +8 -3
  19. package/dist/testing-e2e/admin-pages-suite.cjs +14 -11
  20. package/dist/testing-e2e/admin-pages-suite.js +14 -11
  21. package/dist/{ui-notify-COUzXEQ4.d.ts → use-super-admin-i18n-B3v3-SWZ.d.ts} +35 -3
  22. package/dist/{ui-notify-D_eAAwRh.d.cts → use-super-admin-i18n-DMktRVEt.d.cts} +35 -3
  23. package/package.json +2 -2
  24. package/src/client/action-registry.ts +4 -4
  25. package/src/client/batch-column-fetcher.ts +4 -4
  26. package/src/client/boot-loader.ts +3 -3
  27. package/src/client/i18n/currency.ts +42 -0
  28. package/src/client/i18n/define.ts +59 -0
  29. package/src/client/i18n/format.ts +12 -0
  30. package/src/client/i18n/index.ts +14 -0
  31. package/src/client/i18n/locale.ts +15 -0
  32. package/src/client/i18n/messages/audit.ts +42 -0
  33. package/src/client/i18n/messages/bundles.ts +459 -0
  34. package/src/client/i18n/messages/common.ts +147 -0
  35. package/src/client/i18n/messages/dashboard.ts +16 -0
  36. package/src/client/i18n/messages/discovery.ts +229 -0
  37. package/src/client/i18n/messages/email.ts +125 -0
  38. package/src/client/i18n/messages/marketing.ts +270 -0
  39. package/src/client/i18n/messages/nav.ts +72 -0
  40. package/src/client/i18n/messages/pilots.ts +192 -0
  41. package/src/client/i18n/messages/plan-detail.ts +266 -0
  42. package/src/client/i18n/messages/plan-editor.ts +177 -0
  43. package/src/client/i18n/messages/plan-versions.ts +251 -0
  44. package/src/client/i18n/messages/plans.ts +503 -0
  45. package/src/client/i18n/messages/promos.ts +202 -0
  46. package/src/client/i18n/messages/shell.ts +145 -0
  47. package/src/client/i18n/messages/tenants.ts +104 -0
  48. package/src/client/i18n/messages/users.ts +78 -0
  49. package/src/client/i18n/messages.ts +84 -0
  50. package/src/client/index.ts +2 -0
  51. package/src/client/manifest-loader.ts +4 -4
  52. package/src/client/nav-builder.ts +44 -51
  53. package/src/client/plan-versions-catalog.ts +42 -30
  54. package/src/components/BundleVersionPublishDialog.vue +35 -24
  55. package/src/components/MarketingPromotionsTab.vue +112 -59
  56. package/src/components/MfaPromptDialog.vue +11 -11
  57. package/src/components/TenantActionConfirmDialog.vue +19 -17
  58. package/src/components/VersionDiffPreview.vue +72 -43
  59. package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
  60. package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
  61. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
  62. package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
  63. package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
  64. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
  65. package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
  66. package/src/components/bundle-editor/bundle-version-status.ts +29 -29
  67. package/src/components/dialogs/PilotCreateDialog.vue +58 -63
  68. package/src/components/dialogs/PilotEditDialog.vue +40 -34
  69. package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
  70. package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
  71. package/src/components/dialogs/types.ts +3 -13
  72. package/src/components/plan/PlanCycleToggle.vue +20 -9
  73. package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
  74. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
  75. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
  76. package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
  77. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
  78. package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
  79. package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
  80. package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
  81. package/src/components/plan-detail/PlanAuditLog.vue +11 -6
  82. package/src/components/plan-detail/PlanDetail.vue +21 -23
  83. package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
  84. package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
  85. package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
  86. package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
  87. package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
  88. package/src/components/plan-list/PlanList.vue +99 -57
  89. package/src/components/plan-matrix/PlanMatrix.vue +85 -38
  90. package/src/components/plan-review/PlanReview.vue +105 -80
  91. package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
  92. package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
  93. package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
  94. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
  95. package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
  96. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
  97. package/src/index.ts +1 -1
  98. package/src/pages-standard/AdminLayout.vue +42 -17
  99. package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
  100. package/src/pages-standard/AuditPage.vue +58 -19
  101. package/src/pages-standard/BundlesPage.vue +29 -18
  102. package/src/pages-standard/DashboardPage.vue +29 -15
  103. package/src/pages-standard/DiscoveryPage.vue +40 -28
  104. package/src/pages-standard/EmailHistoryPage.vue +92 -59
  105. package/src/pages-standard/MarketingCatalogPage.vue +35 -16
  106. package/src/pages-standard/PilotsPage.vue +57 -41
  107. package/src/pages-standard/PlanVersionsPage.vue +7 -3
  108. package/src/pages-standard/PlansPage.vue +89 -57
  109. package/src/pages-standard/PlatformEmailPage.vue +115 -50
  110. package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
  111. package/src/pages-standard/PromoCodesPage.vue +46 -35
  112. package/src/pages-standard/SubscriptionsPage.vue +14 -9
  113. package/src/pages-standard/SuperAdminLoginPage.vue +16 -13
  114. package/src/pages-standard/SuperAdminSetupWizard.vue +51 -47
  115. package/src/pages-standard/TenantDetailPage.vue +50 -36
  116. package/src/pages-standard/TenantsPage.vue +54 -41
  117. package/src/pages-standard/UsersPage.vue +66 -42
  118. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
  119. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
  120. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
  121. package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
  122. package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
  123. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
  124. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
  125. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
  126. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
  127. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
  128. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
  129. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
  130. package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
  131. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
  132. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
  133. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
  134. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
  135. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
  136. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +41 -25
  137. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +45 -26
  138. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +7 -5
  139. package/src/pages-standard/plan-versions/PlanVersionsList.vue +39 -16
  140. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +24 -12
  141. package/src/pages-standard/plan-versions/format.ts +33 -17
  142. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
  143. package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
  144. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
  145. package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
  146. package/src/pages-standard/tenants/format.ts +3 -2
  147. package/src/pages-tenant/MySubscriptionBundlesPage.vue +72 -37
  148. package/src/pages-tenant/PlanChangeWizard.vue +8 -4
  149. package/src/pages-tenant/TenantPlanSection.vue +17 -13
  150. package/src/pages-tenant/default-i18n.ts +217 -1
  151. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +8 -6
  152. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +9 -10
  153. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
  154. package/src/quasar/create-super-admin-app.ts +19 -0
  155. package/src/testing-e2e/admin-pages-suite.ts +14 -11
  156. package/src/vue/project-page-host.ts +5 -3
  157. package/src/vue/use-audit-entries.ts +3 -3
  158. package/src/vue/use-bulk-publish.ts +6 -4
  159. package/src/vue/use-bundle-versions-map.ts +6 -4
  160. package/src/vue/use-bundles.ts +13 -13
  161. package/src/vue/use-catalog-entries.ts +13 -10
  162. package/src/vue/use-discovery.ts +8 -5
  163. package/src/vue/use-entitlement.ts +3 -3
  164. package/src/vue/use-live-plan-versions.ts +4 -4
  165. package/src/vue/use-marketing-projections.ts +9 -6
  166. package/src/vue/use-nav.ts +8 -2
  167. package/src/vue/use-plan-editor.ts +2 -2
  168. package/src/vue/use-plan-versions.ts +3 -3
  169. package/src/vue/use-plans.ts +12 -12
  170. package/src/vue/use-platform-tenant-actions.ts +9 -6
  171. package/src/vue/use-promotions.ts +9 -10
  172. package/src/vue/use-subscription-draft.ts +2 -3
  173. package/src/vue/use-super-admin-context.ts +3 -3
  174. package/src/vue/use-super-admin-i18n.ts +82 -0
  175. package/src/vue/use-tenant-action-flow.ts +11 -6
  176. package/src/vue/use-tenant-manifest.ts +1 -3
  177. package/src/vue/use-tenant-subscription-bundles.ts +4 -4
  178. package/src/vue/use-tenants.ts +3 -3
  179. package/dist/action-registry-CQ2wL3R-.d.cts +0 -85
  180. package/dist/action-registry-CQ2wL3R-.d.ts +0 -85
  181. package/dist/chunk-LVGUSD3T.js +0 -26
  182. package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
  183. package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
  184. package/src/pages-standard/BusinessTypesPage.vue +0 -662
  185. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
  186. package/src/vue/use-business-types.ts +0 -269
@@ -8,15 +8,15 @@
8
8
  >
9
9
  <div class="pve-col-header pve-col-header--basket">
10
10
  <div>
11
- <div class="pve-col-title">Plan-Korb · v{{ form.version }}</div>
12
- <div class="pve-col-sub">Was dieser Plan enthält · live editierbar</div>
11
+ <div class="pve-col-title">{{ title }}</div>
12
+ <div class="pve-col-sub">{{ msg.basket.subtitle }}</div>
13
13
  </div>
14
- <span class="pve-chip pve-chip--changes">{{ changeCount }} Änderungen</span>
14
+ <span class="pve-chip pve-chip--changes">{{ changeCountLabel }}</span>
15
15
  </div>
16
16
 
17
17
  <div class="pve-basket-settings">
18
18
  <div class="pve-bs-row">
19
- <div class="pve-bs-label">Gültig ab</div>
19
+ <div class="pve-bs-label">{{ msg.basket.validFrom }}</div>
20
20
  <input
21
21
  class="pve-bs-input"
22
22
  :class="{ 'pve-bs-input--error': !!validFromError }"
@@ -26,7 +26,7 @@
26
26
  placeholder="YYYY-MM-DD"
27
27
  @input="emitTextInput('update:validFrom', $event)"
28
28
  />
29
- <div class="pve-bs-label pve-bs-label--inline">bis</div>
29
+ <div class="pve-bs-label pve-bs-label--inline">{{ msg.basket.until }}</div>
30
30
  <input
31
31
  class="pve-bs-input"
32
32
  type="date"
@@ -51,7 +51,7 @@
51
51
  <span>{{ validFromError }}</span>
52
52
  </div>
53
53
  <div class="pve-bs-row">
54
- <div class="pve-bs-label">Preis</div>
54
+ <div class="pve-bs-label">{{ msg.sections.price }}</div>
55
55
  <div class="pve-bs-input-grp">
56
56
  <span class="pve-bs-prefix">€</span>
57
57
  <input
@@ -60,7 +60,7 @@
60
60
  inputmode="decimal"
61
61
  @input="emitTextInput('update:monthlyNet', $event)"
62
62
  />
63
- <span class="pve-bs-suffix">/ Mo</span>
63
+ <span class="pve-bs-suffix">{{ msg.perMonthShort }}</span>
64
64
  </div>
65
65
  <div class="pve-bs-input-grp pve-bs-input-grp--gap">
66
66
  <span class="pve-bs-prefix">€</span>
@@ -70,11 +70,11 @@
70
70
  inputmode="decimal"
71
71
  @input="emitTextInput('update:yearlyNet', $event)"
72
72
  />
73
- <span class="pve-bs-suffix">/ J</span>
73
+ <span class="pve-bs-suffix">{{ msg.perYearShort }}</span>
74
74
  </div>
75
75
  </div>
76
76
  <div class="pve-bs-row">
77
- <div class="pve-bs-label">Im Public-Catalog</div>
77
+ <div class="pve-bs-label">{{ msg.basket.inPublicCatalog }}</div>
78
78
  <label class="pve-toggle">
79
79
  <input type="checkbox" :checked="form.marketed" @change="emitCheckboxInput" />
80
80
  <span />
@@ -82,7 +82,7 @@
82
82
  <input
83
83
  class="pve-bs-input pve-bs-input--grow"
84
84
  :value="form.changeNote"
85
- placeholder="Change-Note (Pflicht beim Publish)…"
85
+ :placeholder="msg.basket.changeNotePlaceholder"
86
86
  @input="emitTextInput('update:changeNote', $event)"
87
87
  />
88
88
  </div>
@@ -91,8 +91,8 @@
91
91
  <div class="pve-basket-group">
92
92
  <div class="pve-bg-header">
93
93
  <span class="pve-bg-dot pve-bg-dot--quota" />
94
- <span class="pve-bg-title">Quotas</span>
95
- <span class="pve-bg-count">{{ selectedQuotaList.length }} zugewiesen</span>
94
+ <span class="pve-bg-title">{{ msg.sections.quotas }}</span>
95
+ <span class="pve-bg-count">{{ assignedCount(selectedQuotaList.length) }}</span>
96
96
  </div>
97
97
  <div class="pve-dz">
98
98
  <div v-for="row in selectedQuotaList" :key="row.quotaKey" class="pve-sel-row">
@@ -113,7 +113,7 @@
113
113
  <button
114
114
  class="pve-sel-x"
115
115
  type="button"
116
- aria-label="Quota entfernen"
116
+ :aria-label="msg.basket.removeQuota"
117
117
  @click="$emit('toggle-quota', row.quotaKey, false)"
118
118
  >
119
119
  <svg
@@ -129,7 +129,7 @@
129
129
  </button>
130
130
  </div>
131
131
  <div v-if="selectedQuotaList.length === 0" class="pve-dz-empty">
132
- Quotas aus der Library ziehen oder anklicken
132
+ {{ msg.basket.emptyQuotas }}
133
133
  </div>
134
134
  </div>
135
135
  </div>
@@ -137,8 +137,8 @@
137
137
  <div class="pve-basket-group">
138
138
  <div class="pve-bg-header">
139
139
  <span class="pve-bg-dot pve-bg-dot--feature" />
140
- <span class="pve-bg-title">Features</span>
141
- <span class="pve-bg-count">{{ form.features.length }} zugewiesen</span>
140
+ <span class="pve-bg-title">{{ msg.sections.features }}</span>
141
+ <span class="pve-bg-count">{{ assignedCount(form.features.length) }}</span>
142
142
  </div>
143
143
  <div class="pve-dz">
144
144
  <div v-for="key in sortedSelectedFeatures" :key="key" class="pve-sel-row">
@@ -150,7 +150,7 @@
150
150
  <button
151
151
  class="pve-sel-x"
152
152
  type="button"
153
- aria-label="Feature entfernen"
153
+ :aria-label="msg.basket.removeFeature"
154
154
  @click="$emit('toggle-feature', key, false)"
155
155
  >
156
156
  <svg
@@ -166,7 +166,7 @@
166
166
  </button>
167
167
  </div>
168
168
  <div v-if="form.features.length === 0" class="pve-dz-empty">
169
- Features aus der Library ziehen oder anklicken
169
+ {{ msg.basket.emptyFeatures }}
170
170
  </div>
171
171
  </div>
172
172
  </div>
@@ -174,8 +174,8 @@
174
174
  <div class="pve-basket-group">
175
175
  <div class="pve-bg-header">
176
176
  <span class="pve-bg-dot pve-bg-dot--bundle" />
177
- <span class="pve-bg-title">Bundles</span>
178
- <span class="pve-bg-count">{{ activeBundles.length }} zugewiesen</span>
177
+ <span class="pve-bg-title">{{ msg.sections.bundles }}</span>
178
+ <span class="pve-bg-count">{{ assignedCount(activeBundles.length) }}</span>
179
179
  </div>
180
180
  <div class="pve-dz">
181
181
  <div v-for="b in activeBundles" :key="b.bundleKey" class="pve-sel-row">
@@ -183,13 +183,13 @@
183
183
  <div class="pve-sel-body">
184
184
  <div class="pve-sel-label">{{ b.label || b.bundleKey }}</div>
185
185
  <div class="pve-sel-sub">
186
- {{ b.bundleKey }} · {{ b.features.length }} Features
186
+ {{ b.bundleKey }} · {{ bundleFeatureCount(b.features.length) }}
187
187
  </div>
188
188
  </div>
189
189
  <button
190
190
  class="pve-sel-x"
191
191
  type="button"
192
- aria-label="Bundle entfernen"
192
+ :aria-label="msg.basket.removeBundle"
193
193
  @click="$emit('toggle-bundle', b, false)"
194
194
  >
195
195
  <svg
@@ -205,7 +205,7 @@
205
205
  </button>
206
206
  </div>
207
207
  <div v-if="activeBundles.length === 0" class="pve-dz-empty pve-dz-empty--center">
208
- Keine Bundles zugewiesen — aus der Library ziehen
208
+ {{ msg.basket.emptyBundles }}
209
209
  </div>
210
210
  </div>
211
211
  </div>
@@ -213,9 +213,12 @@
213
213
  </template>
214
214
 
215
215
  <script setup lang="ts">
216
+ import { computed } from 'vue';
217
+ import { formatMessage } from '../../client/i18n/format.js';
218
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
216
219
  import type { BundleEntry, DraftForm, SelectedQuotaRow } from './types.js';
217
220
 
218
- defineProps<{
221
+ const props = defineProps<{
219
222
  form: DraftForm;
220
223
  dragOver: boolean;
221
224
  changeCount: number;
@@ -243,6 +246,24 @@ const emit = defineEmits<{
243
246
  (e: 'drop'): void;
244
247
  }>();
245
248
 
249
+ const msg = useSaMessages('planEditor');
250
+
251
+ const title = computed(() =>
252
+ formatMessage(msg.value.basket.title, { version: props.form.version }),
253
+ );
254
+
255
+ const changeCountLabel = computed(() =>
256
+ formatMessage(msg.value.basket.changeCount, { count: props.changeCount }),
257
+ );
258
+
259
+ function assignedCount(count: number): string {
260
+ return formatMessage(msg.value.basket.assignedCount, { count });
261
+ }
262
+
263
+ function bundleFeatureCount(count: number): string {
264
+ return formatMessage(msg.value.basket.bundleFeatureCount, { count });
265
+ }
266
+
246
267
  type TextInputEvent =
247
268
  | 'update:validFrom'
248
269
  | 'update:validUntil'
@@ -4,20 +4,17 @@
4
4
  <div class="pve-diff-head">
5
5
  <div>
6
6
  <div id="pve-diff-title" class="pve-diff-title">
7
- Diff
7
+ {{ msg.diffDialog.title }}
8
8
  <template v-if="predecessorVersion">
9
9
  v{{ predecessorVersion.version }} → v{{ version }}
10
10
  </template>
11
11
  </div>
12
- <div class="pve-diff-sub">
13
- Was sich gegenüber der Vorgänger-Version ändert (aktueller, ungespeicherter
14
- Stand).
15
- </div>
12
+ <div class="pve-diff-sub">{{ msg.diffDialog.subtitle }}</div>
16
13
  </div>
17
14
  <button
18
15
  class="pve-diff-close"
19
16
  type="button"
20
- aria-label="Schließen"
17
+ :aria-label="common.close"
21
18
  @click="$emit('update:modelValue', false)"
22
19
  >
23
20
  <svg
@@ -61,7 +58,7 @@
61
58
  >
62
59
  </div>
63
60
  <div v-if="diffRows.length === 0" class="pve-diff-empty">
64
- Keine Änderungen gegenüber der Vorgänger-Version.
61
+ {{ msg.diffDialog.empty }}
65
62
  </div>
66
63
  </div>
67
64
  </div>
@@ -69,6 +66,7 @@
69
66
  </template>
70
67
 
71
68
  <script setup lang="ts">
69
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
72
70
  import type { EditorDiffRow, PredecessorVersion } from './types.js';
73
71
 
74
72
  defineProps<{
@@ -81,4 +79,7 @@ defineProps<{
81
79
  defineEmits<{
82
80
  (e: 'update:modelValue', value: boolean): void;
83
81
  }>();
82
+
83
+ const msg = useSaMessages('planEditor');
84
+ const common = useSaMessages('common');
84
85
  </script>
@@ -109,6 +109,9 @@
109
109
 
110
110
  <script setup lang="ts">
111
111
  import { computed, onMounted, reactive, ref } from 'vue';
112
+ import { formatMessage } from '../../client/i18n/format.js';
113
+ import { formatCurrency } from '../../client/i18n/currency.js';
114
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
112
115
  import PlanComponentPool from './PlanComponentPool.vue';
113
116
  import PlanCatalogPreview from './PlanCatalogPreview.vue';
114
117
  import PlanVersionBasket from './PlanVersionBasket.vue';
@@ -187,6 +190,10 @@ const emit = defineEmits<{
187
190
  (e: 'cancel'): void;
188
191
  }>();
189
192
 
193
+ const msg = useSaMessages('planEditor');
194
+ const common = useSaMessages('common');
195
+ const { locale, intlLocale } = useSuperAdminI18n();
196
+
190
197
  const form = reactive<DraftForm>({
191
198
  version: props.initialForm.version,
192
199
  features: [...props.initialForm.features],
@@ -238,13 +245,25 @@ const searchTerm = ref('');
238
245
  const activeTab = ref<PoolTab>('features');
239
246
 
240
247
  const poolTabs = computed<PoolTabItem[]>(() => [
241
- { id: 'features' as PoolTab, label: 'Features', count: filteredFeatureCount.value },
242
- { id: 'quotas' as PoolTab, label: 'Quotas', count: filteredQuotas.value.length },
243
- { id: 'bundles' as PoolTab, label: 'Bundles', count: filteredBundles.value.length },
248
+ {
249
+ id: 'features' as PoolTab,
250
+ label: msg.value.sections.features,
251
+ count: filteredFeatureCount.value,
252
+ },
253
+ {
254
+ id: 'quotas' as PoolTab,
255
+ label: msg.value.sections.quotas,
256
+ count: filteredQuotas.value.length,
257
+ },
258
+ {
259
+ id: 'bundles' as PoolTab,
260
+ label: msg.value.sections.bundles,
261
+ count: filteredBundles.value.length,
262
+ },
244
263
  ]);
245
264
 
246
265
  function normalize(s: string): string {
247
- return s.toLocaleLowerCase('de-DE');
266
+ return s.toLocaleLowerCase(intlLocale.value);
248
267
  }
249
268
 
250
269
  function matchesSearch(haystack: string[], needle: string): boolean {
@@ -258,12 +277,12 @@ function featureLabel(key: string): string {
258
277
  }
259
278
 
260
279
  function featureGroupLabel(key: string): string {
261
- return props.featureRegistry?.[key]?.group ?? 'Allgemein';
280
+ return props.featureRegistry?.[key]?.group ?? common.value.general;
262
281
  }
263
282
 
264
283
  // Pool lists sorted by display label (not by key/snapshot order),
265
284
  // so that planning stays predictable — same order as in the basket.
266
- const byLabel = (a: string, b: string) => a.localeCompare(b, 'de-DE');
285
+ const byLabel = (a: string, b: string) => a.localeCompare(b, intlLocale.value);
267
286
 
268
287
  const filteredFeatures = computed(() =>
269
288
  props.availableFeatures
@@ -282,7 +301,7 @@ const filteredFeatureGroups = computed<FeatureGroup[]>(() => {
282
301
  byGroup.set(g, list);
283
302
  }
284
303
  return [...byGroup.entries()]
285
- .sort(([a], [b]) => a.localeCompare(b, 'de-DE'))
304
+ .sort(([a], [b]) => byLabel(a, b))
286
305
  .map(([key, rows]) => ({ key, label: key, rows }));
287
306
  });
288
307
 
@@ -414,8 +433,7 @@ function toTitleCase(s: string): string {
414
433
  function formatMoney(raw: string): string {
415
434
  if (!/^\d+(\.\d{1,2})?$/.test(raw)) return raw + ' €';
416
435
  const num = Number(raw);
417
- if (Number.isInteger(num)) return `${num} €`;
418
- return `${num.toFixed(2).replace('.', ',')} €`;
436
+ return formatCurrency(num, locale.value);
419
437
  }
420
438
 
421
439
  const formattedMonthly = computed(() => formatMoney(form.monthlyNet));
@@ -429,7 +447,7 @@ const yearlySavingsLabel = computed(() => {
429
447
  if (y >= fullYear) return null;
430
448
  const pct = Math.round(((fullYear - y) / fullYear) * 100);
431
449
  if (pct <= 0) return null;
432
- return `${pct} % günstiger`;
450
+ return formatMessage(msg.value.editor.yearlySavings, { percent: pct });
433
451
  });
434
452
 
435
453
  // ── Validation ──────────────────────────────────────────────────────
@@ -443,10 +461,11 @@ const validFromError = computed<string | null>(() => {
443
461
  const draftDay = form.validFrom.slice(0, 10);
444
462
  const prevDay = prev.validFrom.slice(0, 10);
445
463
  if (draftDay <= prevDay) {
446
- return (
447
- `„Gültig ab" (${draftDay}) muss nach dem „Gültig ab" der ` +
448
- `Vorgänger-Version v${prev.version} (${prevDay}) liegen.`
449
- );
464
+ return formatMessage(msg.value.editor.validFromError, {
465
+ draftDay,
466
+ version: prev.version,
467
+ prevDay,
468
+ });
450
469
  }
451
470
  return null;
452
471
  });
@@ -484,47 +503,52 @@ const canSave = computed(() => {
484
503
  });
485
504
 
486
505
  const predecessorValidUntilHint = computed(() => {
487
- if (!form.validFrom) return 'ab-Datum − 1 Tag';
506
+ const fallback = msg.value.editor.validUntilHintFallback;
507
+ if (!form.validFrom) return fallback;
488
508
  const d = new Date(form.validFrom);
489
- if (Number.isNaN(d.getTime())) return 'ab-Datum − 1 Tag';
509
+ if (Number.isNaN(d.getTime())) return fallback;
490
510
  d.setDate(d.getDate() - 1);
491
511
  return d.toISOString().slice(0, 10);
492
512
  });
493
513
 
494
514
  const checklist = computed<ChecklistItem[]>(() => {
515
+ const texts = msg.value.editor;
495
516
  const items: ChecklistItem[] = [
496
517
  {
497
518
  id: 'prices',
498
- label: 'Preise gesetzt (Mo + J)',
519
+ label: texts.checklistPrices,
499
520
  ok:
500
521
  /^\d+(\.\d{1,2})?$/.test(form.monthlyNet) &&
501
522
  /^\d+(\.\d{1,2})?$/.test(form.yearlyNet),
502
523
  },
503
524
  {
504
525
  id: 'note',
505
- label: 'Change-Note vorhanden',
526
+ label: texts.checklistChangeNote,
506
527
  ok: form.changeNote.trim().length > 0,
507
528
  },
508
529
  {
509
530
  id: 'valid-from',
510
- label: 'Gültigkeit gesetzt',
531
+ label: texts.checklistValidity,
511
532
  ok: !!form.validFrom,
512
533
  },
513
534
  {
514
535
  id: 'valid-from-order',
515
- label: 'Gültig-ab liegt nach der Vorgänger-Version',
536
+ label: texts.checklistValidFromOrder,
516
537
  ok: validFromError.value === null,
517
538
  },
518
539
  {
519
540
  id: 'min-feature',
520
- label: 'Mindestens 1 Feature zugewiesen',
541
+ label: texts.checklistMinFeature,
521
542
  ok: form.features.length > 0,
522
543
  },
523
544
  ];
524
545
  if (props.tenantImpactCount > 0) {
525
546
  items.push({
526
547
  id: 'tenant-impact',
527
- label: `Tenant-Impact: <b>${props.tenantImpactCount} Mandanten</b> erhalten v${form.version} automatisch`,
548
+ label: formatMessage(texts.checklistTenantImpact, {
549
+ count: props.tenantImpactCount,
550
+ version: form.version,
551
+ }),
528
552
  ok: false,
529
553
  });
530
554
  }
@@ -537,14 +561,22 @@ const checklistOkCount = computed(() => checklist.value.filter((c) => c.ok).leng
537
561
  const showDiff = ref(false);
538
562
 
539
563
  const DIFF_STYLE = {
540
- added: { bg: '#ecfdf5', border: '#a7f3d0', color: '#047857', sign: '+', tag: 'neu' },
541
- removed: { bg: '#fef2f2', border: '#fecaca', color: '#b91c1c', sign: '−', tag: 'entfernt' },
542
- changed: { bg: '#fffbeb', border: '#fde68a', color: '#b45309', sign: '~', tag: 'geändert' },
564
+ added: { bg: '#ecfdf5', border: '#a7f3d0', color: '#047857', sign: '+' },
565
+ removed: { bg: '#fef2f2', border: '#fecaca', color: '#b91c1c', sign: '−' },
566
+ changed: { bg: '#fffbeb', border: '#fde68a', color: '#b45309', sign: '~' },
543
567
  } as const;
544
568
 
569
+ const diffStyles = computed(() => ({
570
+ added: { ...DIFF_STYLE.added, tag: msg.value.editor.diffTagAdded },
571
+ removed: { ...DIFF_STYLE.removed, tag: msg.value.editor.diffTagRemoved },
572
+ changed: { ...DIFF_STYLE.changed, tag: msg.value.editor.diffTagChanged },
573
+ }));
574
+
545
575
  const diffRows = computed<EditorDiffRow[]>(() => {
546
576
  const prev = props.predecessorVersion;
547
577
  if (!prev) return [];
578
+ const style = diffStyles.value;
579
+ const sections = msg.value.sections;
548
580
  const out: EditorDiffRow[] = [];
549
581
 
550
582
  // Features
@@ -554,10 +586,10 @@ const diffRows = computed<EditorDiffRow[]>(() => {
554
586
  if (!prevFeatures.has(f)) {
555
587
  out.push({
556
588
  id: `add-f-${f}`,
557
- section: 'Features',
589
+ section: sections.features,
558
590
  label: featureLabel(f),
559
591
  sub: f,
560
- ...DIFF_STYLE.added,
592
+ ...style.added,
561
593
  });
562
594
  }
563
595
  }
@@ -565,10 +597,10 @@ const diffRows = computed<EditorDiffRow[]>(() => {
565
597
  if (!curFeatures.has(f)) {
566
598
  out.push({
567
599
  id: `rem-f-${f}`,
568
- section: 'Features',
600
+ section: sections.features,
569
601
  label: featureLabel(f),
570
602
  sub: f,
571
- ...DIFF_STYLE.removed,
603
+ ...style.removed,
572
604
  });
573
605
  }
574
606
  }
@@ -585,43 +617,45 @@ const diffRows = computed<EditorDiffRow[]>(() => {
585
617
  if (before === undefined && after !== undefined) {
586
618
  out.push({
587
619
  id: `add-q-${k}`,
588
- section: 'Quotas',
620
+ section: sections.quotas,
589
621
  label,
590
622
  sub: k,
591
623
  to: fmt(after),
592
- ...DIFF_STYLE.added,
624
+ ...style.added,
593
625
  });
594
626
  } else if (before !== undefined && after === undefined) {
595
627
  out.push({
596
628
  id: `rem-q-${k}`,
597
- section: 'Quotas',
629
+ section: sections.quotas,
598
630
  label,
599
631
  sub: k,
600
632
  from: fmt(before),
601
- ...DIFF_STYLE.removed,
633
+ ...style.removed,
602
634
  });
603
635
  } else if (before !== undefined && after !== undefined && before !== after) {
604
636
  out.push({
605
637
  id: `chg-q-${k}`,
606
- section: 'Quotas',
638
+ section: sections.quotas,
607
639
  label,
608
640
  sub: k,
609
641
  from: fmt(before),
610
642
  to: fmt(after),
611
- ...DIFF_STYLE.changed,
643
+ ...style.changed,
612
644
  });
613
645
  }
614
646
  }
615
647
 
616
648
  // Price
617
649
  if (prev.monthlyNet !== form.monthlyNet || prev.yearlyNet !== form.yearlyNet) {
650
+ const perMonth = msg.value.perMonthShort;
651
+ const perYear = msg.value.perYearShort;
618
652
  out.push({
619
653
  id: 'chg-price',
620
- section: 'Preis',
621
- label: 'Monats- + Jahrespreis',
622
- from: `${formatMoney(prev.monthlyNet)} / Mo · ${formatMoney(prev.yearlyNet)} / J`,
623
- to: `${formattedMonthly.value} / Mo · ${formattedYearly.value} / J`,
624
- ...DIFF_STYLE.changed,
654
+ section: sections.price,
655
+ label: msg.value.editor.diffPriceLabel,
656
+ from: `${formatMoney(prev.monthlyNet)} ${perMonth} · ${formatMoney(prev.yearlyNet)} ${perYear}`,
657
+ to: `${formattedMonthly.value} ${perMonth} · ${formattedYearly.value} ${perYear}`,
658
+ ...style.changed,
625
659
  });
626
660
  }
627
661
 
@@ -2,16 +2,15 @@
2
2
  <header class="pve-bar">
3
3
  <div class="pve-bar-left">
4
4
  <div class="pve-titlechip">
5
- <span class="pve-titlechip-kicker">PLAN</span>
5
+ <span class="pve-titlechip-kicker">{{ msg.header.planKicker }}</span>
6
6
  <span class="pve-chip pve-chip--plan">{{ planKey }}</span>
7
7
  </div>
8
- <h2 class="pve-title">
9
- {{ editingId ? `Draft v${version} bearbeiten` : `Neue Version v${version}` }}
10
- </h2>
11
- <span class="pve-chip pve-chip--draft pve-chip--dot">Draft</span>
8
+ <h2 class="pve-title">{{ title }}</h2>
9
+ <span class="pve-chip pve-chip--draft pve-chip--dot">{{ msg.header.draftChip }}</span>
12
10
  <span class="pve-bar-note">
13
- Vorgänger wird beim Publish auf
14
- <code class="pve-mono">{{ predecessorValidUntilHint }}</code> superseded
11
+ {{ msg.header.supersedeNoteBefore }}
12
+ <code class="pve-mono">{{ predecessorValidUntilHint }}</code>
13
+ {{ msg.header.supersedeNoteAfter }}
15
14
  </span>
16
15
  </div>
17
16
  <div class="pve-bar-right">
@@ -19,7 +18,7 @@
19
18
  class="pve-btn"
20
19
  type="button"
21
20
  :disabled="!hasPredecessor"
22
- :title="hasPredecessor ? undefined : 'Keine Vorgänger-Version (v1)'"
21
+ :title="hasPredecessor ? undefined : msg.header.noPredecessorHint"
23
22
  @click="$emit('showDiff')"
24
23
  >
25
24
  <span class="pve-ico" aria-hidden="true">
@@ -35,7 +34,7 @@
35
34
  <circle cx="12" cy="12" r="3" />
36
35
  </svg>
37
36
  </span>
38
- <span>Diff vs. Vorgänger</span>
37
+ <span>{{ msg.header.diffButton }}</span>
39
38
  </button>
40
39
  <button class="pve-btn" type="button" @click="$emit('cancel')">
41
40
  <span class="pve-ico" style="transform: rotate(180deg)" aria-hidden="true">
@@ -50,7 +49,7 @@
50
49
  <path d="M5 12h14M13 5l7 7-7 7" />
51
50
  </svg>
52
51
  </span>
53
- <span>Zurück</span>
52
+ <span>{{ common.back }}</span>
54
53
  </button>
55
54
  <button
56
55
  class="pve-btn pve-btn--primary"
@@ -58,7 +57,7 @@
58
57
  :disabled="!canSave"
59
58
  @click="$emit('save')"
60
59
  >
61
- <span>Weiter · Review</span>
60
+ <span>{{ msg.header.saveButton }}</span>
62
61
  <span class="pve-ico" aria-hidden="true">
63
62
  <svg
64
63
  width="14"
@@ -77,7 +76,11 @@
77
76
  </template>
78
77
 
79
78
  <script setup lang="ts">
80
- defineProps<{
79
+ import { computed } from 'vue';
80
+ import { formatMessage } from '../../client/i18n/format.js';
81
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
82
+
83
+ const props = defineProps<{
81
84
  planKey: string;
82
85
  editingId: string | null;
83
86
  version: number;
@@ -91,4 +94,13 @@ defineEmits<{
91
94
  (e: 'cancel'): void;
92
95
  (e: 'save'): void;
93
96
  }>();
97
+
98
+ const msg = useSaMessages('planEditor');
99
+ const common = useSaMessages('common');
100
+
101
+ const title = computed(() =>
102
+ formatMessage(props.editingId ? msg.value.header.editDraft : msg.value.header.newVersion, {
103
+ version: props.version,
104
+ }),
105
+ );
94
106
  </script>
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export * from './client/index.js';
32
32
  export * from './vue/super-admin-context.js';
33
33
  export * from './vue/ui-notify.js';
34
34
  export * from './vue/use-super-admin-context.js';
35
+ export * from './vue/use-super-admin-i18n.js';
35
36
 
36
37
  // ---------------------------------------------------------------------------
37
38
  // Vue layer: composables, guards, hosts, store factory.
@@ -58,7 +59,6 @@ export * from './vue/use-catalog-entries.js';
58
59
  export * from './vue/use-bundles.js';
59
60
  export * from './vue/use-bundle-versions-map.js';
60
61
  export * from './vue/use-tenant-subscription-bundles.js';
61
- export * from './vue/use-business-types.js';
62
62
  export * from './vue/use-marketing-projections.js';
63
63
  export * from './vue/use-promotions.js';
64
64
  export * from './vue/use-plans.js';