@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
@@ -15,10 +15,10 @@
15
15
  :disabled="locked"
16
16
  :title="
17
17
  locked
18
- ? 'Live-Version ist read-only'
18
+ ? msg.compatPicker.lockedTooltip
19
19
  : q.quotaKey in quotas
20
- ? 'Quota entfernen'
21
- : 'Quota aufnehmen'
20
+ ? msg.quotasEditor.removeTooltip
21
+ : msg.quotasEditor.addTooltip
22
22
  "
23
23
  @click="$emit('toggle', q.quotaKey)"
24
24
  >
@@ -70,7 +70,7 @@
70
70
  </div>
71
71
  </div>
72
72
  <div v-if="availableQuotas.length === 0" class="bd-quotas-empty">
73
- Keine Quotas im Discovery-Snapshot.
73
+ {{ msg.quotasEditor.empty }}
74
74
  </div>
75
75
  </div>
76
76
  </template>
@@ -79,6 +79,7 @@
79
79
  import { computed } from 'vue';
80
80
  import type { DiscoveredQuota } from '@saasicat/types';
81
81
  import type { QuotaMeta } from './catalog-i18n.js';
82
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
82
83
 
83
84
  // BundleQuotasEditor — toggle + numeric per Quota, against the discovery
84
85
  // snapshot as a library (after plan simulation). `quotas[key]` is the
@@ -98,6 +99,8 @@ const props = defineProps<{
98
99
  quotaRegistry?: Record<string, QuotaMeta>;
99
100
  }>();
100
101
 
102
+ const msg = useSaMessages('bundles');
103
+
101
104
  defineEmits<{
102
105
  (e: 'toggle', quotaKey: string): void;
103
106
  (e: 'setValue', quotaKey: string, value: number): void;
@@ -51,35 +51,29 @@
51
51
  </span>
52
52
  <div class="bv-status-text">
53
53
  <template v-if="status === 'live'">
54
- <b>v{{ version.version }}</b> ist <b>live</b> seit
55
- {{ formatDateDE(version.validFrom) }} wird aktuell als Add-On angeboten.
56
- <span class="bv-status-warn">
57
- Inhalt &amp; Preis sind read-only (laufende Verträge). Für Änderungen eine neue
58
- Version anlegen.
59
- </span>
54
+ <b>v{{ version.version }}</b> {{ msg.statusBanner.is }}
55
+ <b>{{ msg.statusBanner.live }}</b> {{ liveTail }}
56
+ <span class="bv-status-warn">{{ msg.statusBanner.liveWarning }}</span>
60
57
  </template>
61
58
  <template v-else-if="status === 'scheduled'">
62
- <b>v{{ version.version }}</b> ist <b>geplant</b> für
63
- {{ formatDateDE(version.validFrom) }} wird ab dann verkaufbar.
64
- <span class="bv-status-ok">Frei editierbar bis dahin.</span>
59
+ <b>v{{ version.version }}</b> {{ msg.statusBanner.is }}
60
+ <b>{{ msg.statusBanner.scheduled }}</b> {{ scheduledTail }}
61
+ <span class="bv-status-ok">{{ msg.statusBanner.scheduledOk }}</span>
65
62
  </template>
66
63
  <template v-else-if="status === 'superseded'">
67
- <b>v{{ version.version }}</b> ist <b>abgelöst</b> ({{
68
- formatDateDE(version.validFrom)
69
- }}
70
- – {{ formatDateDE(version.validUntil) }}) — wird nicht mehr angeboten, Bestand
71
- bleibt für Abrechnung erhalten.
64
+ <b>v{{ version.version }}</b> {{ msg.statusBanner.is }}
65
+ <b>{{ msg.statusBanner.superseded }}</b> {{ supersededTail }}
72
66
  </template>
73
67
  <template v-else>
74
- <b>v{{ version.version }}</b> ist <b>Draft</b> — noch nicht published, frei
75
- editierbar.
68
+ <b>v{{ version.version }}</b> {{ msg.statusBanner.is }}
69
+ <b>{{ msg.statusBanner.draft }}</b> {{ msg.statusBanner.draftTail }}
76
70
  </template>
77
71
  </div>
78
72
  <button
79
73
  v-if="status === 'scheduled' || status === 'draft'"
80
74
  class="bv-status-discard"
81
75
  type="button"
82
- title="Geplante Version verwerfen"
76
+ :title="msg.statusBanner.discardTooltip"
83
77
  @click="$emit('discard')"
84
78
  >
85
79
  <svg
@@ -94,7 +88,7 @@
94
88
  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"
95
89
  />
96
90
  </svg>
97
- <span>Verwerfen</span>
91
+ <span>{{ common.discard }}</span>
98
92
  </button>
99
93
  </div>
100
94
  </template>
@@ -103,7 +97,9 @@
103
97
  import { computed } from 'vue';
104
98
  import type { BundleVersionRow } from '@saasicat/types';
105
99
 
106
- import { bundleVersionStatus, formatDateDE } from './bundle-version-status';
100
+ import { bundleVersionStatus, formatDate } from './bundle-version-status';
101
+ import { formatMessage } from '../../client/i18n/format.js';
102
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
107
103
 
108
104
  // BundleStatusBanner — inline hint per bundle version with plain-text info on
109
105
  // editability (after plan simulation). Shows the status + the business
@@ -119,7 +115,28 @@ defineEmits<{
119
115
  (e: 'discard'): void;
120
116
  }>();
121
117
 
118
+ const msg = useSaMessages('bundles');
119
+ const common = useSaMessages('common');
120
+ const { locale } = useSuperAdminI18n();
121
+
122
122
  const status = computed(() => bundleVersionStatus(props.version, props.now));
123
+
124
+ const liveTail = computed(() =>
125
+ formatMessage(msg.value.statusBanner.liveTail, {
126
+ date: formatDate(props.version.validFrom, locale.value),
127
+ }),
128
+ );
129
+ const scheduledTail = computed(() =>
130
+ formatMessage(msg.value.statusBanner.scheduledTail, {
131
+ date: formatDate(props.version.validFrom, locale.value),
132
+ }),
133
+ );
134
+ const supersededTail = computed(() =>
135
+ formatMessage(msg.value.statusBanner.supersededTail, {
136
+ from: formatDate(props.version.validFrom, locale.value),
137
+ until: formatDate(props.version.validUntil, locale.value),
138
+ }),
139
+ );
123
140
  </script>
124
141
 
125
142
  <style scoped>
@@ -5,9 +5,8 @@
5
5
  <div v-if="hasOverlap" class="bve-overlap-banner" role="alert">
6
6
  <span class="bve-overlap-ico">⚠</span>
7
7
  <span>
8
- <b>{{ overlapPlansCount }}</b> Plan{{ overlapPlansCount === 1 ? '' : 'e' }} mit
9
- Überschneidung Features/Quotas dieses Bundles sind im Plan bereits enthalten.
10
- Bitte vor Publish bereinigen.
8
+ <b>{{ overlapPlansCount }}</b>
9
+ {{ overlapPlansCount === 1 ? msg.editor.overlapOne : msg.editor.overlapMany }}
11
10
  </span>
12
11
  </div>
13
12
 
@@ -15,7 +14,7 @@
15
14
  <!-- LEFT: Features + Quotas -->
16
15
  <section class="bve-col">
17
16
  <div class="bve-section-label">
18
- <span>Features</span>
17
+ <span>{{ msg.editor.sectionFeatures }}</span>
19
18
  <span class="bve-section-count">
20
19
  {{ form.features.length }} / {{ availableFeatures.length }}
21
20
  </span>
@@ -30,7 +29,7 @@
30
29
  />
31
30
 
32
31
  <div class="bve-section-label bve-section-label--top">
33
- <span>Quotas</span>
32
+ <span>{{ msg.fields.quotas }}</span>
34
33
  <span class="bve-section-count">
35
34
  {{ Object.keys(form.quotas).length }} / {{ availableQuotas.length }}
36
35
  </span>
@@ -49,12 +48,12 @@
49
48
  <!-- RIGHT: Pricing · Validity · Compat -->
50
49
  <section class="bve-col">
51
50
  <div class="bve-section-label">
52
- <span>Pricing</span>
53
- <span class="bve-section-count">Bundle-Preis · zusätzlich zum Plan</span>
51
+ <span>{{ msg.editor.sectionPricing }}</span>
52
+ <span class="bve-section-count">{{ msg.editor.pricingHint }}</span>
54
53
  </div>
55
54
  <div class="bve-row bve-row--pricing">
56
55
  <label class="bve-field">
57
- <span class="bve-field-label">Monatspreis</span>
56
+ <span class="bve-field-label">{{ msg.fields.monthlyPrice }}</span>
58
57
  <div class="bve-input-grp">
59
58
  <input
60
59
  type="text"
@@ -64,11 +63,11 @@
64
63
  :disabled="locked"
65
64
  @input="onPriceInput('monthlyNet', $event)"
66
65
  />
67
- <span class="bve-input-unit">€/Mo</span>
66
+ <span class="bve-input-unit">{{ msg.fields.perMonthUnit }}</span>
68
67
  </div>
69
68
  </label>
70
69
  <label class="bve-field">
71
- <span class="bve-field-label">Jahrespreis</span>
70
+ <span class="bve-field-label">{{ msg.fields.yearlyPrice }}</span>
72
71
  <div class="bve-input-grp">
73
72
  <input
74
73
  type="text"
@@ -78,26 +77,26 @@
78
77
  :disabled="locked"
79
78
  @input="onPriceInput('yearlyNet', $event)"
80
79
  />
81
- <span class="bve-input-unit">€/Jahr</span>
80
+ <span class="bve-input-unit">{{ msg.fields.perYearUnit }}</span>
82
81
  </div>
83
82
  </label>
84
83
  <div class="bve-savings">
85
84
  <template v-if="savingsPercent !== null">
86
- {{ monthlyEquivalent }} €/Mo bei Jahresbuchung
85
+ {{ yearlyEquivalentText }}
87
86
  <span v-if="savingsPercent > 0" class="bve-savings-pill">
88
- {{ savingsPercent }} % sparen
87
+ {{ savingsText }}
89
88
  </span>
90
89
  </template>
91
90
  </div>
92
91
  </div>
93
92
 
94
93
  <div class="bve-section-label bve-section-label--top">
95
- <span>Gültigkeit</span>
96
- <span class="bve-section-count">validUntil automatisch (Auto-Sukzession)</span>
94
+ <span>{{ common.validity }}</span>
95
+ <span class="bve-section-count">{{ msg.editor.validityHint }}</span>
97
96
  </div>
98
97
  <div class="bve-row bve-row--validity">
99
98
  <label class="bve-field">
100
- <span class="bve-field-label">Gültig ab</span>
99
+ <span class="bve-field-label">{{ msg.fields.validFrom }}</span>
101
100
  <input
102
101
  type="date"
103
102
  class="bve-input"
@@ -107,21 +106,19 @@
107
106
  />
108
107
  </label>
109
108
  <label class="bve-field">
110
- <span class="bve-field-label">Gültig bis</span>
109
+ <span class="bve-field-label">{{ msg.fields.validUntil }}</span>
111
110
  <input
112
111
  type="text"
113
112
  class="bve-input bve-input--readonly"
114
- :value="formatDateDE(form.validUntil) || 'offen'"
113
+ :value="validUntilDisplay"
115
114
  disabled
116
115
  />
117
- <span class="bve-field-hint">
118
- wird beim Publish einer Nachfolge-Version automatisch gesetzt
119
- </span>
116
+ <span class="bve-field-hint">{{ msg.editor.validUntilHint }}</span>
120
117
  </label>
121
118
  </div>
122
119
 
123
120
  <div class="bve-section-label bve-section-label--top">
124
- <span>Marketing</span>
121
+ <span>{{ msg.editor.sectionMarketing }}</span>
125
122
  </div>
126
123
  <label class="bve-toggle-row">
127
124
  <input
@@ -130,25 +127,25 @@
130
127
  :disabled="locked"
131
128
  @change="onMarketedToggle"
132
129
  />
133
- <span class="bve-toggle-label">Im Public-Catalog vermarkten</span>
130
+ <span class="bve-toggle-label">{{ msg.editor.marketed }}</span>
134
131
  </label>
135
132
 
136
133
  <div class="bve-section-label bve-section-label--top">
137
- <span>Change-Note</span>
138
- <span class="bve-section-count">Pflicht beim Publish</span>
134
+ <span>{{ msg.editor.sectionChangeNote }}</span>
135
+ <span class="bve-section-count">{{ msg.editor.changeNoteRequired }}</span>
139
136
  </div>
140
137
  <textarea
141
138
  class="bve-textarea"
142
139
  rows="2"
143
140
  :value="form.changeNote"
144
141
  :disabled="locked"
145
- placeholder="z. B. Add WhatsApp + Preisanpassung +4 €"
142
+ :placeholder="msg.editor.changeNotePlaceholder"
146
143
  @input="onChangeNoteInput"
147
144
  />
148
145
 
149
146
  <div class="bve-section-label bve-section-label--top">
150
- <span>Kompatibel mit Plänen</span>
151
- <span class="bve-section-count">{{ form.planIds.length }} ausgewählt</span>
147
+ <span>{{ msg.fields.planCompat }}</span>
148
+ <span class="bve-section-count">{{ selectedCountText }}</span>
152
149
  </div>
153
150
  <BundlePlanCompatPicker
154
151
  :plans="plans"
@@ -172,16 +169,16 @@
172
169
  :disabled="saving || !hasChanges"
173
170
  @click="onReset"
174
171
  >
175
- Zurücksetzen
172
+ {{ common.reset }}
176
173
  </button>
177
174
  <button
178
175
  type="button"
179
176
  class="bve-btn bve-btn--primary"
180
177
  :disabled="!canSave || saving"
181
- :title="!canSave ? 'Form ist invalide oder unverändert' : 'Änderungen speichern'"
178
+ :title="!canSave ? msg.editor.saveDisabledTooltip : msg.editor.saveTooltip"
182
179
  @click="onSave"
183
180
  >
184
- {{ saving ? 'Speichern …' : 'Speichern' }}
181
+ {{ saving ? common.saving : common.save }}
185
182
  </button>
186
183
  </div>
187
184
  <div v-if="saveError" class="bve-error">
@@ -206,7 +203,9 @@ import BundlePlanCompatPicker from './BundlePlanCompatPicker.vue';
206
203
  import BundleQuotasEditor from './BundleQuotasEditor.vue';
207
204
  import BundleStatusBanner from './BundleStatusBanner.vue';
208
205
  import type { QuotaMeta } from './catalog-i18n.js';
209
- import { bundleVersionStatus, findBundlePlanOverlap, formatDateDE } from './bundle-version-status';
206
+ import { bundleVersionStatus, findBundlePlanOverlap, formatDate } from './bundle-version-status';
207
+ import { formatMessage } from '../../client/i18n/format.js';
208
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
210
209
 
211
210
  // BundleVersionInlineEditor — orchestrates the five sub-components (status
212
211
  // banner + features editor + quotas editor + pricing/validity block +
@@ -262,6 +261,10 @@ const emit = defineEmits<{
262
261
 
263
262
  const PRICE_RE = /^\d+(\.\d{1,2})?$/;
264
263
 
264
+ const msg = useSaMessages('bundles');
265
+ const common = useSaMessages('common');
266
+ const { locale } = useSuperAdminI18n();
267
+
265
268
  function buildForm(v: BundleVersionRow): Form {
266
269
  return {
267
270
  features: [...v.features],
@@ -307,20 +310,33 @@ const monthlyEquivalent = computed(() => {
307
310
  return (y / 12).toFixed(2);
308
311
  });
309
312
 
313
+ const yearlyEquivalentText = computed(() =>
314
+ formatMessage(msg.value.editor.yearlyEquivalent, { amount: monthlyEquivalent.value }),
315
+ );
316
+ const savingsText = computed(() =>
317
+ formatMessage(msg.value.editor.savings, { percent: savingsPercent.value ?? 0 }),
318
+ );
319
+ const selectedCountText = computed(() =>
320
+ formatMessage(msg.value.editor.selectedCount, { count: form.planIds.length }),
321
+ );
322
+ const validUntilDisplay = computed(
323
+ () => formatDate(form.validUntil, locale.value) || msg.value.fields.validUntilOpen,
324
+ );
325
+
310
326
  // ── Validation ─────────────────────────────────────────────
311
327
  const validFromError = computed<string | null>(() => {
312
328
  if (locked.value) return null;
313
- if (!form.validFrom) return 'Gültig ab ist Pflicht.';
329
+ if (!form.validFrom) return msg.value.validation.validFromRequired;
314
330
  return null;
315
331
  });
316
332
 
317
333
  const priceError = computed<string | null>(() => {
318
334
  if (locked.value) return null;
319
335
  if (form.monthlyNet && !PRICE_RE.test(form.monthlyNet)) {
320
- return 'Monatspreis: Dezimal mit max. 2 Nachkommastellen.';
336
+ return msg.value.validation.monthlyPriceFormat;
321
337
  }
322
338
  if (form.yearlyNet && !PRICE_RE.test(form.yearlyNet)) {
323
- return 'Jahrespreis: Dezimal mit max. 2 Nachkommastellen.';
339
+ return msg.value.validation.yearlyPriceFormat;
324
340
  }
325
341
  return null;
326
342
  });
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="bv-vstrip">
3
- <div class="bv-vstrip-label">Versionen</div>
3
+ <div class="bv-vstrip-label">{{ msg.versionStrip.label }}</div>
4
4
  <div class="bv-vstrip-tabs">
5
5
  <button
6
6
  v-for="v in sortedVersions"
@@ -8,20 +8,23 @@
8
8
  type="button"
9
9
  class="bv-vtab"
10
10
  :class="[`bv-vtab-${statusOf(v)}`, { 'bv-vtab-current': v.id === modelValue }]"
11
- :title="BUNDLE_STATUS_META[statusOf(v)].tooltip"
11
+ :title="statusMetaOf(v).tooltip"
12
12
  @click="$emit('update:modelValue', v.id)"
13
13
  >
14
14
  <span class="bv-vtab-name">v{{ v.version }}</span>
15
15
  <span :class="['bv-vtab-status', statusOf(v)]">
16
- {{ BUNDLE_STATUS_META[statusOf(v)].label }}
16
+ {{ statusMetaOf(v).label }}
17
17
  </span>
18
18
  <span class="bv-vtab-dates">
19
- {{ formatDateDE(v.validFrom) }}
20
- <template v-if="v.validUntil"> – {{ formatDateDE(v.validUntil) }}</template>
21
- <template v-else> offen</template>
19
+ {{ formatDate(v.validFrom, locale) }}
20
+ <template v-if="v.validUntil">
21
+ {{ formatDate(v.validUntil, locale) }}</template
22
+ >
23
+ <template v-else> – {{ msg.fields.validUntilOpen }}</template>
22
24
  </span>
23
25
  <span class="bv-vtab-price">
24
- {{ v.monthlyNet ?? '—' }}<template v-if="v.monthlyNet"> €</template> / Mo
26
+ {{ v.monthlyNet ?? '—' }}<template v-if="v.monthlyNet"> €</template>
27
+ {{ msg.versionStrip.perMonth }}
25
28
  </span>
26
29
  </button>
27
30
  <button
@@ -30,8 +33,8 @@
30
33
  :disabled="!canAddVersion"
31
34
  :title="
32
35
  canAddVersion
33
- ? 'Neue, zukünftige Version anlegen'
34
- : 'Bundle hat bereits eine Draft-Version — erst publishen oder verwerfen'
36
+ ? msg.versionStrip.addTooltip
37
+ : msg.versionStrip.addDisabledTooltip
35
38
  "
36
39
  @click="$emit('addVersion')"
37
40
  >
@@ -45,7 +48,7 @@
45
48
  >
46
49
  <path d="M12 5v14M5 12h14" />
47
50
  </svg>
48
- <span>Neue Version</span>
51
+ <span>{{ msg.versionStrip.addVersion }}</span>
49
52
  </button>
50
53
  </div>
51
54
  </div>
@@ -56,11 +59,13 @@ import { computed } from 'vue';
56
59
  import type { BundleVersionRow } from '@saasicat/types';
57
60
 
58
61
  import {
59
- BUNDLE_STATUS_META,
62
+ bundleStatusMeta,
60
63
  bundleVersionStatus,
61
64
  bundleVersionsSorted,
62
- formatDateDE,
65
+ formatDate,
66
+ type BundleStatusMeta,
63
67
  } from './bundle-version-status';
68
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
64
69
 
65
70
  // BundleVersionStrip — tab bar across all versions of a bundle, modeled on
66
71
  // the plan simulation (saasadminui/project/bundles.jsx → BundleVersionStrip).
@@ -82,6 +87,9 @@ defineEmits<{
82
87
  (e: 'addVersion'): void;
83
88
  }>();
84
89
 
90
+ const msg = useSaMessages('bundles');
91
+ const { locale } = useSuperAdminI18n();
92
+
85
93
  const sortedVersions = computed(() => bundleVersionsSorted(props.versions));
86
94
 
87
95
  const canAddVersion = computed(() => !props.versions.some((v) => v.publishedAt === null));
@@ -89,6 +97,10 @@ const canAddVersion = computed(() => !props.versions.some((v) => v.publishedAt =
89
97
  function statusOf(v: BundleVersionRow) {
90
98
  return bundleVersionStatus(v, props.now);
91
99
  }
100
+
101
+ function statusMetaOf(v: BundleVersionRow): BundleStatusMeta {
102
+ return bundleStatusMeta(statusOf(v), locale.value);
103
+ }
92
104
  </script>
93
105
 
94
106
  <style scoped>
@@ -6,6 +6,9 @@
6
6
 
7
7
  import type { BundleVersionRow, PlanVersionRow } from '@saasicat/types';
8
8
 
9
+ import { SA_INTL_LOCALES, type SaLocale } from '../../client/i18n/locale.js';
10
+ import { bundlesMessages } from '../../client/i18n/messages/bundles.js';
11
+
9
12
  /** UI lifecycle status of a single BundleVersion. */
10
13
  export type BundleVersionUiStatus = 'draft' | 'live' | 'scheduled' | 'superseded';
11
14
 
@@ -51,34 +54,23 @@ export interface BundleStatusMeta {
51
54
  tooltip: string;
52
55
  }
53
56
 
54
- export const BUNDLE_STATUS_META: Record<BundleAggregateStatus, BundleStatusMeta> = {
55
- draft: {
56
- label: 'Draft',
57
- cls: 'draft',
58
- tooltip: 'Noch nicht veröffentlicht — frei editierbar',
59
- },
60
- live: {
61
- label: 'Live',
62
- cls: 'live',
63
- tooltip: 'Aktive Version · verkaufbar · read-only (laufende Verträge)',
64
- },
65
- scheduled: {
66
- label: 'Geplant',
67
- cls: 'scheduled',
68
- tooltip: 'Zukünftig aktiv · noch nicht verkaufbar · frei editierbar',
69
- },
70
- superseded: {
71
- label: 'Abgelöst',
72
- cls: 'supersed',
73
- tooltip: 'Durch neue Version ersetzt · Bestand bleibt',
74
- },
75
- retired: {
76
- label: 'Retired',
77
- cls: 'supersed',
78
- tooltip: 'Bundle-Stamm wurde soft-deleted',
79
- },
57
+ const BUNDLE_STATUS_CLASS: Record<BundleAggregateStatus, BundleStatusMeta['cls']> = {
58
+ draft: 'draft',
59
+ live: 'live',
60
+ scheduled: 'scheduled',
61
+ superseded: 'supersed',
62
+ retired: 'supersed',
80
63
  };
81
64
 
65
+ /** Label, chip class and tooltip for a status in the given UI locale. */
66
+ export function bundleStatusMeta(
67
+ status: BundleAggregateStatus,
68
+ locale: SaLocale,
69
+ ): BundleStatusMeta {
70
+ const texts = bundlesMessages[locale].status[status];
71
+ return { label: texts.label, cls: BUNDLE_STATUS_CLASS[status], tooltip: texts.tooltip };
72
+ }
73
+
82
74
  /**
83
75
  * Sorts the versions of a bundle line ascending by
84
76
  * `validFrom` (drafts without validFrom go to the end).
@@ -154,11 +146,19 @@ export function findBundlePlanOverlap(
154
146
  return { features, quotas, hasAny: features.length > 0 || quotas.length > 0 };
155
147
  }
156
148
 
157
- /** ISO `YYYY-MM-DD` → `DD.MM.YYYY` for UI display; null/empty → '—'. */
158
- export function formatDateDE(iso: string | null | undefined): string {
149
+ /**
150
+ * ISO `YYYY-MM-DD` numeric date in the UI locale; null/empty → '—'.
151
+ * Formatted in UTC so a calendar day never shifts across time zones.
152
+ */
153
+ export function formatDate(iso: string | null | undefined, locale: SaLocale): string {
159
154
  if (!iso) return '—';
160
155
  const day = iso.slice(0, 10);
161
156
  const [y, m, d] = day.split('-');
162
157
  if (!y || !m || !d) return iso;
163
- return `${d}.${m}.${y}`;
158
+ return new Date(`${day}T00:00:00Z`).toLocaleDateString(SA_INTL_LOCALES[locale], {
159
+ day: '2-digit',
160
+ month: '2-digit',
161
+ year: 'numeric',
162
+ timeZone: 'UTC',
163
+ });
164
164
  }