@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
@@ -1,16 +1,21 @@
1
1
  <template>
2
- <div v-if="!changes || changes.length === 0" class="diff-empty">
3
- Keine Änderungen gegenüber der Vorgängerversion.
2
+ <div v-if="!changes || changes.length === 0" class="sa-diff-preview__empty">
3
+ {{ msg.diffPreview.empty }}
4
4
  </div>
5
- <div v-else class="diff-list">
6
- <div v-for="(c, idx) in changes" :key="idx" class="diff-row" :class="rowClass(c.direction)">
7
- <div class="diff-direction">
5
+ <div v-else class="sa-diff-preview__list">
6
+ <div
7
+ v-for="(c, idx) in changes"
8
+ :key="idx"
9
+ class="sa-diff-preview__row"
10
+ :class="rowClass(c.direction)"
11
+ >
12
+ <div class="sa-diff-preview__direction">
8
13
  <q-icon :name="iconFor(c.direction)" size="16px" />
9
14
  <span>{{ labelFor(c.direction) }}</span>
10
15
  </div>
11
- <div class="diff-field">{{ humanFieldLabel(c.field) }}</div>
12
- <div class="diff-values">
13
- <span class="old" v-if="c.field !== 'features.added'">{{
16
+ <div class="sa-diff-preview__field">{{ humanFieldLabel(c.field) }}</div>
17
+ <div class="sa-diff-preview__values">
18
+ <span v-if="c.field !== 'features.added'" class="old">{{
14
19
  formatValue(c.oldValue)
15
20
  }}</span>
16
21
  <q-icon
@@ -18,7 +23,7 @@
18
23
  name="arrow_forward"
19
24
  size="14px"
20
25
  />
21
- <span class="new" v-if="c.field !== 'features.removed'">{{
26
+ <span v-if="c.field !== 'features.removed'" class="new">{{
22
27
  formatValue(c.newValue)
23
28
  }}</span>
24
29
  </div>
@@ -27,27 +32,43 @@
27
32
  </template>
28
33
 
29
34
  <script setup lang="ts">
35
+ import { computed } from 'vue';
30
36
  import type { VersionChange } from '@saasicat/types';
37
+ import { formatCurrency } from '../client/i18n/currency.js';
38
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
31
39
 
32
- // Platform default field labels (plan-catalog quotas + pricing).
33
- // Consumer apps can add or override app-specific labels
34
- // (e.g. maxVehicles or maxMembers) via the `fieldLabels` prop.
35
- const PLATFORM_FIELD_LABELS: Readonly<Record<string, string>> = Object.freeze({
36
- 'features.added': 'Hinzugefügte Features',
37
- 'features.removed': 'Entfernte Features',
38
- maxUsers: 'Max. Benutzer',
39
- maxStorageGb: 'Speicher (GB)',
40
- monthlyNet: 'Preis monatlich (netto)',
41
- yearlyNet: 'Preis jährlich (netto)',
42
- unitSize: 'Einheitsgröße',
43
- });
40
+ // Renders a plan-version diff as one row per changed field. Used by the
41
+ // plan-versions page and available to consumer apps through the
42
+ // `./components/*` subpath export.
44
43
 
45
44
  const props = defineProps<{
46
45
  changes?: VersionChange[] | null;
47
- /** App-specific field labels; merged with the platform defaults. */
46
+ /**
47
+ * Optional: field labels (e.g. `{ maxUsers: 'Max. Benutzer' }`).
48
+ * Apps map their own quota fields here; the default covers the generic
49
+ * platform fields.
50
+ */
48
51
  fieldLabels?: Record<string, string>;
49
52
  }>();
50
53
 
54
+ const msg = useSaMessages('planVersions');
55
+ const { locale } = useSuperAdminI18n();
56
+
57
+ const defaultFieldLabels = computed<Record<string, string>>(() => ({
58
+ 'features.added': msg.value.diffFields.featuresAdded,
59
+ 'features.removed': msg.value.diffFields.featuresRemoved,
60
+ maxUsers: msg.value.diffFields.maxUsers,
61
+ maxStorageGb: msg.value.diffFields.maxStorageGb,
62
+ monthlyNet: msg.value.diffFields.monthlyNet,
63
+ yearlyNet: msg.value.diffFields.yearlyNet,
64
+ unitSize: msg.value.diffFields.unitSize,
65
+ }));
66
+
67
+ const mergedFieldLabels = computed(() => ({
68
+ ...defaultFieldLabels.value,
69
+ ...(props.fieldLabels ?? {}),
70
+ }));
71
+
51
72
  function iconFor(direction: VersionChange['direction']): string {
52
73
  if (direction === 'IMPROVEMENT') return 'trending_up';
53
74
  if (direction === 'REGRESSION') return 'trending_down';
@@ -55,36 +76,39 @@ function iconFor(direction: VersionChange['direction']): string {
55
76
  }
56
77
 
57
78
  function labelFor(direction: VersionChange['direction']): string {
58
- if (direction === 'IMPROVEMENT') return 'Verbesserung';
59
- if (direction === 'REGRESSION') return 'Verschlechterung';
60
- return 'Neutral';
79
+ if (direction === 'IMPROVEMENT') return msg.value.diffPreview.improvement;
80
+ if (direction === 'REGRESSION') return msg.value.diffPreview.regression;
81
+ return msg.value.diffPreview.neutral;
61
82
  }
62
83
 
63
84
  function rowClass(direction: VersionChange['direction']): string {
64
- if (direction === 'IMPROVEMENT') return 'diff-row--improvement';
65
- if (direction === 'REGRESSION') return 'diff-row--regression';
66
- return 'diff-row--neutral';
85
+ if (direction === 'IMPROVEMENT') return 'sa-diff-preview__row--improvement';
86
+ if (direction === 'REGRESSION') return 'sa-diff-preview__row--regression';
87
+ return 'sa-diff-preview__row--neutral';
67
88
  }
68
89
 
69
90
  function humanFieldLabel(field: string): string {
70
- return props.fieldLabels?.[field] ?? PLATFORM_FIELD_LABELS[field] ?? field;
91
+ return mergedFieldLabels.value[field] ?? field;
71
92
  }
72
93
 
94
+ /** Decimal strings the API sends for money fields (`"29.90"`). */
95
+ const MONEY_STRING = /^\d+\.\d{2}$/;
96
+
73
97
  function formatValue(value: unknown): string {
74
98
  if (value === null || value === undefined) return '–';
75
99
  if (Array.isArray(value)) {
76
100
  if (value.length === 0) return '–';
77
101
  return value.join(', ');
78
102
  }
79
- if (typeof value === 'string' && /^\d+\.\d{2}$/.test(value)) {
80
- return `${value} €`;
103
+ if (typeof value === 'string' && MONEY_STRING.test(value)) {
104
+ return formatCurrency(value, locale.value);
81
105
  }
82
106
  return String(value);
83
107
  }
84
108
  </script>
85
109
 
86
110
  <style scoped>
87
- .diff-empty {
111
+ .sa-diff-preview__empty {
88
112
  color: #64748b;
89
113
  font-style: italic;
90
114
  padding: 12px;
@@ -92,12 +116,12 @@ function formatValue(value: unknown): string {
92
116
  border-radius: 8px;
93
117
  border: 1px dashed #cbd5e1;
94
118
  }
95
- .diff-list {
119
+ .sa-diff-preview__list {
96
120
  display: flex;
97
121
  flex-direction: column;
98
122
  gap: 6px;
99
123
  }
100
- .diff-row {
124
+ .sa-diff-preview__row {
101
125
  display: grid;
102
126
  grid-template-columns: 160px 200px 1fr;
103
127
  gap: 12px;
@@ -107,22 +131,22 @@ function formatValue(value: unknown): string {
107
131
  font-size: 13px;
108
132
  align-items: center;
109
133
  }
110
- .diff-row--improvement {
134
+ .sa-diff-preview__row--improvement {
111
135
  background: #f0fdf4;
112
136
  border-color: #bbf7d0;
113
137
  color: #166534;
114
138
  }
115
- .diff-row--regression {
139
+ .sa-diff-preview__row--regression {
116
140
  background: #fef2f2;
117
141
  border-color: #fecaca;
118
142
  color: #991b1b;
119
143
  }
120
- .diff-row--neutral {
144
+ .sa-diff-preview__row--neutral {
121
145
  background: #f8fafc;
122
146
  border-color: #e2e8f0;
123
147
  color: #475569;
124
148
  }
125
- .diff-direction {
149
+ .sa-diff-preview__direction {
126
150
  display: flex;
127
151
  align-items: center;
128
152
  gap: 6px;
@@ -131,20 +155,25 @@ function formatValue(value: unknown): string {
131
155
  text-transform: uppercase;
132
156
  letter-spacing: 0.04em;
133
157
  }
134
- .diff-field {
158
+ .sa-diff-preview__field {
135
159
  color: #0f172a;
136
160
  font-weight: 500;
137
161
  }
138
- .diff-values {
162
+ .sa-diff-preview__values {
139
163
  display: flex;
140
164
  align-items: center;
141
165
  gap: 8px;
142
166
  }
143
- .diff-values .old {
167
+ .sa-diff-preview__values .old {
168
+ color: #94a3b8;
144
169
  text-decoration: line-through;
145
- opacity: 0.7;
170
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
171
+ font-size: 12px;
146
172
  }
147
- .diff-values .new {
173
+ .sa-diff-preview__values .new {
174
+ color: inherit;
148
175
  font-weight: 600;
176
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
177
+ font-size: 12px;
149
178
  }
150
179
  </style>
@@ -2,13 +2,10 @@
2
2
  <section class="bcp">
3
3
  <div class="bcp-head">
4
4
  <div>
5
- <div class="bcp-title">Neues Bundle anlegen</div>
6
- <div class="bcp-sub">
7
- Features &amp; Quotas zu einem Add-On bündeln — bepreist und kompatibel mit
8
- ausgewählten Plänen.
9
- </div>
5
+ <div class="bcp-title">{{ msg.create.title }}</div>
6
+ <div class="bcp-sub">{{ msg.create.subtitle }}</div>
10
7
  </div>
11
- <button class="bcp-close" type="button" aria-label="Schließen" @click="close">
8
+ <button class="bcp-close" type="button" :aria-label="common.close" @click="close">
12
9
  <svg
13
10
  width="14"
14
11
  height="14"
@@ -28,26 +25,24 @@
28
25
  <div class="bcp-section-head">
29
26
  <span class="bcp-section-num">1</span>
30
27
  <div>
31
- <div class="bcp-section-title">Stammdaten</div>
32
- <div class="bcp-section-sub">Sichtbarer Name + technischer Key.</div>
28
+ <div class="bcp-section-title">{{ msg.fields.masterData }}</div>
29
+ <div class="bcp-section-sub">{{ msg.create.masterDataHint }}</div>
33
30
  </div>
34
31
  </div>
35
32
  <div class="bcp-grid">
36
33
  <label class="bcp-field bcp-col-2">
37
- <span class="bcp-field-label">Label</span>
34
+ <span class="bcp-field-label">{{ msg.fields.label }}</span>
38
35
  <input
39
36
  ref="labelInput"
40
37
  v-model="form.label"
41
38
  class="bcp-input"
42
- placeholder="z. B. Communication Pro"
39
+ :placeholder="msg.create.labelPlaceholder"
43
40
  />
44
41
  </label>
45
42
  <label class="bcp-field bcp-col-2">
46
43
  <span class="bcp-field-label">
47
- Bundle-Key
48
- <span class="bcp-field-hint">
49
- API-stabil · wird aus dem Label erzeugt
50
- </span>
44
+ {{ msg.create.bundleKey }}
45
+ <span class="bcp-field-hint">{{ msg.create.bundleKeyHint }}</span>
51
46
  </span>
52
47
  <input
53
48
  v-model="form.bundleKey"
@@ -60,12 +55,12 @@
60
55
  </span>
61
56
  </label>
62
57
  <label class="bcp-field bcp-col-2">
63
- <span class="bcp-field-label">Beschreibung</span>
58
+ <span class="bcp-field-label">{{ common.description }}</span>
64
59
  <textarea
65
60
  v-model="form.description"
66
61
  rows="2"
67
62
  class="bcp-input bcp-textarea"
68
- placeholder="z. B. Kampagnen, WhatsApp und Korrespondenz für aktive Vereine."
63
+ :placeholder="msg.create.descriptionPlaceholder"
69
64
  />
70
65
  </label>
71
66
  </div>
@@ -76,15 +71,13 @@
76
71
  <div class="bcp-section-head">
77
72
  <span class="bcp-section-num">2</span>
78
73
  <div>
79
- <div class="bcp-section-title">v1 · Pricing &amp; Gültigkeit</div>
80
- <div class="bcp-section-sub">
81
- Monats- &amp; Jahrespreis sowie Datum, ab dem das Bundle verkaufbar wird.
82
- </div>
74
+ <div class="bcp-section-title">{{ msg.create.sectionPricing }}</div>
75
+ <div class="bcp-section-sub">{{ msg.create.pricingHint }}</div>
83
76
  </div>
84
77
  </div>
85
78
  <div class="bcp-grid">
86
79
  <label class="bcp-field">
87
- <span class="bcp-field-label">Monatspreis</span>
80
+ <span class="bcp-field-label">{{ msg.fields.monthlyPrice }}</span>
88
81
  <div class="bcp-input-grp">
89
82
  <input
90
83
  v-model="form.monthlyNet"
@@ -93,11 +86,11 @@
93
86
  class="bcp-input"
94
87
  placeholder="9.90"
95
88
  />
96
- <span class="bcp-input-unit">€/Mo</span>
89
+ <span class="bcp-input-unit">{{ msg.fields.perMonthUnit }}</span>
97
90
  </div>
98
91
  </label>
99
92
  <label class="bcp-field">
100
- <span class="bcp-field-label">Jahrespreis</span>
93
+ <span class="bcp-field-label">{{ msg.fields.yearlyPrice }}</span>
101
94
  <div class="bcp-input-grp">
102
95
  <input
103
96
  v-model="form.yearlyNet"
@@ -106,23 +99,13 @@
106
99
  class="bcp-input"
107
100
  placeholder="99.00"
108
101
  />
109
- <span class="bcp-input-unit">€/Jahr</span>
102
+ <span class="bcp-input-unit">{{ msg.fields.perYearUnit }}</span>
110
103
  </div>
111
104
  </label>
112
105
  <label class="bcp-field bcp-col-2">
113
- <span class="bcp-field-label">Gültig ab</span>
106
+ <span class="bcp-field-label">{{ msg.fields.validFrom }}</span>
114
107
  <input v-model="form.validFrom" type="date" class="bcp-input" />
115
- <span class="bcp-field-hint">
116
- <template v-if="form.validFrom && form.validFrom <= todayIso">
117
- ✓ Bundle ist nach Anlage sofort live und verkaufbar.
118
- </template>
119
- <template v-else-if="form.validFrom">
120
- ⏳ Geplant — wird ab {{ formatDateDE(form.validFrom) }} verkaufbar.
121
- </template>
122
- <template v-else>
123
- Pflicht beim Publish — kann auch nach Anlage gesetzt werden.
124
- </template>
125
- </span>
108
+ <span class="bcp-field-hint">{{ validFromHint }}</span>
126
109
  </label>
127
110
  </div>
128
111
  </section>
@@ -132,11 +115,8 @@
132
115
  <div class="bcp-section-head">
133
116
  <span class="bcp-section-num">3</span>
134
117
  <div>
135
- <div class="bcp-section-title">Kompatibel mit Plänen</div>
136
- <div class="bcp-section-sub">
137
- Mit welchen Plänen darf dieses Bundle als Add-On gebucht werden?
138
- Überschneidungen werden direkt markiert.
139
- </div>
118
+ <div class="bcp-section-title">{{ msg.fields.planCompat }}</div>
119
+ <div class="bcp-section-sub">{{ msg.create.planCompatHint }}</div>
140
120
  </div>
141
121
  </div>
142
122
  <BundlePlanCompatPicker
@@ -157,10 +137,8 @@
157
137
  <div class="bcp-section-head">
158
138
  <span class="bcp-section-num">4</span>
159
139
  <div>
160
- <div class="bcp-section-title">Features im Bundle</div>
161
- <div class="bcp-section-sub">
162
- {{ form.features.length }} von {{ availableFeatures.length }} ausgewählt
163
- </div>
140
+ <div class="bcp-section-title">{{ msg.create.sectionFeatures }}</div>
141
+ <div class="bcp-section-sub">{{ featureSelectionText }}</div>
164
142
  </div>
165
143
  </div>
166
144
  <BundleFeaturesEditor
@@ -178,12 +156,12 @@
178
156
  <span class="bcp-section-num">5</span>
179
157
  <div>
180
158
  <div class="bcp-section-title">
181
- Quotas
182
- <span class="bcp-field-hint" style="margin-left: 6px">optional</span>
183
- </div>
184
- <div class="bcp-section-sub">
185
- Quotas, die das Bundle on top des Plans setzt.
159
+ {{ msg.fields.quotas }}
160
+ <span class="bcp-field-hint" style="margin-left: 6px">
161
+ {{ msg.create.quotasOptional }}
162
+ </span>
186
163
  </div>
164
+ <div class="bcp-section-sub">{{ msg.create.quotasHint }}</div>
187
165
  </div>
188
166
  </div>
189
167
  <BundleQuotasEditor
@@ -201,10 +179,7 @@
201
179
 
202
180
  <div class="bcp-foot">
203
181
  <span v-if="overlapPlansCount > 0" class="bcp-foot-hint bcp-foot-hint--warn">
204
- ⚠ Überschneidung mit {{ overlapPlansCount }} Plan{{
205
- overlapPlansCount === 1 ? '' : 'en'
206
- }}
207
- — bitte prüfen
182
+ {{ overlapWarningText }}
208
183
  </span>
209
184
  <span v-else-if="form.features.length > 0" class="bcp-foot-hint">
210
185
  <svg
@@ -218,25 +193,33 @@
218
193
  <path d="M5 13l4 4L19 7" />
219
194
  </svg>
220
195
  <span>
221
- <b>{{ form.features.length }}</b> Feature{{
222
- form.features.length === 1 ? '' : 's'
196
+ <b>{{ form.features.length }}</b>
197
+ {{
198
+ form.features.length === 1
199
+ ? msg.create.summaryFeatureOne
200
+ : msg.create.summaryFeatureMany
223
201
  }}
224
202
  <template v-if="quotaCount > 0">
225
- · <b>{{ quotaCount }}</b> Quota{{ quotaCount === 1 ? '' : 's' }}
203
+ · <b>{{ quotaCount }}</b>
204
+ {{
205
+ quotaCount === 1
206
+ ? msg.create.summaryQuotaOne
207
+ : msg.create.summaryQuotaMany
208
+ }}
226
209
  </template>
227
210
  <template v-if="form.planIds.length > 0">
228
- · <b>{{ form.planIds.length }}</b> Plan-Kompat.
211
+ · <b>{{ form.planIds.length }}</b> {{ msg.create.summaryPlanCompat }}
229
212
  </template>
230
213
  </span>
231
214
  </span>
232
- <button class="bcp-btn" type="button" @click="close">Abbrechen</button>
215
+ <button class="bcp-btn" type="button" @click="close">{{ common.cancel }}</button>
233
216
  <button
234
217
  class="bcp-btn bcp-btn--primary"
235
218
  type="button"
236
219
  :disabled="!canSubmit || submitting"
237
220
  @click="submit"
238
221
  >
239
- {{ submitting ? 'Lege an …' : 'Bundle anlegen' }}
222
+ {{ submitting ? msg.create.submitting : msg.create.submit }}
240
223
  </button>
241
224
  </div>
242
225
  </section>
@@ -259,7 +242,9 @@ import BundleFeaturesEditor, { type FeatureMeta } from './BundleFeaturesEditor.v
259
242
  import BundlePlanCompatPicker from './BundlePlanCompatPicker.vue';
260
243
  import BundleQuotasEditor from './BundleQuotasEditor.vue';
261
244
  import type { QuotaMeta } from './catalog-i18n.js';
262
- import { findBundlePlanOverlap, formatDateDE } from './bundle-version-status';
245
+ import { findBundlePlanOverlap, formatDate } from './bundle-version-status';
246
+ import { formatMessage } from '../../client/i18n/format.js';
247
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
263
248
 
264
249
  // BundleCreatePanel — expandable inline panel for creating a new
265
250
  // bundle (root + v1 draft) at the top of the bundle list. Uses the same
@@ -306,8 +291,20 @@ const emit = defineEmits<{
306
291
  (e: 'created', bundle: BundleRow): void;
307
292
  }>();
308
293
 
294
+ const msg = useSaMessages('bundles');
295
+ const common = useSaMessages('common');
296
+ const { locale } = useSuperAdminI18n();
297
+
309
298
  const todayIso = computed(() => new Date().toISOString().slice(0, 10));
310
299
 
300
+ const validFromHint = computed(() => {
301
+ if (!form.validFrom) return msg.value.create.validFromHint;
302
+ if (form.validFrom <= todayIso.value) return msg.value.create.validFromImmediate;
303
+ return formatMessage(msg.value.create.validFromScheduled, {
304
+ date: formatDate(form.validFrom, locale.value),
305
+ });
306
+ });
307
+
311
308
  const BUNDLE_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
312
309
  const PRICE_RE = /^\d+(\.\d{1,2})?$/;
313
310
 
@@ -362,14 +359,21 @@ function onBundleKeyInput(event: Event): void {
362
359
  const bundleKeyError = computed<string | null>(() => {
363
360
  if (!form.bundleKey) return null;
364
361
  if (!BUNDLE_KEY_PATTERN.test(form.bundleKey)) {
365
- return 'Nur A-Z, 0-9, Underscore; muss mit Buchstabe beginnen.';
362
+ return msg.value.create.errorKeyFormat;
366
363
  }
367
364
  if (props.existingBundleKeys.includes(form.bundleKey)) {
368
- return 'Dieser Bundle-Key existiert bereits.';
365
+ return msg.value.create.errorKeyExists;
369
366
  }
370
367
  return null;
371
368
  });
372
369
 
370
+ const featureSelectionText = computed(() =>
371
+ formatMessage(msg.value.create.selectedOfTotal, {
372
+ selected: form.features.length,
373
+ total: props.availableFeatures.length,
374
+ }),
375
+ );
376
+
373
377
  const canSubmit = computed(() => {
374
378
  if (!form.label.trim()) return false;
375
379
  if (!form.bundleKey) return false;
@@ -409,6 +413,15 @@ const overlapPlansCount = computed(
409
413
  ).length,
410
414
  );
411
415
 
416
+ const overlapWarningText = computed(() =>
417
+ formatMessage(
418
+ overlapPlansCount.value === 1
419
+ ? msg.value.create.overlapWarningOne
420
+ : msg.value.create.overlapWarningMany,
421
+ { count: overlapPlansCount.value },
422
+ ),
423
+ );
424
+
412
425
  const quotaCount = computed(() => Object.keys(form.quotas).length);
413
426
 
414
427
  // ── Event handlers ────────────────────────────────────────
@@ -48,7 +48,7 @@
48
48
  </div>
49
49
  </template>
50
50
  <div v-if="groupedFeatures.length === 0" class="bd-features-empty">
51
- Keine Features im Discovery-Snapshot.
51
+ {{ msg.featuresEditor.empty }}
52
52
  </div>
53
53
  </div>
54
54
  </template>
@@ -56,6 +56,7 @@
56
56
  <script setup lang="ts">
57
57
  import { computed } from 'vue';
58
58
  import type { DiscoveredFeature } from '@saasicat/types';
59
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
59
60
 
60
61
  // BundleFeaturesEditor — grouped pills for the feature selection of a
61
62
  // bundle version (after plan simulation). The source of truth for the
@@ -88,6 +89,10 @@ const props = defineProps<{
88
89
  overlapKeys?: string[];
89
90
  }>();
90
91
 
92
+ const msg = useSaMessages('bundles');
93
+ const common = useSaMessages('common');
94
+ const { intlLocale } = useSuperAdminI18n();
95
+
91
96
  const emit = defineEmits<{
92
97
  (e: 'toggle', featureKey: string): void;
93
98
  }>();
@@ -97,15 +102,17 @@ function featureLabel(key: string): string {
97
102
  }
98
103
 
99
104
  function featureGroup(key: string): string {
100
- return props.featureRegistry?.[key]?.group ?? 'Allgemein';
105
+ return props.featureRegistry?.[key]?.group ?? common.value.general;
101
106
  }
102
107
 
103
108
  function pillTitle(key: string): string {
104
- if (props.locked) return 'Live-Version ist read-only';
109
+ if (props.locked) return msg.value.compatPicker.lockedTooltip;
105
110
  if (props.overlapKeys?.includes(key)) {
106
- return 'Feature ist im kompatiblen Plan bereits enthalten — Doppel-Berechnung';
111
+ return msg.value.featuresEditor.overlapTooltip;
107
112
  }
108
- return props.features.includes(key) ? 'Aus Bundle entfernen' : 'In Bundle aufnehmen';
113
+ return props.features.includes(key)
114
+ ? msg.value.featuresEditor.removeTooltip
115
+ : msg.value.featuresEditor.addTooltip;
109
116
  }
110
117
 
111
118
  const overlapKeys = computed(() => props.overlapKeys ?? []);
@@ -119,7 +126,7 @@ const groupedFeatures = computed(() => {
119
126
  byGroup.set(g, list);
120
127
  }
121
128
  return [...byGroup.entries()]
122
- .sort(([a], [b]) => a.localeCompare(b, 'de-DE'))
129
+ .sort(([a], [b]) => a.localeCompare(b, intlLocale.value))
123
130
  .map(([key, rows]) => ({ key, label: key, rows }));
124
131
  });
125
132
 
@@ -1,9 +1,6 @@
1
1
  <template>
2
2
  <div class="bv-compat">
3
- <div class="bv-compat-hint">
4
- Pläne, mit denen dieses Bundle als Add-On gebucht werden kann. Features/Quotas, die der
5
- Plan bereits enthält, werden als Überschneidung markiert (Doppel-Berechnung).
6
- </div>
3
+ <div class="bv-compat-hint">{{ msg.compatPicker.hint }}</div>
7
4
  <div class="bv-compat-grid">
8
5
  <button
9
6
  v-for="entry in entries"
@@ -17,10 +14,10 @@
17
14
  :disabled="locked"
18
15
  :title="
19
16
  locked
20
- ? 'Live-Version ist read-only'
17
+ ? msg.compatPicker.lockedTooltip
21
18
  : selectedKeys.includes(entry.plan.planKey)
22
- ? 'Plan-Kompatibilität entfernen'
23
- : 'Plan-Kompatibilität setzen'
19
+ ? msg.compatPicker.removeTooltip
20
+ : msg.compatPicker.addTooltip
24
21
  "
25
22
  @click="onToggle(entry.plan.planKey)"
26
23
  >
@@ -48,9 +45,11 @@
48
45
  </span>
49
46
  </div>
50
47
  <div v-if="showOverlap(entry)" class="bv-compat-overlap">
51
- <div class="bv-compat-overlap-head">⚠ Überschneidung</div>
48
+ <div class="bv-compat-overlap-head">{{ msg.compatPicker.overlapHead }}</div>
52
49
  <div v-if="entry.overlap.features.length > 0" class="bv-compat-overlap-list">
53
- <span class="bv-compat-overlap-kind">Features:</span>
50
+ <span class="bv-compat-overlap-kind">
51
+ {{ msg.compatPicker.overlapFeatures }}
52
+ </span>
54
53
  <span
55
54
  v-for="fk in entry.overlap.features"
56
55
  :key="fk"
@@ -60,7 +59,9 @@
60
59
  </span>
61
60
  </div>
62
61
  <div v-if="entry.overlap.quotas.length > 0" class="bv-compat-overlap-list">
63
- <span class="bv-compat-overlap-kind">Quotas:</span>
62
+ <span class="bv-compat-overlap-kind">
63
+ {{ msg.compatPicker.overlapQuotas }}
64
+ </span>
64
65
  <span
65
66
  v-for="qk in entry.overlap.quotas"
66
67
  :key="qk"
@@ -72,15 +73,16 @@
72
73
  </div>
73
74
  </button>
74
75
  <div v-if="plans.length === 0" class="bv-compat-empty">
75
- Keine Pläne vorhanden — der Plan-Stamm muss zuerst angelegt werden.
76
+ {{ msg.compatPicker.empty }}
76
77
  </div>
77
78
  </div>
78
79
  <div v-if="overlapCount > 0" class="bv-compat-summary">
79
80
  <span class="bv-compat-summary-ico">⚠</span>
80
81
  <span>
81
- <b>{{ overlapCount }}</b> Plan{{ overlapCount === 1 ? '' : 'e' }} mit Überschneidung
82
- — Features/Quotas dieses Bundles sind im Plan bereits enthalten. Vor Publish
83
- entweder das Bundle oder den Plan bereinigen.
82
+ <b>{{ overlapCount }}</b>
83
+ {{
84
+ overlapCount === 1 ? msg.compatPicker.summaryOne : msg.compatPicker.summaryMany
85
+ }}
84
86
  </span>
85
87
  </div>
86
88
  </div>
@@ -94,6 +96,7 @@ import { findBundlePlanOverlap, type BundlePlanOverlap } from './bundle-version-
94
96
 
95
97
  import type { FeatureMeta } from './BundleFeaturesEditor.vue';
96
98
  import type { QuotaMeta } from './catalog-i18n.js';
99
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
97
100
 
98
101
  // BundlePlanCompatPicker — multi-select of the plans this bundle may be
99
102
  // booked with as an add-on (plan-bundle visibility, after plan
@@ -132,6 +135,8 @@ const emit = defineEmits<{
132
135
  (e: 'toggle', planKey: string): void;
133
136
  }>();
134
137
 
138
+ const msg = useSaMessages('bundles');
139
+
135
140
  const entries = computed<CompatEntry[]>(() =>
136
141
  props.plans.map((plan) => ({
137
142
  plan,