@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,7 +2,7 @@
2
2
  <div class="sa-promo-codes">
3
3
  <header class="sa-page-head">
4
4
  <div>
5
- <h1 class="sa-page-head__title">Promo-Codes</h1>
5
+ <h1 class="sa-page-head__title">{{ msg.title }}</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">
@@ -12,7 +12,7 @@
12
12
  unelevated
13
13
  color="primary"
14
14
  icon="add"
15
- :label="createLabel"
15
+ :label="resolvedCreateLabel"
16
16
  @click="showCreate = true"
17
17
  />
18
18
  </slot>
@@ -41,7 +41,7 @@
41
41
  v-model="filter.search"
42
42
  outlined
43
43
  dense
44
- label="Suche (Code)"
44
+ :label="msg.list.searchLabel"
45
45
  clearable
46
46
  @update:model-value="reload"
47
47
  />
@@ -52,7 +52,7 @@
52
52
  emit-value
53
53
  map-options
54
54
  clearable
55
- label="Status"
55
+ :label="common.status"
56
56
  :options="statusOptions"
57
57
  @update:model-value="reload"
58
58
  />
@@ -138,6 +138,8 @@ export function computePlanColors(
138
138
  <script setup lang="ts">
139
139
  import { computed, onMounted, reactive, ref } from 'vue';
140
140
  import { useQuasar } from 'quasar';
141
+ import { formatMessage } from '../client/i18n/format.js';
142
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
141
143
  import { useSuperAdminNotify } from '../quasar/notify.js';
142
144
  import PromoCodeCreateDialog from '../components/dialogs/PromoCodeCreateDialog.vue';
143
145
  import PromoCodeEditDialog, {
@@ -210,11 +212,15 @@ const props = withDefaults(
210
212
  }>(),
211
213
  {
212
214
  statusOptions: () => ['ACTIVE', 'PAUSED', 'EXHAUSTED', 'EXPIRED'],
213
- createLabel: 'Promo-Code anlegen',
214
215
  planOptions: () => [],
215
216
  },
216
217
  );
217
218
 
219
+ const msg = useSaMessages('promos');
220
+ const common = useSaMessages('common');
221
+ const { intlLocale } = useSuperAdminI18n();
222
+ const resolvedCreateLabel = computed(() => props.createLabel ?? msg.value.createAction);
223
+
218
224
  const q = useQuasar();
219
225
  const notify = useSuperAdminNotify();
220
226
  const rows = ref<PromoRow[]>([]);
@@ -267,23 +273,23 @@ const statTiles = computed<
267
273
  if (c) counts[c]++;
268
274
  }
269
275
  return [
270
- { id: 'all', label: 'Alle', count: rows.value.length },
276
+ { id: 'all', label: common.value.all, count: rows.value.length },
271
277
  {
272
278
  id: 'active',
273
- label: 'Aktiv',
279
+ label: common.value.active,
274
280
  count: counts.active,
275
281
  tone: 'positive',
276
- hint: 'einlösbar',
282
+ hint: msg.value.list.hintRedeemable,
277
283
  },
278
284
  {
279
285
  id: 'scheduled',
280
- label: 'Geplant',
286
+ label: msg.value.list.tileScheduled,
281
287
  count: counts.scheduled,
282
288
  tone: 'info',
283
- hint: 'startet künftig',
289
+ hint: msg.value.list.hintStartsLater,
284
290
  },
285
- { id: 'paused', label: 'Pausiert', count: counts.paused, tone: 'warn' },
286
- { id: 'expired', label: 'Abgelaufen', count: counts.expired, tone: 'muted' },
291
+ { id: 'paused', label: msg.value.list.tilePaused, count: counts.paused, tone: 'warn' },
292
+ { id: 'expired', label: common.value.expired, count: counts.expired, tone: 'muted' },
287
293
  ];
288
294
  });
289
295
 
@@ -305,36 +311,41 @@ function onStatusTileClick(id: StatusFilter): void {
305
311
  }
306
312
  }
307
313
 
308
- const baseColumns = [
314
+ const baseColumns = computed(() => [
309
315
  {
310
316
  name: 'code',
311
- label: 'Code',
317
+ label: msg.value.list.columnCode,
312
318
  field: 'code',
313
319
  align: 'left' as const,
314
320
  sortable: true,
315
321
  },
316
- { name: 'valueType', label: 'Typ', field: 'valueType', align: 'left' as const },
317
- { name: 'value', label: 'Wert', field: 'value', align: 'right' as const },
318
- { name: 'status', label: 'Status', field: 'status', align: 'left' as const },
322
+ {
323
+ name: 'valueType',
324
+ label: common.value.type,
325
+ field: 'valueType',
326
+ align: 'left' as const,
327
+ },
328
+ { name: 'value', label: msg.value.list.columnValue, field: 'value', align: 'right' as const },
329
+ { name: 'status', label: common.value.status, field: 'status', align: 'left' as const },
319
330
  {
320
331
  name: 'redemptions',
321
- label: 'Einlösungen',
332
+ label: msg.value.list.columnRedemptions,
322
333
  field: (r: PromoRow) => `${r.redemptionsCount} / ${r.maxRedemptions ?? '∞'}`,
323
334
  align: 'right' as const,
324
335
  },
325
336
  {
326
337
  name: 'campaign',
327
- label: 'Kampagne',
338
+ label: msg.value.list.columnCampaign,
328
339
  field: (r: PromoRow) => r.campaignTag ?? '—',
329
340
  align: 'left' as const,
330
341
  },
331
342
  {
332
343
  name: 'validUntil',
333
- label: 'Gültig bis',
344
+ label: msg.value.list.columnValidUntil,
334
345
  field: (r: PromoRow) => formatDate(r.validUntil) ?? '—',
335
346
  align: 'left' as const,
336
347
  },
337
- ];
348
+ ]);
338
349
 
339
350
  // Built-in default actions — APPENDED to the consumer actions, not replacing them.
340
351
  const bakedActions = computed<PromoRowAction[]>(() => {
@@ -342,7 +353,7 @@ const bakedActions = computed<PromoRowAction[]>(() => {
342
353
  if (props.enableEdit && props.submitEdit) {
343
354
  out.push({
344
355
  id: 'edit',
345
- label: 'Bearbeiten',
356
+ label: common.value.edit,
346
357
  icon: 'edit',
347
358
  color: 'primary',
348
359
  handler: (row) => openEdit(row),
@@ -351,7 +362,7 @@ const bakedActions = computed<PromoRowAction[]>(() => {
351
362
  if (props.enableStatusToggle && props.submitEdit) {
352
363
  out.push({
353
364
  id: 'pause',
354
- label: 'Pausieren',
365
+ label: msg.value.list.actionPause,
355
366
  icon: 'pause',
356
367
  color: 'warning',
357
368
  condition: (row) => row.status === 'ACTIVE',
@@ -359,7 +370,7 @@ const bakedActions = computed<PromoRowAction[]>(() => {
359
370
  });
360
371
  out.push({
361
372
  id: 'activate',
362
- label: 'Aktivieren',
373
+ label: msg.value.list.actionActivate,
363
374
  icon: 'play_arrow',
364
375
  color: 'primary',
365
376
  condition: (row) => row.status === 'PAUSED',
@@ -369,7 +380,7 @@ const bakedActions = computed<PromoRowAction[]>(() => {
369
380
  if (props.enableDelete && props.submitDelete) {
370
381
  out.push({
371
382
  id: 'delete',
372
- label: 'Löschen',
383
+ label: common.value.delete,
373
384
  icon: 'delete',
374
385
  color: 'negative',
375
386
  handler: (row) => onDeleteClick(row),
@@ -384,7 +395,7 @@ const mergedActions = computed<readonly PromoRowAction[]>(() => [
384
395
  ]);
385
396
 
386
397
  const effectiveColumns = computed(() => {
387
- const cols = [...baseColumns];
398
+ const cols = [...baseColumns.value];
388
399
  if (mergedActions.value.length > 0) {
389
400
  cols.push({
390
401
  name: 'actions',
@@ -503,14 +514,14 @@ function onDeleteClick(row: PromoRow): void {
503
514
  if (!props.submitDelete) return;
504
515
  const submit = props.submitDelete;
505
516
  q.dialog({
506
- title: `Promo-Code "${row.code}" löschen`,
507
- message: 'Soft-Delete via Status=EXPIRED. Bestehende Redemptions bleiben.',
508
- cancel: 'Abbrechen',
509
- ok: { label: 'Löschen', color: 'negative' },
517
+ title: formatMessage(msg.value.list.deleteTitle, { code: row.code }),
518
+ message: msg.value.list.deleteMessage,
519
+ cancel: common.value.cancel,
520
+ ok: { label: common.value.delete, color: 'negative' },
510
521
  }).onOk(async () => {
511
522
  try {
512
523
  await submit(row.id);
513
- notify('positive', `${row.code} gelöscht.`);
524
+ notify('positive', formatMessage(msg.value.list.deletedNotice, { code: row.code }));
514
525
  await reload();
515
526
  } catch (err) {
516
527
  notify('negative', errMsg(err));
@@ -519,12 +530,12 @@ function onDeleteClick(row: PromoRow): void {
519
530
  }
520
531
 
521
532
  function onCreated(): void {
522
- notify('positive', 'Promo-Code angelegt.');
533
+ notify('positive', msg.value.list.createdNotice);
523
534
  void reload();
524
535
  }
525
536
 
526
537
  function onUpdated(): void {
527
- notify('positive', 'Promo-Code aktualisiert.');
538
+ notify('positive', msg.value.list.updatedNotice);
528
539
  void reload();
529
540
  }
530
541
 
@@ -532,14 +543,14 @@ function errMsg(err: unknown): string {
532
543
  return (
533
544
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
534
545
  (err as Error)?.message ??
535
- 'Aktion fehlgeschlagen'
546
+ msg.value.list.actionFailed
536
547
  );
537
548
  }
538
549
 
539
550
  function formatDate(iso: string | Date | null | undefined): string | null {
540
551
  if (!iso) return null;
541
552
  try {
542
- return new Date(iso).toLocaleDateString('de-DE', {
553
+ return new Date(iso).toLocaleDateString(intlLocale.value, {
543
554
  day: '2-digit',
544
555
  month: 'short',
545
556
  year: 'numeric',
@@ -2,7 +2,7 @@
2
2
  <div class="sa-subscriptions">
3
3
  <header class="sa-subscriptions__head">
4
4
  <div>
5
- <h1 class="sa-subscriptions__title">Abonnements</h1>
5
+ <h1 class="sa-subscriptions__title">{{ msg.subscriptions.title }}</h1>
6
6
  <p v-if="subtitle" class="sa-subscriptions__sub">{{ subtitle }}</p>
7
7
  </div>
8
8
  <slot name="head-actions" />
@@ -26,6 +26,7 @@
26
26
 
27
27
  <script setup lang="ts">
28
28
  import { computed, onMounted, ref } from 'vue';
29
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
29
30
 
30
31
  // Platform standard page: subscriptions (minimal). Apps pass
31
32
  // `loadSubscriptions` + optional `columns` through — default columns show
@@ -60,39 +61,43 @@ const props = withDefaults(
60
61
  {},
61
62
  );
62
63
 
64
+ const msg = useSaMessages('tenants');
65
+ const common = useSaMessages('common');
66
+ const { intlLocale } = useSuperAdminI18n();
67
+
63
68
  const rows = ref<SubscriptionRow[]>([]);
64
69
  const loading = ref(false);
65
70
 
66
- const defaultColumns: Column[] = [
71
+ const defaultColumns = computed<Column[]>(() => [
67
72
  {
68
73
  name: 'tenant',
69
- label: 'Mandant',
74
+ label: msg.value.tenant,
70
75
  field: (r) => r.tenant?.name ?? r.tenant?.slug ?? r.tenantSlug ?? '—',
71
76
  align: 'left',
72
77
  sortable: true,
73
78
  },
74
79
  {
75
80
  name: 'plan',
76
- label: 'Plan',
81
+ label: msg.value.plan,
77
82
  field: (r) => r.plan ?? r.planId ?? '—',
78
83
  align: 'left',
79
84
  },
80
85
  {
81
86
  name: 'status',
82
- label: 'Status',
87
+ label: common.value.status,
83
88
  field: (r) => r.status ?? '—',
84
89
  align: 'left',
85
90
  },
86
91
  {
87
92
  name: 'periodEndsAt',
88
- label: 'Endet',
93
+ label: msg.value.subscriptions.columnEndsAt,
89
94
  field: (r) => formatDate(r.periodEndsAt) ?? '∞',
90
95
  align: 'left',
91
96
  },
92
- ];
97
+ ]);
93
98
 
94
99
  const effectiveColumns = computed<Column[]>(() =>
95
- props.columns ? [...props.columns] : defaultColumns,
100
+ props.columns ? [...props.columns] : defaultColumns.value,
96
101
  );
97
102
 
98
103
  async function reload() {
@@ -112,7 +117,7 @@ onMounted(reload);
112
117
  function formatDate(iso: string | null | undefined): string | null {
113
118
  if (!iso) return null;
114
119
  try {
115
- return new Date(iso).toLocaleDateString('de-DE', {
120
+ return new Date(iso).toLocaleDateString(intlLocale.value, {
116
121
  day: '2-digit',
117
122
  month: 'short',
118
123
  year: 'numeric',
@@ -20,30 +20,32 @@
20
20
  </div>
21
21
  </div>
22
22
 
23
- <h1 class="sa-login-title">Anmelden</h1>
23
+ <h1 class="sa-login-title">{{ msg.login.signIn }}</h1>
24
24
  <p v-if="subtitle" class="sa-login-subtitle">{{ subtitle }}</p>
25
25
 
26
26
  <q-form @submit.prevent="handleSubmit" class="sa-login-form">
27
27
  <q-input
28
28
  v-model="form.email"
29
- label="E-Mail"
29
+ :label="msg.emailLabel"
30
30
  type="email"
31
+ autocomplete="username"
31
32
  outlined
32
33
  dense
33
34
  autofocus
34
35
  :disable="loading"
35
36
  class="q-mb-sm"
36
- :rules="[(v: string) => /\S+@\S+\.\S+/.test(v) || 'Bitte gültige E-Mail']"
37
+ :rules="[(v: string) => /\S+@\S+\.\S+/.test(v) || msg.invalidEmail]"
37
38
  />
38
39
  <q-input
39
40
  v-model="form.password"
40
- label="Passwort"
41
+ :label="msg.passwordLabel"
41
42
  :type="showPw ? 'text' : 'password'"
43
+ autocomplete="current-password"
42
44
  outlined
43
45
  dense
44
46
  :disable="loading"
45
47
  class="q-mb-sm"
46
- :rules="[(v: string) => v.length > 0 || 'Passwort ist Pflicht']"
48
+ :rules="[(v: string) => v.length > 0 || msg.login.passwordRequired]"
47
49
  >
48
50
  <template #append>
49
51
  <q-icon
@@ -62,7 +64,7 @@
62
64
  unelevated
63
65
  color="primary"
64
66
  icon="login"
65
- label="Anmelden"
67
+ :label="msg.login.signIn"
66
68
  :loading="loading"
67
69
  :disable="!canSubmit"
68
70
  class="full-width q-mt-sm"
@@ -71,7 +73,8 @@
71
73
  </q-form>
72
74
 
73
75
  <div v-if="devHint" class="sa-login-hint">
74
- Test-Account: <code>{{ devHint.email }}</code> / <code>{{ devHint.password }}</code>
76
+ {{ msg.login.devHintLabel }} <code>{{ devHint.email }}</code> /
77
+ <code>{{ devHint.password }}</code>
75
78
  </div>
76
79
  </div>
77
80
 
@@ -94,6 +97,7 @@ import {
94
97
  useSuperAdminLoginAdapter,
95
98
  } from '../vue/use-super-admin-context.js';
96
99
  import { getJson } from '../client/http-json.js';
100
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
97
101
  import SuperAdminSetupWizard from './SuperAdminSetupWizard.vue';
98
102
 
99
103
  interface Props {
@@ -107,6 +111,7 @@ interface Props {
107
111
  const props = defineProps<Props>();
108
112
 
109
113
  const router = useRouter();
114
+ const msg = useSaMessages('shell');
110
115
  const brand = useSuperAdminBrand();
111
116
  const endpoints = useSuperAdminEndpoints();
112
117
  const adapter = useSuperAdminLoginAdapter();
@@ -161,11 +166,9 @@ function describeError(
161
166
  result: Extract<Awaited<ReturnType<typeof adapter.login>>, { ok: false }>,
162
167
  ): string {
163
168
  if (result.message) return result.message;
164
- if (result.code === 'BAD_CREDENTIALS') return 'E-Mail oder Passwort falsch.';
165
- if (result.code === 'NOT_SUPER_ADMIN') {
166
- return 'Dieser Account hat keine SUPER_ADMIN-Rolle. Bitte Tenant-Frontend nutzen.';
167
- }
168
- return 'Anmeldung fehlgeschlagen.';
169
+ if (result.code === 'BAD_CREDENTIALS') return msg.value.login.errorBadCredentials;
170
+ if (result.code === 'NOT_SUPER_ADMIN') return msg.value.login.errorNotSuperAdmin;
171
+ return msg.value.login.errorFailed;
169
172
  }
170
173
 
171
174
  async function handleSubmit(): Promise<void> {
@@ -182,7 +185,7 @@ async function handleSubmit(): Promise<void> {
182
185
  } catch (err) {
183
186
  const e = err as { response?: { data?: { message?: string; code?: string } } };
184
187
  errorMessage.value =
185
- e.response?.data?.message ?? e.response?.data?.code ?? 'Anmeldung fehlgeschlagen.';
188
+ e.response?.data?.message ?? e.response?.data?.code ?? msg.value.login.errorFailed;
186
189
  } finally {
187
190
  loading.value = false;
188
191
  }
@@ -4,8 +4,8 @@
4
4
  <div class="sa-setup-head">
5
5
  <div class="sa-setup-badge">{{ iconText }}</div>
6
6
  <div>
7
- <div class="sa-setup-title">Erst­einrichtung</div>
8
- <div class="sa-setup-sub">{{ brandName }} · ersten SuperAdmin anlegen</div>
7
+ <div class="sa-setup-title">{{ msg.setup.title }}</div>
8
+ <div class="sa-setup-sub">{{ subtitleText }}</div>
9
9
  </div>
10
10
  </div>
11
11
 
@@ -16,40 +16,44 @@
16
16
  <!-- Step 1: create SuperAdmin -->
17
17
  <q-form v-if="step === 'form'" @submit.prevent="submitCreate" class="sa-setup-form">
18
18
  <p class="sa-setup-hint">
19
- Es existiert noch kein SuperAdmin. Gib das vom Betreiber gesetzte
20
- <code>SETUP_TOKEN</code> ein und lege den ersten Account an.
19
+ {{ msg.setup.tokenHintBefore }}
20
+ <code>SETUP_TOKEN</code>
21
+ {{ msg.setup.tokenHintAfter }}
21
22
  </p>
22
23
  <q-input
23
24
  v-model="form.token"
24
- label="Setup-Token"
25
+ :label="msg.setup.tokenLabel"
25
26
  type="password"
26
27
  outlined
27
28
  dense
28
29
  autofocus
29
30
  :disable="loading"
30
31
  class="q-mb-sm"
31
- :rules="[(v: string) => v.length > 0 || 'Setup-Token ist Pflicht']"
32
+ :rules="[(v: string) => v.length > 0 || msg.setup.tokenRequired]"
32
33
  />
33
34
  <q-input
34
35
  v-model="form.email"
35
- label="E-Mail"
36
+ :label="msg.emailLabel"
36
37
  type="email"
37
38
  outlined
38
39
  dense
39
40
  :disable="loading"
40
41
  class="q-mb-sm"
41
- :rules="[(v: string) => /\S+@\S+\.\S+/.test(v) || 'Bitte gültige E-Mail']"
42
+ :rules="[(v: string) => /\S+@\S+\.\S+/.test(v) || msg.invalidEmail]"
42
43
  />
43
44
  <q-input
44
45
  v-model="form.password"
45
- label="Passwort (leer = generieren)"
46
+ :label="msg.setup.passwordLabel"
46
47
  :type="showPw ? 'text' : 'password'"
47
48
  outlined
48
49
  dense
49
50
  :disable="loading"
50
- hint="Mindestens 8 Zeichen, oder leer lassen für ein generiertes Passwort."
51
+ :hint="msg.setup.passwordHint"
51
52
  class="q-mb-sm"
52
- :rules="[(v: string) => v.length === 0 || v.length >= 8 || 'Mindestens 8 Zeichen']"
53
+ :rules="[
54
+ (v: string) =>
55
+ v.length === 0 || v.length >= 8 || msg.setup.passwordMinLength,
56
+ ]"
53
57
  >
54
58
  <template #append>
55
59
  <q-icon
@@ -63,7 +67,7 @@
63
67
  unelevated
64
68
  color="primary"
65
69
  icon="person_add"
66
- label="SuperAdmin anlegen"
70
+ :label="msg.setup.submit"
67
71
  :loading="loading"
68
72
  :disable="!canCreate"
69
73
  class="full-width q-mt-sm"
@@ -74,48 +78,48 @@
74
78
  <!-- Step 2: set up MFA -->
75
79
  <div v-else-if="step === 'mfa' && result" class="sa-setup-mfa">
76
80
  <p class="sa-setup-hint">
77
- Account <strong>{{ result.email }}</strong> angelegt. Richte jetzt die
78
- Zwei-Faktor-Authentisierung (TOTP) ein.
81
+ {{ msg.setup.mfaIntroBefore }} <strong>{{ result.email }}</strong>
82
+ {{ msg.setup.mfaIntroAfter }}
79
83
  </p>
80
84
 
81
85
  <div v-if="result.generatedPassword" class="sa-setup-secret">
82
- <div class="sa-setup-secret__label">Generiertes Passwort (einmalig sichern)</div>
86
+ <div class="sa-setup-secret__label">
87
+ {{ msg.setup.generatedPasswordLabel }}
88
+ </div>
83
89
  <code class="sa-setup-secret__value">{{ result.generatedPassword }}</code>
84
90
  </div>
85
91
 
86
92
  <div v-if="result.qrDataUrl" class="sa-setup-qr">
87
- <img :src="result.qrDataUrl" alt="MFA-QR-Code" width="200" height="200" />
88
- <div class="sa-setup-qr__hint">Mit der Authenticator-App scannen</div>
93
+ <img :src="result.qrDataUrl" :alt="msg.setup.qrAlt" width="200" height="200" />
94
+ <div class="sa-setup-qr__hint">{{ msg.setup.qrHint }}</div>
89
95
  </div>
90
96
 
91
97
  <div class="sa-setup-secret">
92
- <div class="sa-setup-secret__label">
93
- In Authenticator-App als „Schlüssel manuell eingeben"
94
- </div>
98
+ <div class="sa-setup-secret__label">{{ msg.setup.manualSecretLabel }}</div>
95
99
  <code class="sa-setup-secret__value">{{ result.secret }}</code>
96
100
  </div>
97
101
  <details class="sa-setup-uri">
98
- <summary>otpauth-URI (für QR-Generator)</summary>
102
+ <summary>{{ msg.setup.otpauthSummary }}</summary>
99
103
  <code class="sa-setup-uri__value">{{ result.otpauthUri }}</code>
100
104
  </details>
101
105
 
102
106
  <q-form @submit.prevent="submitConfirm" class="sa-setup-form q-mt-md">
103
107
  <q-input
104
108
  v-model="mfaCode"
105
- label="6-stelliger Code aus der App"
109
+ :label="msg.setup.codeLabel"
106
110
  outlined
107
111
  dense
108
112
  autofocus
109
113
  inputmode="numeric"
110
114
  maxlength="6"
111
115
  :disable="loading"
112
- :rules="[(v: string) => /^\d{6}$/.test(v) || '6-stelliger Code']"
116
+ :rules="[(v: string) => /^\d{6}$/.test(v) || msg.setup.codeInvalid]"
113
117
  />
114
118
  <q-btn
115
119
  unelevated
116
120
  color="primary"
117
121
  icon="verified_user"
118
- label="MFA bestätigen"
122
+ :label="msg.setup.confirmMfa"
119
123
  :loading="loading"
120
124
  :disable="!/^\d{6}$/.test(mfaCode) || loading"
121
125
  class="full-width q-mt-sm"
@@ -123,21 +127,19 @@
123
127
  />
124
128
  </q-form>
125
129
  <button class="sa-setup-skip" type="button" @click="step = 'done'">
126
- Überspringen Code beim ersten Login eingeben
130
+ {{ msg.setup.skip }}
127
131
  </button>
128
132
  </div>
129
133
 
130
134
  <!-- Step 3: done -->
131
135
  <div v-else-if="step === 'done'" class="sa-setup-done">
132
136
  <q-icon name="check_circle" color="positive" size="48px" />
133
- <p class="sa-setup-hint">
134
- Einrichtung abgeschlossen. Melde dich jetzt mit dem neuen SuperAdmin an.
135
- </p>
137
+ <p class="sa-setup-hint">{{ msg.setup.doneHint }}</p>
136
138
  <q-btn
137
139
  unelevated
138
140
  color="primary"
139
141
  icon="login"
140
- label="Zum Login"
142
+ :label="msg.setup.toLogin"
141
143
  class="full-width"
142
144
  @click="emit('done')"
143
145
  />
@@ -148,11 +150,7 @@
148
150
 
149
151
  <script setup lang="ts">
150
152
  import { computed, reactive, ref } from 'vue';
151
- import {
152
- SETUP_ERROR_CODES,
153
- type SetupConfirmMfaResponse,
154
- type SetupResult,
155
- } from '@saasicat/types';
153
+ import { SETUP_ERROR_CODES, type SetupConfirmMfaResponse, type SetupResult } from '@saasicat/types';
156
154
 
157
155
  import {
158
156
  useSuperAdminBrand,
@@ -160,6 +158,8 @@ import {
160
158
  useSuperAdminHttp,
161
159
  } from '../vue/use-super-admin-context.js';
162
160
  import { HttpJsonError, postJson as httpPostJson } from '../client/http-json.js';
161
+ import { formatMessage } from '../client/i18n/format.js';
162
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
163
163
 
164
164
  interface Props {
165
165
  /** Display name + badge abbreviation (override the app branding, e.g. from PublicBoot). */
@@ -172,12 +172,16 @@ const props = defineProps<Props>();
172
172
  // shown again — the freshly created SuperAdmin then signs in.
173
173
  const emit = defineEmits<{ done: [] }>();
174
174
 
175
+ const msg = useSaMessages('shell');
175
176
  const brand = useSuperAdminBrand();
176
177
  const endpoints = useSuperAdminEndpoints();
177
178
  const http = useSuperAdminHttp();
178
179
 
179
180
  const brandName = computed(() => props.displayName ?? brand.name);
180
181
  const iconText = computed(() => props.icon ?? brand.logoText);
182
+ const subtitleText = computed(() =>
183
+ formatMessage(msg.value.setup.subtitle, { brand: brandName.value }),
184
+ );
181
185
 
182
186
  const step = ref<'form' | 'mfa' | 'done'>('form');
183
187
  const form = reactive({ token: '', email: '', password: '' });
@@ -195,15 +199,13 @@ const canCreate = computed(
195
199
  !loading.value,
196
200
  );
197
201
 
198
- const ERROR_BY_CODE: Record<string, string> = {
199
- [SETUP_ERROR_CODES.SETUP_DISABLED]: 'Setup ist serverseitig deaktiviert (SETUP_TOKEN nicht gesetzt).',
200
- [SETUP_ERROR_CODES.INVALID_SETUP_TOKEN]: 'Setup-Token ungültig.',
201
- [SETUP_ERROR_CODES.SETUP_ALREADY_DONE]:
202
- 'Es existiert bereits ein SuperAdmin — Setup ist abgeschlossen.',
203
- [SETUP_ERROR_CODES.INVALID_EMAIL]: 'Ungültige E-Mail-Adresse.',
204
- [SETUP_ERROR_CODES.EMAIL_EXISTS]:
205
- 'Diese E-Mail ist bereits vergeben. Wähle eine andere oder hebe den bestehenden User per CLI/DB zum SUPER_ADMIN an.',
206
- };
202
+ const errorByCode = computed<Record<string, string>>(() => ({
203
+ [SETUP_ERROR_CODES.SETUP_DISABLED]: msg.value.setup.errorSetupDisabled,
204
+ [SETUP_ERROR_CODES.INVALID_SETUP_TOKEN]: msg.value.setup.errorInvalidToken,
205
+ [SETUP_ERROR_CODES.SETUP_ALREADY_DONE]: msg.value.setup.errorAlreadyDone,
206
+ [SETUP_ERROR_CODES.INVALID_EMAIL]: msg.value.setup.errorInvalidEmail,
207
+ [SETUP_ERROR_CODES.EMAIL_EXISTS]: msg.value.setup.errorEmailExists,
208
+ }));
207
209
 
208
210
  // Goes through the injected HttpClient (consumer's auth/baseURL apply);
209
211
  // maps the error code from the body to a readable message.
@@ -213,7 +215,8 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
213
215
  } catch (err) {
214
216
  if (err instanceof HttpJsonError) {
215
217
  throw new Error(
216
- (err.code && ERROR_BY_CODE[err.code]) || `Fehler (HTTP ${err.status}).`,
218
+ (err.code && errorByCode.value[err.code]) ||
219
+ formatMessage(msg.value.setup.errorHttp, { status: err.status }),
217
220
  );
218
221
  }
219
222
  throw err;
@@ -232,7 +235,7 @@ async function submitCreate(): Promise<void> {
232
235
  });
233
236
  step.value = 'mfa';
234
237
  } catch (err) {
235
- errorMessage.value = err instanceof Error ? err.message : 'Setup fehlgeschlagen.';
238
+ errorMessage.value = err instanceof Error ? err.message : msg.value.setup.errorFailed;
236
239
  } finally {
237
240
  loading.value = false;
238
241
  }
@@ -251,10 +254,11 @@ async function submitConfirm(): Promise<void> {
251
254
  if (res.ok) {
252
255
  step.value = 'done';
253
256
  } else {
254
- errorMessage.value = 'Code ungültig — bitte erneut versuchen.';
257
+ errorMessage.value = msg.value.setup.errorCodeInvalid;
255
258
  }
256
259
  } catch (err) {
257
- errorMessage.value = err instanceof Error ? err.message : 'Bestätigung fehlgeschlagen.';
260
+ errorMessage.value =
261
+ err instanceof Error ? err.message : msg.value.setup.errorConfirmFailed;
258
262
  } finally {
259
263
  loading.value = false;
260
264
  }