@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
@@ -8,13 +8,15 @@
8
8
  <div class="pcd-modal" role="dialog" aria-labelledby="pcd-title">
9
9
  <div class="pcd-head">
10
10
  <div class="pcd-head-text">
11
- <div id="pcd-title" class="pcd-title">Neuen Plan anlegen</div>
12
- <div class="pcd-sub">
13
- Plan-Stamm anlegen. Im nächsten Schritt weist du Features, Quotas und
14
- Bundles zu.
15
- </div>
11
+ <div id="pcd-title" class="pcd-title">{{ msg.createDialog.title }}</div>
12
+ <div class="pcd-sub">{{ msg.createDialog.subtitle }}</div>
16
13
  </div>
17
- <button class="pcd-close" type="button" aria-label="Schließen" @click="onCancel">
14
+ <button
15
+ class="pcd-close"
16
+ type="button"
17
+ :aria-label="common.close"
18
+ @click="onCancel"
19
+ >
18
20
  <svg
19
21
  width="14"
20
22
  height="14"
@@ -32,41 +34,43 @@
32
34
  <div class="pcd-row pcd-row--2col">
33
35
  <div class="pcd-field">
34
36
  <div class="pcd-field-label">
35
- Plan-Key
37
+ {{ msg.createDialog.labelPlanKey }}
36
38
  <span class="pcd-kbd">UNIQUE</span>
37
39
  </div>
38
40
  <input
39
41
  ref="keyInput"
40
42
  class="pcd-input"
41
43
  :class="{ 'pcd-input--error': keyError }"
42
- placeholder="z. B. SCALE"
44
+ :placeholder="msg.createDialog.placeholderPlanKey"
43
45
  :value="form.planKey"
44
46
  @input="onPlanKeyInput"
45
47
  />
46
48
  <div v-if="keyError" class="pcd-hint pcd-hint--error">{{ keyError }}</div>
47
- <div v-else class="pcd-hint">
48
- Großbuchstaben + Unterstriche · API-stabil, nicht änderbar nach Publish
49
- </div>
49
+ <div v-else class="pcd-hint">{{ msg.createDialog.hintPlanKey }}</div>
50
50
  </div>
51
51
  <div class="pcd-field">
52
- <div class="pcd-field-label">Anzeigename</div>
53
- <input class="pcd-input" placeholder="z. B. Scale" v-model="form.label" />
54
- <div class="pcd-hint">Wie der Plan im Catalog erscheint</div>
52
+ <div class="pcd-field-label">{{ msg.createDialog.labelDisplayName }}</div>
53
+ <input
54
+ class="pcd-input"
55
+ :placeholder="msg.createDialog.placeholderDisplayName"
56
+ v-model="form.label"
57
+ />
58
+ <div class="pcd-hint">{{ msg.createDialog.hintDisplayName }}</div>
55
59
  </div>
56
60
  </div>
57
61
 
58
62
  <div class="pcd-field">
59
- <div class="pcd-field-label">Beschreibung</div>
63
+ <div class="pcd-field-label">{{ common.description }}</div>
60
64
  <textarea
61
65
  class="pcd-input pcd-input--textarea"
62
66
  rows="2"
63
- placeholder="Was bekommt der Kunde mit diesem Plan?"
67
+ :placeholder="msg.createDialog.placeholderDescription"
64
68
  v-model="form.description"
65
69
  />
66
70
  </div>
67
71
 
68
72
  <div class="pcd-field">
69
- <div class="pcd-field-label">Basis für die erste Version</div>
73
+ <div class="pcd-field-label">{{ msg.createDialog.labelBasis }}</div>
70
74
  <div class="pcd-choice-grid">
71
75
  <button
72
76
  v-for="opt in choiceOptions"
@@ -87,7 +91,7 @@
87
91
 
88
92
  <div class="pcd-foot">
89
93
  <button class="pcd-btn pcd-btn--ghost" type="button" @click="onCancel">
90
- Abbrechen
94
+ {{ common.cancel }}
91
95
  </button>
92
96
  <button
93
97
  class="pcd-btn pcd-btn--primary"
@@ -95,7 +99,9 @@
95
99
  :disabled="!canSubmit || submitting"
96
100
  @click="onSubmit"
97
101
  >
98
- <span>{{ submitting ? 'Wird angelegt…' : 'Weiter · Komponenten' }}</span>
102
+ <span>{{
103
+ submitting ? msg.createDialog.submitting : msg.createDialog.submit
104
+ }}</span>
99
105
  <span class="pcd-ico" aria-hidden="true">
100
106
  <svg
101
107
  width="14"
@@ -116,6 +122,8 @@
116
122
 
117
123
  <script setup lang="ts">
118
124
  import { computed, reactive, ref, watch } from 'vue';
125
+ import { formatMessage } from '../../client/i18n/format.js';
126
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
119
127
 
120
128
  // PlanCreateDialog — step 1 of the "new plan" flow from the plan
121
129
  // simulation. Collects the plan master data (key, label, description) plus the
@@ -168,13 +176,16 @@ const emit = defineEmits<{
168
176
  (e: 'cancel'): void;
169
177
  }>();
170
178
 
171
- const EMPTY_BASIS: TemplateOption = {
179
+ const msg = useSaMessages('plans');
180
+ const common = useSaMessages('common');
181
+
182
+ const emptyBasis = computed<TemplateOption>(() => ({
172
183
  key: 'empty',
173
- label: 'Leerer Plan',
184
+ label: msg.value.createDialog.emptyPlan,
174
185
  features: [],
175
186
  quotas: {},
176
187
  bundles: [],
177
- };
188
+ }));
178
189
 
179
190
  const form = reactive({
180
191
  planKey: '',
@@ -206,10 +217,10 @@ function onPlanKeyInput(e: Event): void {
206
217
  const keyError = computed<string | null>(() => {
207
218
  if (!form.planKey) return null;
208
219
  if (!/^[A-Z][A-Z0-9_]*$/.test(form.planKey)) {
209
- return 'Muss mit Buchstabe beginnen, nur A–Z, 0–9, _.';
220
+ return msg.value.createDialog.errorKeyFormat;
210
221
  }
211
222
  if (props.existingPlanKeys.includes(form.planKey)) {
212
- return `„${form.planKey}" existiert bereits.`;
223
+ return formatMessage(msg.value.createDialog.errorKeyExists, { key: form.planKey });
213
224
  }
214
225
  return null;
215
226
  });
@@ -222,12 +233,12 @@ const choiceOptions = computed(() => {
222
233
  const options = [
223
234
  {
224
235
  key: 'empty',
225
- title: 'Leerer Plan',
226
- subtitle: 'Im Editor alles selbst zusammenstellen',
236
+ title: msg.value.createDialog.emptyPlan,
237
+ subtitle: msg.value.createDialog.emptyPlanHint,
227
238
  },
228
239
  ...props.availableTemplates.map((t) => ({
229
240
  key: t.key,
230
- title: `Klon von ${t.label}`,
241
+ title: formatMessage(msg.value.createDialog.cloneOf, { label: t.label }),
231
242
  subtitle: countsLabel(t),
232
243
  })),
233
244
  ];
@@ -235,17 +246,16 @@ const choiceOptions = computed(() => {
235
246
  });
236
247
 
237
248
  function countsLabel(t: TemplateOption): string {
238
- const parts = [
239
- `${t.features.length} Features`,
240
- `${Object.keys(t.quotas).length} Quotas`,
241
- `${t.bundles.length} Bundles`,
242
- ];
243
- return parts.join(' · ');
249
+ return formatMessage(msg.value.createDialog.counts, {
250
+ features: t.features.length,
251
+ quotas: Object.keys(t.quotas).length,
252
+ bundles: t.bundles.length,
253
+ });
244
254
  }
245
255
 
246
256
  function selectedTemplate(): TemplateOption {
247
- if (form.basis === 'empty') return EMPTY_BASIS;
248
- return props.availableTemplates.find((t) => t.key === form.basis) ?? EMPTY_BASIS;
257
+ if (form.basis === 'empty') return emptyBasis.value;
258
+ return props.availableTemplates.find((t) => t.key === form.basis) ?? emptyBasis.value;
249
259
  }
250
260
 
251
261
  function onSubmit(): void {
@@ -2,12 +2,12 @@
2
2
  <section class="pd-panel pd-audit">
3
3
  <div class="pd-panel-head">
4
4
  <div style="min-width: 0">
5
- <h3 class="pd-panel-title">Audit-Log</h3>
6
- <div class="pd-panel-sub">Letzte Änderungen an diesem Plan</div>
5
+ <h3 class="pd-panel-title">{{ msg.auditLog.title }}</h3>
6
+ <div class="pd-panel-sub">{{ msg.auditLog.subtitle }}</div>
7
7
  </div>
8
8
  </div>
9
9
  <div class="pd-audit-body">
10
- <div v-if="loadingAudit" class="pd-diff-empty">Lade Audit-Log…</div>
10
+ <div v-if="loadingAudit" class="pd-diff-empty">{{ msg.auditLog.loading }}</div>
11
11
  <div v-for="a in auditRows" :key="a.id" class="pd-audit-row">
12
12
  <span :class="['pd-audit-dot', `pd-audit-${auditKind(a.action)}`]" />
13
13
  <span class="pd-audit-when">{{ formatAuditDate(a.createdAt) }}</span>
@@ -20,6 +20,8 @@
20
20
  </template>
21
21
 
22
22
  <script setup lang="ts">
23
+ import { formatMessage } from '../../client/i18n/format.js';
24
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
23
25
  import type { AuditRow } from './types.js';
24
26
 
25
27
  defineProps<{
@@ -27,6 +29,9 @@ defineProps<{
27
29
  loadingAudit: boolean;
28
30
  }>();
29
31
 
32
+ const msg = useSaMessages('planDetail');
33
+ const { intlLocale } = useSuperAdminI18n();
34
+
30
35
  function auditKind(action: string): string {
31
36
  const a = action.toLowerCase();
32
37
  if (a.includes('publish')) return 'publish';
@@ -51,9 +56,9 @@ function formatAuditDate(iso: string): string {
51
56
  const d = new Date(iso);
52
57
  if (Number.isNaN(d.getTime())) return iso;
53
58
  const isToday = d.toDateString() === new Date().toDateString();
54
- const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
59
+ const time = d.toLocaleTimeString(intlLocale.value, { hour: '2-digit', minute: '2-digit' });
55
60
  return isToday
56
- ? `heute · ${time}`
57
- : `${d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' })} · ${time}`;
61
+ ? formatMessage(msg.value.auditLog.today, { time })
62
+ : `${d.toLocaleDateString(intlLocale.value, { day: '2-digit', month: 'short' })} · ${time}`;
58
63
  }
59
64
  </script>
@@ -77,11 +77,9 @@
77
77
 
78
78
  <script setup lang="ts">
79
79
  import { computed, ref, watch } from 'vue';
80
- import {
81
- isVersionEditable,
82
- type PlanRow,
83
- type PlanVersionRow,
84
- } from '@saasicat/types';
80
+ import { isVersionEditable, type PlanRow, type PlanVersionRow } from '@saasicat/types';
81
+ import { formatCurrency } from '../../client/i18n/currency.js';
82
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
85
83
  import PlanAuditLog from './PlanAuditLog.vue';
86
84
  import PlanDetailHeader from './PlanDetailHeader.vue';
87
85
  import PlanDetailKpis from './PlanDetailKpis.vue';
@@ -143,6 +141,9 @@ const emit = defineEmits<{
143
141
  (e: 'terminate', versionId: string, endsAt: string): void;
144
142
  }>();
145
143
 
144
+ const msg = useSaMessages('planDetail');
145
+ const { locale, intlLocale } = useSuperAdminI18n();
146
+
146
147
  // ── Status / Selection ──────────────────────────────────────────────
147
148
  function statusOf(v: PlanVersionRow): PlanVersionStatus {
148
149
  if (v.publishedAt === null) return 'draft';
@@ -201,7 +202,7 @@ const timelineTicks = computed(() => {
201
202
  for (const v of chronological.value) {
202
203
  if (v.validFrom) ticks.push(v.validFrom.slice(0, 7));
203
204
  }
204
- ticks.push('jetzt');
205
+ ticks.push(msg.value.versions.timelineNow);
205
206
  return ticks;
206
207
  });
207
208
 
@@ -209,9 +210,7 @@ const timelineTicks = computed(() => {
209
210
  function formatMoney(raw: string | number): string {
210
211
  const num = typeof raw === 'string' ? Number(raw) : raw;
211
212
  if (!Number.isFinite(num)) return String(raw);
212
- if (num === 0) return '0 €';
213
- if (Number.isInteger(num)) return `${num} €`;
214
- return `${num.toFixed(2).replace('.', ',')} €`;
213
+ return formatCurrency(num, locale.value);
215
214
  }
216
215
  function quotasOf(v: PlanVersionRow): Record<string, number> {
217
216
  if (v.quotas && Object.keys(v.quotas).length > 0) return v.quotas;
@@ -290,7 +289,7 @@ const diffRows = computed<DiffRow[]>(() => {
290
289
  id: 'af-' + f,
291
290
  kind: 'add',
292
291
  sign: '+',
293
- tag: 'neu',
292
+ tag: msg.value.diff.tagNew,
294
293
  label: featureLabel(f),
295
294
  key: f,
296
295
  });
@@ -299,7 +298,7 @@ const diffRows = computed<DiffRow[]>(() => {
299
298
  id: 'rf-' + f,
300
299
  kind: 'rm',
301
300
  sign: '−',
302
- tag: 'entfernt',
301
+ tag: msg.value.diff.tagRemoved,
303
302
  label: featureLabel(f),
304
303
  key: f,
305
304
  });
@@ -309,7 +308,7 @@ const diffRows = computed<DiffRow[]>(() => {
309
308
  id: 'cq-' + q.key,
310
309
  kind: 'mod',
311
310
  sign: '~',
312
- tag: 'geändert',
311
+ tag: msg.value.diff.tagChanged,
313
312
  label: quotaLabel(q.key),
314
313
  key: q.key,
315
314
  from: `${q.from} ${u}`.trim(),
@@ -322,7 +321,7 @@ const diffRows = computed<DiffRow[]>(() => {
322
321
  id: 'aq-' + q.key,
323
322
  kind: 'add',
324
323
  sign: '+',
325
- tag: 'neu',
324
+ tag: msg.value.diff.tagNew,
326
325
  label: quotaLabel(q.key),
327
326
  key: q.key,
328
327
  from: '—',
@@ -335,7 +334,7 @@ const diffRows = computed<DiffRow[]>(() => {
335
334
  id: 'rq-' + q.key,
336
335
  kind: 'rm',
337
336
  sign: '−',
338
- tag: 'entfernt',
337
+ tag: msg.value.diff.tagRemoved,
339
338
  label: quotaLabel(q.key),
340
339
  key: q.key,
341
340
  from: `${q.value} ${u}`.trim(),
@@ -347,8 +346,8 @@ const diffRows = computed<DiffRow[]>(() => {
347
346
  id: 'price',
348
347
  kind: 'mod',
349
348
  sign: '~',
350
- tag: 'geändert',
351
- label: 'Preis (Monat / Jahr)',
349
+ tag: msg.value.diff.tagChanged,
350
+ label: msg.value.diff.priceLabel,
352
351
  key: 'pricing',
353
352
  from: `${formatMoney(predecessor.value.monthlyNet)} / ${formatMoney(predecessor.value.yearlyNet)}`,
354
353
  to: `${formatMoney(selectedVersion.value.monthlyNet)} / ${formatMoney(selectedVersion.value.yearlyNet)}`,
@@ -375,7 +374,7 @@ function formatDate(iso: string | null | undefined): string {
375
374
  if (!iso) return '—';
376
375
  const d = new Date(iso);
377
376
  if (Number.isNaN(d.getTime())) return iso;
378
- return d.toLocaleDateString('de-DE', {
377
+ return d.toLocaleDateString(intlLocale.value, {
379
378
  day: '2-digit',
380
379
  month: '2-digit',
381
380
  year: 'numeric',
@@ -401,17 +400,16 @@ async function executeTerminate(): Promise<void> {
401
400
  const e = err as { status?: number; body?: { code?: string; message?: string } };
402
401
  const code = e?.body?.code;
403
402
  if (code === 'PLAN_TERMINATE_DATE_NOT_FUTURE') {
404
- terminateError.value = 'Das Datum muss in der Zukunft liegen.';
403
+ terminateError.value = msg.value.terminateDialog.errorDateNotFuture;
405
404
  } else if (code === 'PLAN_VERSION_NOT_LIVE') {
406
- terminateError.value =
407
- 'Diese Version ist nicht live (draft oder superseded) und kann nicht terminiert werden.';
405
+ terminateError.value = msg.value.terminateDialog.errorVersionNotLive;
408
406
  } else if (code === 'PLAN_TERMINATE_NOT_IMPLEMENTED') {
409
- terminateError.value =
410
- 'Backend unterstützt Terminate noch nicht — bitte API-Server neu bauen.';
407
+ terminateError.value = msg.value.terminateDialog.errorNotImplemented;
411
408
  } else if (e?.body?.message) {
412
409
  terminateError.value = e.body.message;
413
410
  } else {
414
- terminateError.value = err instanceof Error ? err.message : 'Terminate fehlgeschlagen.';
411
+ terminateError.value =
412
+ err instanceof Error ? err.message : msg.value.terminateDialog.errorFailed;
415
413
  }
416
414
  } finally {
417
415
  terminating.value = false;
@@ -13,7 +13,7 @@
13
13
  @keydown.escape="cancelName"
14
14
  @blur="commitName"
15
15
  />
16
- <span class="pd-title-hint">Draft · Enter speichert · Esc bricht ab</span>
16
+ <span class="pd-title-hint">{{ msg.header.nameEditHint }}</span>
17
17
  </template>
18
18
  <template v-else>
19
19
  <h1 class="pd-title">{{ plan.label }}</h1>
@@ -21,8 +21,8 @@
21
21
  v-if="draftVersion"
22
22
  class="pd-title-edit-btn"
23
23
  type="button"
24
- title="Plan-Name im Draft bearbeiten"
25
- aria-label="Plan-Name bearbeiten"
24
+ :title="msg.header.editNameTitle"
25
+ :aria-label="msg.header.editNameAria"
26
26
  @click="startEditName"
27
27
  >
28
28
  <svg
@@ -40,7 +40,7 @@
40
40
  </button>
41
41
  </template>
42
42
  </div>
43
- <div class="pd-desc">{{ plan.description || 'Keine Beschreibung.' }}</div>
43
+ <div class="pd-desc">{{ plan.description || msg.header.noDescription }}</div>
44
44
  </div>
45
45
  </div>
46
46
  <div class="pd-actions">
@@ -57,13 +57,13 @@
57
57
  <path d="M5 12h14M13 5l7 7-7 7" />
58
58
  </svg>
59
59
  </span>
60
- <span>Zurück</span>
60
+ <span>{{ common.back }}</span>
61
61
  </button>
62
62
  <button
63
63
  v-if="tenantTotal === 0 && !draftVersion && publishedCount === 0"
64
64
  class="btn danger"
65
65
  type="button"
66
- title="Plan löschen (nur ohne Mandanten und ohne published Versionen)"
66
+ :title="msg.header.deletePlanTitle"
67
67
  @click="$emit('deletePlan')"
68
68
  >
69
69
  <svg
@@ -78,7 +78,7 @@
78
78
  d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"
79
79
  />
80
80
  </svg>
81
- <span>Plan löschen</span>
81
+ <span>{{ msg.header.deletePlan }}</span>
82
82
  </button>
83
83
  <button class="btn" type="button" @click="$emit('clonePlan')">
84
84
  <svg
@@ -92,7 +92,7 @@
92
92
  <rect x="9" y="9" width="11" height="11" rx="2" />
93
93
  <path d="M5 15V5a2 2 0 012-2h10" />
94
94
  </svg>
95
- <span>Plan klonen</span>
95
+ <span>{{ msg.header.clonePlan }}</span>
96
96
  </button>
97
97
  <button
98
98
  v-if="!draftVersion"
@@ -110,7 +110,7 @@
110
110
  >
111
111
  <path d="M12 5v14M5 12h14" />
112
112
  </svg>
113
- <span>Neue Draft-Version</span>
113
+ <span>{{ msg.header.newDraftVersion }}</span>
114
114
  </button>
115
115
  <button
116
116
  v-else
@@ -128,15 +128,17 @@
128
128
  >
129
129
  <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
130
130
  </svg>
131
- <span>Draft v{{ draftVersion.version }} veröffentlichen</span>
131
+ <span>{{ publishDraftLabel }}</span>
132
132
  </button>
133
133
  </div>
134
134
  </div>
135
135
  </template>
136
136
 
137
137
  <script setup lang="ts">
138
- import { nextTick, ref, watch } from 'vue';
138
+ import { computed, nextTick, ref, watch } from 'vue';
139
139
  import type { PlanRow, PlanVersionRow } from '@saasicat/types';
140
+ import { formatMessage } from '../../client/i18n/format.js';
141
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
140
142
 
141
143
  const props = defineProps<{
142
144
  plan: PlanRow;
@@ -154,6 +156,13 @@ const emit = defineEmits<{
154
156
  (e: 'updatePlan', patch: { label: string }): void;
155
157
  }>();
156
158
 
159
+ const msg = useSaMessages('planDetail');
160
+ const common = useSaMessages('common');
161
+
162
+ const publishDraftLabel = computed(() =>
163
+ formatMessage(msg.value.header.publishDraft, { version: props.draftVersion?.version ?? '' }),
164
+ );
165
+
157
166
  const editingName = ref(false);
158
167
  const nameDraft = ref(props.plan.label);
159
168
  const nameInput = ref<HTMLInputElement | null>(null);
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="pd-kpis">
3
3
  <div class="pd-kpi">
4
- <div class="pd-kpi-label">Aktive Version</div>
4
+ <div class="pd-kpi-label">{{ msg.kpis.activeVersion }}</div>
5
5
  <div class="pd-kpi-big">
6
6
  <template v-if="liveVersion">
7
7
  v{{ liveVersion.version }}
@@ -9,49 +9,62 @@
9
9
  </template>
10
10
  <template v-else>—</template>
11
11
  </div>
12
- <div class="pd-kpi-sub">
13
- {{
14
- liveVersion?.validFrom
15
- ? `seit ${liveVersion.validFrom.slice(0, 10)}`
16
- : 'keine veröffentlichte Version'
17
- }}
18
- </div>
12
+ <div class="pd-kpi-sub">{{ activeVersionSub }}</div>
19
13
  </div>
20
14
 
21
15
  <div class="pd-kpi">
22
- <div class="pd-kpi-label">Tenant-Impact</div>
16
+ <div class="pd-kpi-label">{{ msg.kpis.tenantImpact }}</div>
23
17
  <div class="pd-kpi-big">{{ tenantTotal }}</div>
24
- <div class="pd-kpi-sub">Mandanten auf diesem Plan</div>
18
+ <div class="pd-kpi-sub">{{ msg.kpis.tenantsOnPlan }}</div>
25
19
  </div>
26
20
 
27
21
  <div class="pd-kpi">
28
- <div class="pd-kpi-label">Versionen</div>
22
+ <div class="pd-kpi-label">{{ msg.kpis.versions }}</div>
29
23
  <div class="pd-kpi-big">{{ versionCount }}</div>
30
- <div class="pd-kpi-sub">
31
- {{ publishedCount }} live/superseded{{ draftVersion ? ' · 1 Draft' : '' }}
32
- </div>
24
+ <div class="pd-kpi-sub">{{ versionsSummary }}</div>
33
25
  </div>
34
26
 
35
27
  <div :class="['pd-kpi', draftVersion ? 'draft' : '']">
36
- <div class="pd-kpi-label">Offener Draft</div>
28
+ <div class="pd-kpi-label">{{ msg.kpis.openDraft }}</div>
37
29
  <div class="pd-kpi-big" :style="draftVersion ? '' : 'color:#cbd5e1'">
38
30
  {{ draftVersion ? `v${draftVersion.version}` : '—' }}
39
31
  </div>
40
32
  <div class="pd-kpi-sub">
41
- {{ draftVersion ? 'Bereit zum Bearbeiten' : 'Keine Draft-Version offen' }}
33
+ {{ draftVersion ? msg.kpis.readyToEdit : msg.kpis.noOpenDraft }}
42
34
  </div>
43
35
  </div>
44
36
  </div>
45
37
  </template>
46
38
 
47
39
  <script setup lang="ts">
40
+ import { computed } from 'vue';
48
41
  import type { PlanVersionRow } from '@saasicat/types';
42
+ import { formatMessage } from '../../client/i18n/format.js';
43
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
49
44
 
50
- defineProps<{
45
+ const props = defineProps<{
51
46
  liveVersion: PlanVersionRow | null;
52
47
  draftVersion: PlanVersionRow | null;
53
48
  tenantTotal: number;
54
49
  versionCount: number;
55
50
  publishedCount: number;
56
51
  }>();
52
+
53
+ const msg = useSaMessages('planDetail');
54
+
55
+ const activeVersionSub = computed(() => {
56
+ const validFrom = props.liveVersion?.validFrom;
57
+ return validFrom
58
+ ? formatMessage(msg.value.kpis.activeSince, { date: validFrom.slice(0, 10) })
59
+ : msg.value.kpis.noPublishedVersion;
60
+ });
61
+
62
+ const versionsSummary = computed(() =>
63
+ props.draftVersion
64
+ ? formatMessage(msg.value.kpis.versionsSummaryWithDrafts, {
65
+ published: props.publishedCount,
66
+ drafts: 1,
67
+ })
68
+ : formatMessage(msg.value.kpis.versionsSummary, { published: props.publishedCount }),
69
+ );
57
70
  </script>
@@ -7,17 +7,20 @@
7
7
  <q-card style="min-width: 420px">
8
8
  <q-card-section>
9
9
  <div class="text-h6">
10
- {{ target?.endsAt ? 'Enddatum ändern' : 'Version terminieren' }}
10
+ {{
11
+ target?.endsAt
12
+ ? msg.terminateDialog.titleChangeEndDate
13
+ : msg.terminateDialog.titleTerminate
14
+ }}
11
15
  </div>
12
16
  <div class="text-body2 q-mt-sm">
13
- Plan <code>{{ plan.planKey }}</code> — v{{ target?.version }} wird am gewählten
14
- Datum auslaufen, <b>ohne</b> durch eine Nachfolge-Version ersetzt zu werden.
15
- Bestand-Subscriptions bleiben gebunden (Vertragsschutz P1); neue Buchungen sind
16
- ab diesem Datum nicht mehr möglich.
17
+ {{ msg.terminateDialog.bodyPlanPrefix }} <code>{{ plan.planKey }}</code>
18
+ {{ bodyExpiry }} <b>{{ msg.terminateDialog.bodyWithout }}</b>
19
+ {{ msg.terminateDialog.bodySuffix }}
17
20
  </div>
18
21
  <q-input
19
22
  :model-value="dateInput"
20
- label="Enddatum (YYYY-MM-DD)"
23
+ :label="msg.terminateDialog.dateLabel"
21
24
  type="text"
22
25
  class="q-mt-md"
23
26
  mask="####-##-##"
@@ -43,10 +46,10 @@
43
46
  {{ error }}
44
47
  </q-banner>
45
48
  <q-card-actions align="right">
46
- <q-btn flat label="Abbrechen" @click="$emit('update:modelValue', false)" />
49
+ <q-btn flat :label="common.cancel" @click="$emit('update:modelValue', false)" />
47
50
  <q-btn
48
51
  color="primary"
49
- label="Terminieren"
52
+ :label="msg.terminateDialog.submit"
50
53
  :loading="terminating"
51
54
  :disable="!dateInput"
52
55
  @click="$emit('execute')"
@@ -57,9 +60,12 @@
57
60
  </template>
58
61
 
59
62
  <script setup lang="ts">
63
+ import { computed } from 'vue';
60
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';
61
67
 
62
- defineProps<{
68
+ const props = defineProps<{
63
69
  modelValue: boolean;
64
70
  plan: PlanRow;
65
71
  target: PlanVersionRow | null;
@@ -68,6 +74,13 @@ defineProps<{
68
74
  terminating: boolean;
69
75
  }>();
70
76
 
77
+ const msg = useSaMessages('planDetail');
78
+ const common = useSaMessages('common');
79
+
80
+ const bodyExpiry = computed(() =>
81
+ formatMessage(msg.value.terminateDialog.bodyExpiry, { version: props.target?.version ?? '' }),
82
+ );
83
+
71
84
  defineEmits<{
72
85
  (e: 'update:modelValue', value: boolean): void;
73
86
  (e: 'update:dateInput', value: string): void;