@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
@@ -2,14 +2,9 @@
2
2
  <section class="pc-card">
3
3
  <div class="pc-card-head">
4
4
  <div class="pc-card-head-text">
5
- <div class="pc-card-title">
6
- <template v-if="diffPair">
7
- Diff v{{ diffPair.from.version }} → v{{ diffPair.to.version }}
8
- </template>
9
- <template v-else>Diff</template>
10
- </div>
5
+ <div class="pc-card-title">{{ panelTitle }}</div>
11
6
  <div class="pc-card-sub">
12
- {{ diffPair ? 'Was sich beim Publish ändert' : 'Kein Vergleich verfügbar' }}
7
+ {{ diffPair ? msg.diff.subOnPublish : msg.diff.noComparison }}
13
8
  </div>
14
9
  </div>
15
10
  <div v-if="diffPair" class="pc-pillrow">
@@ -74,18 +69,32 @@
74
69
  </span>
75
70
  </div>
76
71
  <div v-if="diffRows.length === 0" class="pc-empty pc-empty--inline">
77
- Keine Änderungen erkannt.
72
+ {{ msg.diff.noChangesDetected }}
78
73
  </div>
79
74
  </div>
80
75
  </section>
81
76
  </template>
82
77
 
83
78
  <script setup lang="ts">
79
+ import { computed } from 'vue';
80
+ import { formatMessage } from '../../client/i18n/format.js';
81
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
84
82
  import type { DiffRow, DiffSummary, PlanVersionPair } from './types';
85
83
 
86
- defineProps<{
84
+ const props = defineProps<{
87
85
  diffPair: PlanVersionPair | null;
88
86
  diff: DiffSummary;
89
87
  diffRows: DiffRow[];
90
88
  }>();
89
+
90
+ const msg = useSaMessages('planDetail');
91
+
92
+ const panelTitle = computed(() => {
93
+ const pair = props.diffPair;
94
+ if (!pair) return msg.value.diff.title;
95
+ return formatMessage(msg.value.diff.titleCompare, {
96
+ from: pair.from.version,
97
+ to: pair.to.version,
98
+ });
99
+ });
91
100
  </script>
@@ -13,7 +13,7 @@
13
13
  <path d="M5 12h14M13 5l7 7-7 7" />
14
14
  </svg>
15
15
  </span>
16
- <span>Zurück zur Liste</span>
16
+ <span>{{ msg.header.backToList }}</span>
17
17
  </button>
18
18
  </div>
19
19
 
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
  <div class="pc-header-titles">
26
26
  <h2 class="pc-h-title">{{ plan.label }}</h2>
27
- <p class="pc-h-sub">{{ plan.description || 'Keine Beschreibung.' }}</p>
27
+ <p class="pc-h-sub">{{ plan.description || msg.header.noDescription }}</p>
28
28
  </div>
29
29
  </div>
30
30
  <div class="pc-header-right">
@@ -40,7 +40,7 @@
40
40
  <rect x="9" y="9" width="11" height="11" rx="2" />
41
41
  <path d="M5 15V5a2 2 0 012-2h10" />
42
42
  </svg>
43
- <span>Plan klonen</span>
43
+ <span>{{ msg.header.clonePlan }}</span>
44
44
  </button>
45
45
  <button class="pc-btn" type="button" @click="emit('viewCatalog')">
46
46
  <svg
@@ -54,13 +54,13 @@
54
54
  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
55
55
  <circle cx="12" cy="12" r="3" />
56
56
  </svg>
57
- <span>Im Katalog ansehen</span>
57
+ <span>{{ msg.header.viewInCatalog }}</span>
58
58
  </button>
59
59
  <button
60
60
  class="pc-btn pc-btn--primary"
61
61
  type="button"
62
62
  :disabled="hasOpenDraft"
63
- :title="hasOpenDraft ? 'Es gibt bereits eine offene Draft' : undefined"
63
+ :title="hasOpenDraft ? msg.header.draftAlreadyOpen : undefined"
64
64
  @click="emit('createDraft')"
65
65
  >
66
66
  <svg
@@ -73,7 +73,7 @@
73
73
  >
74
74
  <path d="M12 5v14M5 12h14" />
75
75
  </svg>
76
- <span>Neue Draft-Version</span>
76
+ <span>{{ msg.header.newDraftVersion }}</span>
77
77
  </button>
78
78
  </div>
79
79
  </div>
@@ -81,6 +81,7 @@
81
81
 
82
82
  <script setup lang="ts">
83
83
  import type { PlanRow } from '@saasicat/types';
84
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
84
85
 
85
86
  defineProps<{
86
87
  plan: PlanRow;
@@ -95,4 +96,6 @@ const emit = defineEmits<{
95
96
  (e: 'viewCatalog'): void;
96
97
  (e: 'createDraft'): void;
97
98
  }>();
99
+
100
+ const msg = useSaMessages('planDetail');
98
101
  </script>
@@ -2,10 +2,8 @@
2
2
  <section v-if="tenantImpact" class="pc-card">
3
3
  <div class="pc-card-head">
4
4
  <div class="pc-card-head-text">
5
- <div class="pc-card-title">Tenant-Impact</div>
6
- <div class="pc-card-sub">
7
- Wer ist betroffen, wenn die nächste Version published wird
8
- </div>
5
+ <div class="pc-card-title">{{ msg.kpis.tenantImpact }}</div>
6
+ <div class="pc-card-sub">{{ msg.impact.subtitle }}</div>
9
7
  </div>
10
8
  </div>
11
9
  <div class="pc-impact-strip">
@@ -19,7 +17,7 @@
19
17
  background: '#10b981',
20
18
  }"
21
19
  />
22
- <span class="pc-impact-bar-label">{{ tenantImpact.auto }} auto-migrieren</span>
20
+ <span class="pc-impact-bar-label">{{ autoLabel }}</span>
23
21
  </div>
24
22
  <div class="pc-impact-bar">
25
23
  <span
@@ -29,9 +27,7 @@
29
27
  background: '#f59e0b',
30
28
  }"
31
29
  />
32
- <span class="pc-impact-bar-label"
33
- >{{ tenantImpact.review }} brauchen Review</span
34
- >
30
+ <span class="pc-impact-bar-label">{{ reviewLabel }}</span>
35
31
  </div>
36
32
  <div class="pc-impact-bar">
37
33
  <span
@@ -41,7 +37,7 @@
41
37
  background: '#ef4444',
42
38
  }"
43
39
  />
44
- <span class="pc-impact-bar-label">{{ tenantImpact.conflict }} Konflikt</span>
40
+ <span class="pc-impact-bar-label">{{ conflictLabel }}</span>
45
41
  </div>
46
42
  </div>
47
43
  </div>
@@ -64,13 +60,28 @@
64
60
  </template>
65
61
 
66
62
  <script setup lang="ts">
63
+ import { computed } from 'vue';
64
+ import { formatMessage } from '../../client/i18n/format.js';
65
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
67
66
  import type { ImpactTenant, TenantImpact } from './types';
68
67
 
69
- defineProps<{
68
+ const props = defineProps<{
70
69
  tenantImpact: TenantImpact | null;
71
70
  tenantImpactTotal: number;
72
71
  }>();
73
72
 
73
+ const msg = useSaMessages('planDetail');
74
+
75
+ const autoLabel = computed(() =>
76
+ formatMessage(msg.value.impact.autoMigrate, { count: props.tenantImpact?.auto ?? 0 }),
77
+ );
78
+ const reviewLabel = computed(() =>
79
+ formatMessage(msg.value.impact.needReview, { count: props.tenantImpact?.review ?? 0 }),
80
+ );
81
+ const conflictLabel = computed(() =>
82
+ formatMessage(msg.value.impact.conflict, { count: props.tenantImpact?.conflict ?? 0 }),
83
+ );
84
+
74
85
  function pct(part: number, total: number): string {
75
86
  if (total <= 0) return '0%';
76
87
  return `${Math.max(2, Math.round((part / total) * 100))}%`;
@@ -86,7 +97,9 @@ function initialsOf(name: string): string {
86
97
  }
87
98
 
88
99
  function impactStateLabel(state: ImpactTenant['state']): string {
89
- return state === 'auto' ? 'auto' : state === 'review' ? 'review' : 'Konflikt';
100
+ if (state === 'auto') return msg.value.impact.stateAuto;
101
+ if (state === 'review') return msg.value.impact.stateReview;
102
+ return msg.value.impact.stateConflict;
90
103
  }
91
104
 
92
105
  function impactStateChip(state: ImpactTenant['state']): string {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="pc-kpis">
3
3
  <div class="pc-kpi">
4
- <div class="pc-kpi-label">Aktive Version</div>
4
+ <div class="pc-kpi-label">{{ msg.kpis.activeVersion }}</div>
5
5
  <div class="pc-kpi-val">
6
6
  <template v-if="liveVersion">
7
7
  v{{ liveVersion.version }}
@@ -9,44 +9,39 @@
9
9
  </template>
10
10
  <template v-else>—</template>
11
11
  </div>
12
- <div class="pc-kpi-sub">
13
- {{
14
- liveVersion?.validFrom
15
- ? `seit ${liveVersion.validFrom.slice(0, 10)}`
16
- : 'keine veröffentlichte Version'
17
- }}
18
- </div>
12
+ <div class="pc-kpi-sub">{{ activeVersionSub }}</div>
19
13
  </div>
20
14
  <div class="pc-kpi">
21
- <div class="pc-kpi-label">Tenant-Impact</div>
15
+ <div class="pc-kpi-label">{{ msg.kpis.tenantImpact }}</div>
22
16
  <div class="pc-kpi-val">{{ tenantImpactTotal }}</div>
23
- <div class="pc-kpi-sub">Mandanten auf diesem Plan</div>
17
+ <div class="pc-kpi-sub">{{ msg.kpis.tenantsOnPlan }}</div>
24
18
  </div>
25
19
  <div class="pc-kpi">
26
- <div class="pc-kpi-label">Versionen</div>
20
+ <div class="pc-kpi-label">{{ msg.kpis.versions }}</div>
27
21
  <div class="pc-kpi-val">{{ versionsCount }}</div>
28
- <div class="pc-kpi-sub">
29
- {{ publishedCount }} live/superseded · {{ draftCount }} Draft
30
- </div>
22
+ <div class="pc-kpi-sub">{{ versionsSummary }}</div>
31
23
  </div>
32
24
  <div :class="['pc-kpi', draftVersion ? 'pc-kpi--draft' : '']">
33
25
  <div class="pc-kpi-label" :style="draftVersion ? 'color:#b45309' : ''">
34
- {{ draftVersion ? 'Offener Draft' : 'Kein Draft' }}
26
+ {{ draftVersion ? msg.kpis.openDraft : msg.kpis.noDraft }}
35
27
  </div>
36
28
  <div class="pc-kpi-val">
37
29
  {{ draftVersion ? `v${draftVersion.version}` : '—' }}
38
30
  </div>
39
31
  <div class="pc-kpi-sub" :style="draftVersion ? 'color:#b45309' : ''">
40
- {{ draftVersion ? 'Bereit zum Bearbeiten' : 'Neue Draft anlegen' }}
32
+ {{ draftVersion ? msg.kpis.readyToEdit : msg.kpis.createNewDraft }}
41
33
  </div>
42
34
  </div>
43
35
  </div>
44
36
  </template>
45
37
 
46
38
  <script setup lang="ts">
39
+ import { computed } from 'vue';
47
40
  import type { PlanVersionRow } from '@saasicat/types';
41
+ import { formatMessage } from '../../client/i18n/format.js';
42
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
48
43
 
49
- defineProps<{
44
+ const props = defineProps<{
50
45
  liveVersion: PlanVersionRow | null;
51
46
  tenantImpactTotal: number;
52
47
  versionsCount: number;
@@ -54,4 +49,20 @@ defineProps<{
54
49
  draftCount: number;
55
50
  draftVersion: PlanVersionRow | null;
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
+ formatMessage(msg.value.kpis.versionsSummaryWithDrafts, {
64
+ published: props.publishedCount,
65
+ drafts: props.draftCount,
66
+ }),
67
+ );
57
68
  </script>
@@ -2,9 +2,9 @@
2
2
  <section class="pc-card pc-card--left">
3
3
  <div class="pc-card-head">
4
4
  <div class="pc-card-head-text">
5
- <div class="pc-card-title">Versionen</div>
5
+ <div class="pc-card-title">{{ msg.versions.title }}</div>
6
6
  <div class="pc-card-sub">
7
- Pro Plan max. 1 offene Draft-Version · Publish setzt Vorgänger auf
7
+ {{ msg.versions.subtitle }}
8
8
  <code class="pc-mono">supersededAt</code>
9
9
  </div>
10
10
  </div>
@@ -24,7 +24,7 @@
24
24
  >
25
25
  <path d="M12 5v14M5 12h14" />
26
26
  </svg>
27
- <span>Draft v{{ nextDraftVersion }}</span>
27
+ <span>{{ newDraftButtonLabel }}</span>
28
28
  </button>
29
29
  </div>
30
30
 
@@ -36,7 +36,10 @@
36
36
  :class="['pc-vtl-seg', `pc-vtl-${seg.status}`]"
37
37
  :style="{ flex: seg.flex }"
38
38
  >
39
- v{{ seg.version }}<template v-if="seg.status === 'draft'"> · draft</template>
39
+ v{{ seg.version
40
+ }}<template v-if="seg.status === 'draft'">{{
41
+ msg.versions.timelineDraftSuffix
42
+ }}</template>
40
43
  </div>
41
44
  </div>
42
45
  <div class="pc-vtl-axis">
@@ -45,15 +48,19 @@
45
48
  </div>
46
49
 
47
50
  <div class="pc-vrow pc-vrow--head">
48
- <div class="pc-vrow-version pc-vrow-headlabel">Version</div>
49
- <div class="pc-vrow-cell pc-vrow-headlabel">Gültig</div>
50
- <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">Pricing</div>
51
- <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">Impact</div>
52
- <div class="pc-vrow-note pc-vrow-headlabel">Change-Note</div>
51
+ <div class="pc-vrow-version pc-vrow-headlabel">{{ msg.versions.colVersion }}</div>
52
+ <div class="pc-vrow-cell pc-vrow-headlabel">{{ msg.versions.colValidity }}</div>
53
+ <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">
54
+ {{ msg.versions.colPricing }}
55
+ </div>
56
+ <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">
57
+ {{ msg.versions.colImpact }}
58
+ </div>
59
+ <div class="pc-vrow-note pc-vrow-headlabel">{{ msg.versions.colChangeNoteShort }}</div>
53
60
  <div class="pc-vrow-actions" />
54
61
  </div>
55
62
  <div v-if="versions.length === 0" class="pc-empty">
56
- Noch keine Versionen — „Neue Draft-Version" oben rechts.
63
+ {{ msg.versions.empty }}
57
64
  </div>
58
65
  <div
59
66
  v-for="row in sortedVersions"
@@ -77,9 +84,12 @@
77
84
  </div>
78
85
  <div class="pc-vrow-cell pc-vrow-cell--right">
79
86
  <div class="pc-vrow-price">
80
- {{ formatMoney(row.monthlyNet) }}<span class="pc-vrow-price-unit">/ Mo</span>
87
+ {{ formatMoney(row.monthlyNet)
88
+ }}<span class="pc-vrow-price-unit">{{ msg.versions.perMonthUnit }}</span>
89
+ </div>
90
+ <div class="pc-vrow-sub">
91
+ {{ formatMoney(row.yearlyNet) }} {{ msg.versions.perYearUnit }}
81
92
  </div>
82
- <div class="pc-vrow-sub">{{ formatMoney(row.yearlyNet) }} / Jahr</div>
83
93
  </div>
84
94
  <div class="pc-vrow-cell pc-vrow-cell--right">
85
95
  <div
@@ -88,7 +98,7 @@
88
98
  >
89
99
  {{ impactByVersion[row.version] ?? 0 }}
90
100
  </div>
91
- <div class="pc-vrow-sub">Mandanten</div>
101
+ <div class="pc-vrow-sub">{{ msg.versions.tenants }}</div>
92
102
  </div>
93
103
  <div class="pc-vrow-note" :title="row.changeNote ?? ''">
94
104
  {{ row.changeNote || '—' }}
@@ -110,7 +120,7 @@
110
120
  >
111
121
  <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
112
122
  </svg>
113
- <span>Publish</span>
123
+ <span>{{ msg.versions.publish }}</span>
114
124
  </button>
115
125
  <button
116
126
  v-if="editabilityOf(row).editable"
@@ -118,8 +128,8 @@
118
128
  type="button"
119
129
  :title="
120
130
  editabilityOf(row).reason === 'pre-active'
121
- ? 'Published, aber noch nicht aktiv — Features, Quotas und Preis können bis zum Aktivierungsdatum noch korrigiert werden'
122
- : 'Draft bearbeiten'
131
+ ? msg.versions.preActiveTitle
132
+ : msg.versions.editDraftTitle
123
133
  "
124
134
  @click="emit('editDraft', row)"
125
135
  >
@@ -133,7 +143,7 @@
133
143
  >
134
144
  <path d="M12 20h9M16.5 3.5a2.12 2.12 0 113 3L7 19l-4 1 1-4 12.5-12.5z" />
135
145
  </svg>
136
- <span>Bearbeiten</span>
146
+ <span>{{ common.edit }}</span>
137
147
  </button>
138
148
  <button
139
149
  v-if="row.publishedAt !== null && row.supersededAt === null && draftVersion"
@@ -152,7 +162,7 @@
152
162
  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
153
163
  <circle cx="12" cy="12" r="3" />
154
164
  </svg>
155
- <span>Diff</span>
165
+ <span>{{ msg.versions.viewDiff }}</span>
156
166
  </button>
157
167
  </div>
158
168
  </div>
@@ -160,10 +170,14 @@
160
170
  </template>
161
171
 
162
172
  <script setup lang="ts">
173
+ import { computed } from 'vue';
163
174
  import { isVersionEditable, type PlanVersionRow } from '@saasicat/types';
175
+ import { formatMessage } from '../../client/i18n/format.js';
176
+ import { formatCurrency } from '../../client/i18n/currency.js';
177
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
164
178
  import type { TimelineSegment } from './types';
165
179
 
166
- defineProps<{
180
+ const props = defineProps<{
167
181
  versions: PlanVersionRow[];
168
182
  sortedVersions: PlanVersionRow[];
169
183
  timelineSegments: TimelineSegment[];
@@ -181,6 +195,14 @@ const emit = defineEmits<{
181
195
  (e: 'viewDiff', from: PlanVersionRow, to: PlanVersionRow): void;
182
196
  }>();
183
197
 
198
+ const msg = useSaMessages('planDetail');
199
+ const { locale } = useSuperAdminI18n();
200
+ const common = useSaMessages('common');
201
+
202
+ const newDraftButtonLabel = computed(() =>
203
+ formatMessage(msg.value.versions.newDraftButton, { version: props.nextDraftVersion }),
204
+ );
205
+
184
206
  function editabilityOf(version: PlanVersionRow): {
185
207
  editable: boolean;
186
208
  reason: 'draft' | 'pre-active' | null;
@@ -203,8 +225,7 @@ function statusLabel(row: PlanVersionRow): string {
203
225
  function formatMoney(raw: string | number): string {
204
226
  const num = typeof raw === 'string' ? Number(raw) : raw;
205
227
  if (!Number.isFinite(num)) return String(raw);
206
- if (Number.isInteger(num)) return `${num} €`;
207
- return `${num.toFixed(2).replace('.', ',')} €`;
228
+ return formatCurrency(num, locale.value);
208
229
  }
209
230
 
210
231
  function updatedLabel(row: PlanVersionRow): string {
@@ -218,9 +239,13 @@ function updatedLabel(row: PlanVersionRow): string {
218
239
  if (Number.isNaN(d.getTime())) return '';
219
240
  const diffMs = Date.now() - d.getTime();
220
241
  const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
221
- if (days <= 0) return 'heute';
222
- if (days < 30) return `vor ${days} Tagen`;
223
- if (days < 365) return `vor ${Math.floor(days / 30)} Monaten`;
224
- return `vor ${Math.floor(days / 365)} Jahren`;
242
+ if (days <= 0) return msg.value.versions.updatedToday;
243
+ if (days < 30) return formatMessage(msg.value.versions.updatedDaysAgo, { days });
244
+ if (days < 365) {
245
+ return formatMessage(msg.value.versions.updatedMonthsAgo, {
246
+ months: Math.floor(days / 30),
247
+ });
248
+ }
249
+ return formatMessage(msg.value.versions.updatedYearsAgo, { years: Math.floor(days / 365) });
225
250
  }
226
251
  </script>
@@ -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 {