@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,11 +2,17 @@
2
2
  <div class="sa-users">
3
3
  <header class="sa-page-head">
4
4
  <div>
5
- <h1 class="sa-page-head__title">{{ title }}</h1>
5
+ <h1 class="sa-page-head__title">{{ resolvedTitle }}</h1>
6
6
  <p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
7
7
  </div>
8
8
  <div class="sa-page-head__actions">
9
- <q-btn unelevated color="primary" icon="search" label="Suchen" @click="reload" />
9
+ <q-btn
10
+ unelevated
11
+ color="primary"
12
+ icon="search"
13
+ :label="common.search"
14
+ @click="reload"
15
+ />
10
16
  </div>
11
17
  </header>
12
18
 
@@ -31,7 +37,7 @@
31
37
  v-model="filter.q"
32
38
  outlined
33
39
  dense
34
- label="E-Mail oder Name"
40
+ :label="msg.filterQuery"
35
41
  clearable
36
42
  @keyup.enter="reload"
37
43
  @clear="reload"
@@ -40,7 +46,7 @@
40
46
  v-model="filter.tenant"
41
47
  outlined
42
48
  dense
43
- label="Tenant-Slug"
49
+ :label="msg.filterTenant"
44
50
  clearable
45
51
  @keyup.enter="reload"
46
52
  @clear="reload"
@@ -62,12 +68,12 @@
62
68
  <q-td>
63
69
  <q-badge
64
70
  :color="row.isActive ? 'positive' : 'grey'"
65
- :label="row.isActive ? 'aktiv' : 'deaktiviert'"
71
+ :label="row.isActive ? msg.statusActive : msg.statusDeactivated"
66
72
  />
67
73
  <q-badge
68
74
  v-if="row.invitationStatus === 'PENDING'"
69
75
  color="amber-7"
70
- label="Pending"
76
+ :label="msg.badgePending"
71
77
  class="q-ml-xs"
72
78
  />
73
79
  </q-td>
@@ -108,6 +114,8 @@ import { computed, onMounted, reactive, ref } from 'vue';
108
114
  import { useQuasar } from 'quasar';
109
115
  import { useSuperAdminNotify } from '../quasar/notify.js';
110
116
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
117
+ import { formatMessage } from '../client/i18n/format.js';
118
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
111
119
 
112
120
  // Platform standard page: user search. Data-agnostic.
113
121
  //
@@ -166,7 +174,6 @@ const props = withDefaults(
166
174
  submitDeactivate?: (id: string, reason: string, mfaCode?: string) => Promise<void>;
167
175
  }>(),
168
176
  {
169
- title: 'Benutzer',
170
177
  requireMfaForResetPassword: false,
171
178
  requireMfaForDeactivate: false,
172
179
  },
@@ -174,6 +181,12 @@ const props = withDefaults(
174
181
 
175
182
  const q = useQuasar();
176
183
  const notify = useSuperAdminNotify();
184
+ const msg = useSaMessages('users');
185
+ const common = useSaMessages('common');
186
+ const shell = useSaMessages('shell');
187
+ const { intlLocale } = useSuperAdminI18n();
188
+
189
+ const resolvedTitle = computed(() => props.title ?? msg.value.title);
177
190
  const rows = ref<UserRow[]>([]);
178
191
  const loading = ref(false);
179
192
  const filter = reactive({ q: '', tenant: '' });
@@ -256,42 +269,42 @@ const statTiles = computed<
256
269
  if (isSuperAdmin(r)) supr++;
257
270
  }
258
271
  return [
259
- { id: 'all', label: 'Alle', count: rows.value.length },
260
- { id: 'active', label: 'Aktiv', count: active, tone: 'positive' },
261
- { id: 'blocked', label: 'Gesperrt', count: blocked, tone: 'muted' },
262
- { id: 'never', label: 'Nie eingeloggt', count: never, tone: 'warn' },
263
- { id: 'super', label: 'Super-Admins', count: supr, tone: 'purple' },
272
+ { id: 'all', label: common.value.all, count: rows.value.length },
273
+ { id: 'active', label: common.value.active, count: active, tone: 'positive' },
274
+ { id: 'blocked', label: msg.value.tiles.blocked, count: blocked, tone: 'muted' },
275
+ { id: 'never', label: msg.value.tiles.neverLoggedIn, count: never, tone: 'warn' },
276
+ { id: 'super', label: msg.value.tiles.superAdmins, count: supr, tone: 'purple' },
264
277
  ];
265
278
  });
266
279
 
267
- const baseColumns = [
280
+ const baseColumns = computed(() => [
268
281
  {
269
282
  name: 'email',
270
- label: 'E-Mail',
283
+ label: msg.value.columns.email,
271
284
  field: 'email',
272
285
  align: 'left' as const,
273
286
  sortable: true,
274
287
  },
275
288
  {
276
289
  name: 'name',
277
- label: 'Name',
290
+ label: common.value.name,
278
291
  field: (r: UserRow) => `${r.firstName} ${r.lastName}`,
279
292
  align: 'left' as const,
280
293
  },
281
294
  {
282
295
  name: 'tenant',
283
- label: 'Tenant',
284
- field: (r: UserRow) => r.tenantSlug ?? '— (SuperAdmin)',
296
+ label: msg.value.columns.tenant,
297
+ field: (r: UserRow) => r.tenantSlug ?? msg.value.tenantFallback,
285
298
  align: 'left' as const,
286
299
  },
287
- { name: 'role', label: 'Rolle', field: 'role', align: 'left' as const },
288
- { name: 'status', label: 'Status', field: 'isActive', align: 'left' as const },
300
+ { name: 'role', label: msg.value.columns.role, field: 'role', align: 'left' as const },
301
+ { name: 'status', label: common.value.status, field: 'isActive', align: 'left' as const },
289
302
  {
290
303
  name: 'lastLogin',
291
- label: 'Letzter Login',
304
+ label: msg.value.columns.lastLogin,
292
305
  field: (r: UserRow) =>
293
306
  r.lastLoginAt
294
- ? new Date(r.lastLoginAt).toLocaleDateString('de-DE', {
307
+ ? new Date(r.lastLoginAt).toLocaleDateString(intlLocale.value, {
295
308
  day: '2-digit',
296
309
  month: 'short',
297
310
  year: 'numeric',
@@ -299,7 +312,7 @@ const baseColumns = [
299
312
  : '—',
300
313
  align: 'left' as const,
301
314
  },
302
- ];
315
+ ]);
303
316
 
304
317
  // Built-in default actions — APPENDED to consumer actions, not replaced.
305
318
  const bakedActions = computed<UserRowAction[]>(() => {
@@ -307,7 +320,7 @@ const bakedActions = computed<UserRowAction[]>(() => {
307
320
  if (props.enableResetPassword && props.submitResetPassword) {
308
321
  out.push({
309
322
  id: 'reset-password',
310
- label: 'Passwort zurücksetzen',
323
+ label: msg.value.resetPassword.action,
311
324
  icon: 'lock_reset',
312
325
  color: 'primary',
313
326
  handler: (row) => onResetPasswordClick(row),
@@ -316,7 +329,7 @@ const bakedActions = computed<UserRowAction[]>(() => {
316
329
  if (props.enableDeactivate && props.submitDeactivate) {
317
330
  out.push({
318
331
  id: 'deactivate',
319
- label: 'Deaktivieren',
332
+ label: msg.value.deactivate.action,
320
333
  icon: 'block',
321
334
  color: 'negative',
322
335
  condition: (row) => (row as UserRow & { status?: string }).status !== 'suspended',
@@ -332,7 +345,7 @@ const mergedActions = computed<readonly UserRowAction[]>(() => [
332
345
  ]);
333
346
 
334
347
  const effectiveColumns = computed(() => {
335
- const cols = [...baseColumns];
348
+ const cols = [...baseColumns.value];
336
349
  if (mergedActions.value.length > 0) {
337
350
  cols.push({
338
351
  name: 'actions',
@@ -371,7 +384,7 @@ function errMsg(err: unknown): string {
371
384
  return (
372
385
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
373
386
  (err as Error)?.message ??
374
- 'Aktion fehlgeschlagen'
387
+ msg.value.errorAction
375
388
  );
376
389
  }
377
390
 
@@ -404,7 +417,7 @@ async function runAction<R>(
404
417
  } catch (err) {
405
418
  const status = (err as { response?: { status?: number } })?.response?.status;
406
419
  if (status === 401) {
407
- mfaError.value = 'TOTP-Code ungültig oder MFA nicht eingerichtet.';
420
+ mfaError.value = shell.value.mfa.invalidCode;
408
421
  continue;
409
422
  }
410
423
  showMfa.value = false;
@@ -418,15 +431,18 @@ function onResetPasswordClick(row: UserRow): void {
418
431
  if (!props.submitResetPassword) return;
419
432
  const submit = props.submitResetPassword;
420
433
  q.dialog({
421
- title: `Passwort für "${row.email}" zurücksetzen`,
422
- message: 'Grund für Audit (Pflichtfeld):',
434
+ title: formatMessage(msg.value.resetPassword.dialogTitle, { email: row.email }),
435
+ message: msg.value.reasonPrompt,
423
436
  prompt: { model: '', type: 'text' },
424
- cancel: 'Abbrechen',
425
- ok: { label: 'Zurücksetzen', color: 'primary' },
437
+ cancel: common.value.cancel,
438
+ ok: { label: common.value.reset, color: 'primary' },
426
439
  }).onOk(async (reason: string) => {
427
440
  if (!reason || reason.trim().length === 0) return;
428
441
  await runAction(
429
- `Passwort für "${row.email}" zurücksetzen — ${reason.trim()}.`,
442
+ formatMessage(msg.value.resetPassword.mfaDescription, {
443
+ email: row.email,
444
+ reason: reason.trim(),
445
+ }),
430
446
  !!props.requireMfaForResetPassword,
431
447
  (code) => submit(row.id, reason, code),
432
448
  (data) => {
@@ -435,12 +451,14 @@ function onResetPasswordClick(row: UserRow): void {
435
451
  // void → Notify only.
436
452
  if (data && typeof data === 'object' && 'oneTimePassword' in data) {
437
453
  q.dialog({
438
- title: 'Einmal-Passwort generiert',
439
- message: `Bitte sicher übermitteln: ${data.oneTimePassword}`,
440
- ok: { label: 'Verstanden' },
454
+ title: msg.value.resetPassword.otpTitle,
455
+ message: formatMessage(msg.value.resetPassword.otpMessage, {
456
+ password: data.oneTimePassword,
457
+ }),
458
+ ok: { label: msg.value.resetPassword.otpAcknowledge },
441
459
  });
442
460
  } else {
443
- notify('positive', 'Passwort-Reset ausgelöst.');
461
+ notify('positive', msg.value.resetPassword.success);
444
462
  }
445
463
  void reload();
446
464
  },
@@ -452,19 +470,25 @@ function onDeactivateClick(row: UserRow): void {
452
470
  if (!props.submitDeactivate) return;
453
471
  const submit = props.submitDeactivate;
454
472
  q.dialog({
455
- title: `User "${row.email}" deaktivieren`,
456
- message: 'Grund für Audit (Pflichtfeld):',
473
+ title: formatMessage(msg.value.deactivate.dialogTitle, { email: row.email }),
474
+ message: msg.value.reasonPrompt,
457
475
  prompt: { model: '', type: 'text' },
458
- cancel: 'Abbrechen',
459
- ok: { label: 'Deaktivieren', color: 'negative' },
476
+ cancel: common.value.cancel,
477
+ ok: { label: msg.value.deactivate.action, color: 'negative' },
460
478
  }).onOk(async (reason: string) => {
461
479
  if (!reason || reason.trim().length === 0) return;
462
480
  await runAction(
463
- `User "${row.email}" deaktivieren — ${reason.trim()}.`,
481
+ formatMessage(msg.value.deactivate.mfaDescription, {
482
+ email: row.email,
483
+ reason: reason.trim(),
484
+ }),
464
485
  !!props.requireMfaForDeactivate,
465
486
  (code) => submit(row.id, reason, code),
466
487
  () => {
467
- notify('positive', `${row.email} deaktiviert.`);
488
+ notify(
489
+ 'positive',
490
+ formatMessage(msg.value.deactivate.success, { email: row.email }),
491
+ );
468
492
  void reload();
469
493
  },
470
494
  );
@@ -19,7 +19,7 @@
19
19
  {{ statusLabel(bundle) }}
20
20
  </span>
21
21
  <span v-if="i18nLocaleCount(bundle) > 0" class="sa-chip sa-chip--info">
22
- {{ i18nLocaleCount(bundle) }} Übersetzung(en)
22
+ {{ translationCount(bundle) }}
23
23
  </span>
24
24
  </div>
25
25
  <div class="sa-bd-card__name">{{ bundle.label }}</div>
@@ -38,7 +38,7 @@
38
38
  </div>
39
39
 
40
40
  <div v-if="bundlesTotal > 0 && filteredBundles.length === 0" class="sa-bd-empty-row">
41
- Keine Bundles entsprechen der Suche.
41
+ {{ msg.list.emptyNoMatch }}
42
42
  </div>
43
43
  </div>
44
44
  </template>
@@ -46,9 +46,11 @@
46
46
  <script setup lang="ts">
47
47
  import type { BundleRow } from '@saasicat/types';
48
48
  import {
49
- BUNDLE_STATUS_META,
49
+ bundleStatusMeta,
50
50
  type BundleAggregateStatus,
51
51
  } from '../../components/bundle-editor/bundle-version-status.js';
52
+ import { formatMessage } from '../../client/i18n/format.js';
53
+ import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
52
54
 
53
55
  const props = defineProps<{
54
56
  filteredBundles: BundleRow[];
@@ -66,17 +68,27 @@ const emit = defineEmits<{
66
68
  toggle: [bundle: BundleRow];
67
69
  }>();
68
70
 
71
+ const msg = useSaMessages('bundles');
72
+ const { locale } = useSuperAdminI18n();
73
+
74
+ function statusMetaOf(bundle: BundleRow) {
75
+ return bundleStatusMeta(props.aggregateStatusOf(bundle), locale.value);
76
+ }
77
+
69
78
  function statusClass(bundle: BundleRow): string {
70
- return BUNDLE_STATUS_META[props.aggregateStatusOf(bundle)]?.cls ?? 'draft';
79
+ return statusMetaOf(bundle).cls;
71
80
  }
72
81
 
73
82
  function statusLabel(bundle: BundleRow): string {
74
- const status = props.aggregateStatusOf(bundle);
75
- return status === 'retired' ? 'Retired' : (BUNDLE_STATUS_META[status]?.label ?? status);
83
+ return statusMetaOf(bundle).label;
84
+ }
85
+
86
+ function statusTooltip(bundle: BundleRow): string {
87
+ return statusMetaOf(bundle).tooltip;
76
88
  }
77
89
 
78
- function statusTooltip(bundle: BundleRow): string | undefined {
79
- return BUNDLE_STATUS_META[props.aggregateStatusOf(bundle)]?.tooltip;
90
+ function translationCount(bundle: BundleRow): string {
91
+ return formatMessage(msg.value.list.translationCount, { count: props.i18nLocaleCount(bundle) });
80
92
  }
81
93
  </script>
82
94
 
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <div class="sa-bd-grid">
3
3
  <div class="sa-bd-col">
4
- <div class="sa-bd-section-label">Stammdaten</div>
4
+ <div class="sa-bd-section-label">{{ msg.fields.masterData }}</div>
5
5
  <div class="sa-bundles__form">
6
6
  <q-input
7
7
  :model-value="editForm.label"
8
8
  outlined
9
9
  dense
10
- label="Label"
10
+ :label="msg.fields.label"
11
11
  @update:model-value="(value) => patchEditForm({ label: String(value ?? '') })"
12
12
  />
13
13
  <q-input
@@ -16,7 +16,7 @@
16
16
  dense
17
17
  type="textarea"
18
18
  autogrow
19
- label="Beschreibung"
19
+ :label="common.description"
20
20
  @update:model-value="
21
21
  (value) => patchEditForm({ description: String(value ?? '') })
22
22
  "
@@ -25,7 +25,7 @@
25
25
  :model-value="editForm.icon"
26
26
  outlined
27
27
  dense
28
- label="Icon"
28
+ :label="common.icon"
29
29
  @update:model-value="(value) => patchEditForm({ icon: String(value ?? '') })"
30
30
  />
31
31
  <q-input
@@ -33,7 +33,7 @@
33
33
  outlined
34
34
  dense
35
35
  type="number"
36
- label="Sortier-Reihenfolge"
36
+ :label="common.sortOrder"
37
37
  @update:model-value="
38
38
  (value) => patchEditForm({ sortOrder: Number(value ?? 0) })
39
39
  "
@@ -41,11 +41,11 @@
41
41
  </div>
42
42
 
43
43
  <div class="sa-bd-section-label sa-bd-section-label--mt">
44
- <span>Übersetzungen</span>
45
- <span class="sa-bd-section-count">{{ translatableLocales.length }} Sprache(n)</span>
44
+ <span>{{ msg.detail.translations }}</span>
45
+ <span class="sa-bd-section-count">{{ languageCount }}</span>
46
46
  </div>
47
47
  <div v-if="translatableLocales.length === 0" class="sa-bd-i18n-hint">
48
- Keine weiteren Sprachen aktiv — werden im Marketing-Catalog aktiviert.
48
+ {{ msg.detail.noTranslatableLocales }}
49
49
  </div>
50
50
  <div v-for="lng in translatableLocales" :key="lng" class="sa-bd-i18n-block">
51
51
  <div class="sa-bd-i18n-head">
@@ -54,14 +54,14 @@
54
54
  v-if="!i18nDraft[lng]?.label || !i18nDraft[lng]?.description"
55
55
  class="sa-bd-i18n-fallback"
56
56
  >
57
- Fallback aus DE
57
+ {{ msg.detail.fallbackFromDe }}
58
58
  </span>
59
59
  </div>
60
60
  <q-input
61
61
  outlined
62
62
  dense
63
63
  :model-value="i18nDraft[lng]?.label || ''"
64
- :placeholder="`Label (Fallback: „${editForm.label}“)`"
64
+ :placeholder="labelPlaceholder"
65
65
  @update:model-value="
66
66
  (value) => emit('setI18n', lng, 'label', String(value ?? ''))
67
67
  "
@@ -72,7 +72,7 @@
72
72
  type="textarea"
73
73
  autogrow
74
74
  :model-value="i18nDraft[lng]?.description || ''"
75
- :placeholder="`Beschreibung (Fallback aus DE)`"
75
+ :placeholder="msg.detail.descriptionPlaceholder"
76
76
  @update:model-value="
77
77
  (value) => emit('setI18n', lng, 'description', String(value ?? ''))
78
78
  "
@@ -83,7 +83,7 @@
83
83
  class="sa-bd-save"
84
84
  unelevated
85
85
  color="primary"
86
- label="Stammdaten & Übersetzungen speichern"
86
+ :label="msg.detail.save"
87
87
  :loading="editSubmitting"
88
88
  @click="emit('submitEdit')"
89
89
  />
@@ -112,10 +112,7 @@
112
112
  @save="(data) => emit('inlineSave', bundle.id, selectedVersion!.id, data)"
113
113
  @discard="emit('discardVersion', bundle.id, selectedVersion!.id)"
114
114
  />
115
- <div v-else class="sa-bd-empty-row">
116
- Noch keine Version. Lege eine neue Version an, um Features, Quotas &amp; Pricing zu
117
- kuratieren.
118
- </div>
115
+ <div v-else class="sa-bd-empty-row">{{ msg.detail.noVersion }}</div>
119
116
 
120
117
  <div class="sa-bd-version-actions">
121
118
  <q-btn
@@ -123,7 +120,7 @@
123
120
  unelevated
124
121
  color="positive"
125
122
  icon="rocket_launch"
126
- label="Diese Version publishen"
123
+ :label="msg.detail.publishVersion"
127
124
  @click="emit('publishVersion', selectedVersion)"
128
125
  />
129
126
  <q-btn
@@ -132,7 +129,7 @@
132
129
  dense
133
130
  color="negative"
134
131
  icon="delete"
135
- label="Bundle soft-deleten"
132
+ :label="msg.detail.softDelete"
136
133
  @click="emit('deleteBundle', bundle)"
137
134
  />
138
135
  </div>
@@ -141,6 +138,7 @@
141
138
  </template>
142
139
 
143
140
  <script setup lang="ts">
141
+ import { computed } from 'vue';
144
142
  import type {
145
143
  BundleRow,
146
144
  BundleVersionRow,
@@ -154,6 +152,8 @@ import BundleVersionInlineEditor from '../../components/bundle-editor/BundleVers
154
152
  import BundleVersionStrip from '../../components/bundle-editor/BundleVersionStrip.vue';
155
153
  import type { FeatureMeta } from '../../components/bundle-editor/BundleFeaturesEditor.vue';
156
154
  import type { QuotaMeta } from '../../components/bundle-editor/catalog-i18n.js';
155
+ import { formatMessage } from '../../client/i18n/format.js';
156
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
157
157
  import type { BundleEditForm } from './types.js';
158
158
 
159
159
  const props = defineProps<{
@@ -187,6 +187,16 @@ const emit = defineEmits<{
187
187
  deleteBundle: [bundle: BundleRow];
188
188
  }>();
189
189
 
190
+ const msg = useSaMessages('bundles');
191
+ const common = useSaMessages('common');
192
+
193
+ const languageCount = computed(() =>
194
+ formatMessage(msg.value.detail.languageCount, { count: props.translatableLocales.length }),
195
+ );
196
+ const labelPlaceholder = computed(() =>
197
+ formatMessage(msg.value.detail.labelPlaceholder, { label: props.editForm.label }),
198
+ );
199
+
190
200
  function patchEditForm(patch: Partial<BundleEditForm>): void {
191
201
  emit('update:editForm', { ...props.editForm, ...patch });
192
202
  }
@@ -5,7 +5,7 @@
5
5
  dense
6
6
  outlined
7
7
  clearable
8
- placeholder="Bundle-Key oder Label suchen …"
8
+ :placeholder="msg.filterBar.searchPlaceholder"
9
9
  class="sa-bundles__search"
10
10
  @update:model-value="emitQuery"
11
11
  >
@@ -26,6 +26,7 @@
26
26
 
27
27
  <script setup lang="ts">
28
28
  import type { BundlesStatusFilter, BundlesStatusFilterOption } from './types.js';
29
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
29
30
 
30
31
  defineProps<{
31
32
  query: string;
@@ -33,6 +34,8 @@ defineProps<{
33
34
  statusFilterOptions: BundlesStatusFilterOption[];
34
35
  }>();
35
36
 
37
+ const msg = useSaMessages('bundles');
38
+
36
39
  const emit = defineEmits<{
37
40
  'update:query': [value: string];
38
41
  'update:statusFilter': [value: BundlesStatusFilter];
@@ -1,11 +1,8 @@
1
1
  <template>
2
2
  <header class="sa-bundles__head">
3
3
  <div>
4
- <h1 class="sa-bundles__title">Bundles</h1>
5
- <p class="sa-bundles__sub">
6
- Produktgruppen aus Features &amp; Quotas — verwendet in Plänen. Bundles werden im
7
- SuperAdmin kuratiert.
8
- </p>
4
+ <h1 class="sa-bundles__title">{{ msg.header.title }}</h1>
5
+ <p class="sa-bundles__sub">{{ msg.header.subtitle }}</p>
9
6
  </div>
10
7
  <div class="sa-bundles__head-actions">
11
8
  <q-select
@@ -17,7 +14,7 @@
17
14
  emit-value
18
15
  map-options
19
16
  class="sa-bundles__locale"
20
- label="Anzeige-Sprache"
17
+ :label="msg.header.displayLocale"
21
18
  @update:model-value="(value) => emit('update:displayLocale', String(value))"
22
19
  >
23
20
  <template #prepend><q-icon name="translate" size="18px" /></template>
@@ -26,11 +23,11 @@
26
23
  unelevated
27
24
  color="primary"
28
25
  icon="add"
29
- label="Neues Bundle"
26
+ :label="msg.header.newBundle"
30
27
  @click="emit('create')"
31
28
  />
32
29
  <q-btn flat icon="refresh" :loading="loading" @click="emit('refresh')">
33
- <q-tooltip>Liste neu laden</q-tooltip>
30
+ <q-tooltip>{{ msg.header.reload }}</q-tooltip>
34
31
  </q-btn>
35
32
  </div>
36
33
  </header>
@@ -39,6 +36,8 @@
39
36
  <script setup lang="ts">
40
37
  import { computed } from 'vue';
41
38
 
39
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
40
+
42
41
  // The language selector controls which locale feature/quota labels are resolved
43
42
  // on the create form and in the detail view (fallback locale → DE → key). The default is the
44
43
  // default locale (DE); further options come from the active project locales.
@@ -55,6 +54,8 @@ const emit = defineEmits<{
55
54
  'update:displayLocale': [locale: string];
56
55
  }>();
57
56
 
57
+ const msg = useSaMessages('bundles');
58
+
58
59
  const localeOptions = computed(() =>
59
60
  props.locales.map((locale) => ({ label: locale.toUpperCase(), value: locale })),
60
61
  );
@@ -1,32 +1,35 @@
1
1
  <template>
2
2
  <div class="sa-bundles__kpis">
3
3
  <div class="sa-bundles__kpi">
4
- <div class="sa-bundles__kpi-label">Bundles gesamt</div>
4
+ <div class="sa-bundles__kpi-label">{{ msg.kpis.total }}</div>
5
5
  <div class="sa-bundles__kpi-value">{{ bundlesTotal }}</div>
6
- <div class="sa-bundles__kpi-sub">
7
- {{ liveCount }} live · {{ scheduledBundlesCount }} mit geplanter Version
8
- </div>
6
+ <div class="sa-bundles__kpi-sub">{{ totalSub }}</div>
9
7
  </div>
10
8
  <div class="sa-bundles__kpi">
11
- <div class="sa-bundles__kpi-label">Geplante Versionen</div>
9
+ <div class="sa-bundles__kpi-label">{{ msg.kpis.scheduled }}</div>
12
10
  <div class="sa-bundles__kpi-value">{{ totalScheduledVersions }}</div>
13
- <div class="sa-bundles__kpi-sub">zukünftig aktiv · noch nicht verkaufbar</div>
11
+ <div class="sa-bundles__kpi-sub">{{ msg.kpis.scheduledSub }}</div>
14
12
  </div>
15
13
  <div class="sa-bundles__kpi">
16
- <div class="sa-bundles__kpi-label">Offene Drafts</div>
14
+ <div class="sa-bundles__kpi-label">{{ msg.kpis.drafts }}</div>
17
15
  <div class="sa-bundles__kpi-value">{{ totalDraftVersions }}</div>
18
- <div class="sa-bundles__kpi-sub">{{ draftBundlesCount }} Bundle(s) ohne Publish</div>
16
+ <div class="sa-bundles__kpi-sub">{{ draftsSub }}</div>
19
17
  </div>
20
18
  <div class="sa-bundles__kpi">
21
- <div class="sa-bundles__kpi-label">Mit Übersetzung</div>
19
+ <div class="sa-bundles__kpi-label">{{ msg.kpis.translated }}</div>
22
20
  <div class="sa-bundles__kpi-value">{{ translatedCount }}</div>
23
- <div class="sa-bundles__kpi-sub">{{ localesCount }} aktive Sprache(n) im Projekt</div>
21
+ <div class="sa-bundles__kpi-sub">{{ translatedSub }}</div>
24
22
  </div>
25
23
  </div>
26
24
  </template>
27
25
 
28
26
  <script setup lang="ts">
29
- defineProps<{
27
+ import { computed } from 'vue';
28
+
29
+ import { formatMessage } from '../../client/i18n/format.js';
30
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
31
+
32
+ const props = defineProps<{
30
33
  bundlesTotal: number;
31
34
  liveCount: number;
32
35
  scheduledBundlesCount: number;
@@ -36,4 +39,19 @@ defineProps<{
36
39
  translatedCount: number;
37
40
  localesCount: number;
38
41
  }>();
42
+
43
+ const msg = useSaMessages('bundles');
44
+
45
+ const totalSub = computed(() =>
46
+ formatMessage(msg.value.kpis.totalSub, {
47
+ live: props.liveCount,
48
+ scheduled: props.scheduledBundlesCount,
49
+ }),
50
+ );
51
+ const draftsSub = computed(() =>
52
+ formatMessage(msg.value.kpis.draftsSub, { count: props.draftBundlesCount }),
53
+ );
54
+ const translatedSub = computed(() =>
55
+ formatMessage(msg.value.kpis.translatedSub, { count: props.localesCount }),
56
+ );
39
57
  </script>