@saasicat/ui-vue 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +64 -4
  2. package/dist/chunk-A3W2N26K.js +714 -0
  3. package/dist/chunk-QZOSDCNP.js +98 -0
  4. package/dist/chunk-X5SQ5HMB.js +3231 -0
  5. package/dist/client/index.cjs +3988 -0
  6. package/dist/client/index.d.cts +321 -0
  7. package/dist/client/index.d.ts +321 -0
  8. package/dist/client/index.js +70 -0
  9. package/dist/index.cjs +4446 -1249
  10. package/dist/index.d.cts +297 -888
  11. package/dist/index.d.ts +297 -888
  12. package/dist/index.js +609 -1348
  13. package/dist/messages-7b0P8W75.d.cts +1652 -0
  14. package/dist/messages-7b0P8W75.d.ts +1652 -0
  15. package/dist/quasar/index.cjs +3340 -0
  16. package/dist/quasar/index.d.cts +95 -0
  17. package/dist/quasar/index.d.ts +95 -0
  18. package/dist/quasar/index.js +96 -0
  19. package/dist/testing-e2e/admin-pages-suite.cjs +14 -11
  20. package/dist/testing-e2e/admin-pages-suite.js +14 -11
  21. package/dist/use-super-admin-i18n-B3v3-SWZ.d.ts +210 -0
  22. package/dist/use-super-admin-i18n-DMktRVEt.d.cts +210 -0
  23. package/package.json +23 -3
  24. package/src/{action-registry.ts → client/action-registry.ts} +4 -4
  25. package/src/{batch-column-fetcher.ts → client/batch-column-fetcher.ts} +4 -4
  26. package/src/{boot-loader.ts → client/boot-loader.ts} +3 -3
  27. package/src/client/i18n/currency.ts +42 -0
  28. package/src/client/i18n/define.ts +59 -0
  29. package/src/client/i18n/format.ts +12 -0
  30. package/src/client/i18n/index.ts +14 -0
  31. package/src/client/i18n/locale.ts +15 -0
  32. package/src/client/i18n/messages/audit.ts +42 -0
  33. package/src/client/i18n/messages/bundles.ts +459 -0
  34. package/src/client/i18n/messages/common.ts +147 -0
  35. package/src/client/i18n/messages/dashboard.ts +16 -0
  36. package/src/client/i18n/messages/discovery.ts +229 -0
  37. package/src/client/i18n/messages/email.ts +125 -0
  38. package/src/client/i18n/messages/marketing.ts +270 -0
  39. package/src/client/i18n/messages/nav.ts +72 -0
  40. package/src/client/i18n/messages/pilots.ts +192 -0
  41. package/src/client/i18n/messages/plan-detail.ts +266 -0
  42. package/src/client/i18n/messages/plan-editor.ts +177 -0
  43. package/src/client/i18n/messages/plan-versions.ts +251 -0
  44. package/src/client/i18n/messages/plans.ts +503 -0
  45. package/src/client/i18n/messages/promos.ts +202 -0
  46. package/src/client/i18n/messages/shell.ts +145 -0
  47. package/src/client/i18n/messages/tenants.ts +104 -0
  48. package/src/client/i18n/messages/users.ts +78 -0
  49. package/src/client/i18n/messages.ts +84 -0
  50. package/src/client/index.ts +27 -0
  51. package/src/{manifest-loader.ts → client/manifest-loader.ts} +4 -4
  52. package/src/{nav-builder.ts → client/nav-builder.ts} +44 -51
  53. package/src/{plan-versions-catalog.ts → client/plan-versions-catalog.ts} +42 -30
  54. package/src/components/BundleVersionPublishDialog.vue +35 -24
  55. package/src/components/FeatureGate.vue +1 -1
  56. package/src/components/MarketingPromotionsTab.vue +112 -59
  57. package/src/components/MfaPromptDialog.vue +11 -11
  58. package/src/components/TenantActionConfirmDialog.vue +19 -17
  59. package/src/components/VersionDiffPreview.vue +72 -43
  60. package/src/components/bundle-editor/BundleCreatePanel.vue +76 -63
  61. package/src/components/bundle-editor/BundleFeaturesEditor.vue +13 -6
  62. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +19 -14
  63. package/src/components/bundle-editor/BundleQuotasEditor.vue +7 -4
  64. package/src/components/bundle-editor/BundleStatusBanner.vue +36 -19
  65. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +51 -35
  66. package/src/components/bundle-editor/BundleVersionStrip.vue +24 -12
  67. package/src/components/bundle-editor/bundle-version-status.ts +29 -29
  68. package/src/components/dialogs/PilotCreateDialog.vue +58 -63
  69. package/src/components/dialogs/PilotEditDialog.vue +40 -34
  70. package/src/components/dialogs/PromoCodeCreateDialog.vue +89 -63
  71. package/src/components/dialogs/PromoCodeEditDialog.vue +98 -66
  72. package/src/components/dialogs/types.ts +3 -13
  73. package/src/components/plan/PlanCycleToggle.vue +21 -10
  74. package/src/components/plan/PlanGrid.vue +2 -2
  75. package/src/components/plan/PriceSummary.vue +1 -1
  76. package/src/components/plan/PromoCodeInput.vue +1 -1
  77. package/src/components/plan/PublicBundleGrid.vue +1 -1
  78. package/src/components/plan-cockpit/PlanCockpit.vue +34 -15
  79. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +13 -6
  80. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +18 -9
  81. package/src/components/plan-cockpit/PlanCockpitHeader.vue +9 -6
  82. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +24 -11
  83. package/src/components/plan-cockpit/PlanCockpitKpis.vue +28 -17
  84. package/src/components/plan-cockpit/PlanCockpitVersions.vue +50 -25
  85. package/src/components/plan-create-dialog/PlanCreateDialog.vue +45 -35
  86. package/src/components/plan-detail/PlanAuditLog.vue +11 -6
  87. package/src/components/plan-detail/PlanDetail.vue +21 -23
  88. package/src/components/plan-detail/PlanDetailHeader.vue +20 -11
  89. package/src/components/plan-detail/PlanDetailKpis.vue +30 -17
  90. package/src/components/plan-detail/PlanTerminateDialog.vue +22 -9
  91. package/src/components/plan-detail/PlanVersionDiffPanel.vue +61 -27
  92. package/src/components/plan-detail/PlanVersionsPanel.vue +71 -36
  93. package/src/components/plan-list/PlanList.vue +99 -57
  94. package/src/components/plan-matrix/PlanMatrix.vue +85 -38
  95. package/src/components/plan-review/PlanReview.vue +105 -80
  96. package/src/components/plan-version-editor/PlanCatalogPreview.vue +43 -20
  97. package/src/components/plan-version-editor/PlanComponentPool.vue +15 -7
  98. package/src/components/plan-version-editor/PlanVersionBasket.vue +45 -24
  99. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +8 -7
  100. package/src/components/plan-version-editor/PlanVersionEditor.vue +74 -40
  101. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +24 -12
  102. package/src/index.ts +82 -79
  103. package/src/pages-standard/AdminLayout.vue +43 -18
  104. package/src/pages-standard/AdminManifestErrorPage.vue +10 -10
  105. package/src/pages-standard/AuditPage.vue +58 -19
  106. package/src/pages-standard/BundlesPage.vue +29 -18
  107. package/src/pages-standard/DashboardPage.vue +31 -17
  108. package/src/pages-standard/DiscoveryPage.vue +40 -28
  109. package/src/pages-standard/EmailHistoryPage.vue +98 -69
  110. package/src/pages-standard/MarketingCatalogPage.vue +40 -21
  111. package/src/pages-standard/PilotsPage.vue +64 -52
  112. package/src/pages-standard/PlanVersionsPage.vue +9 -5
  113. package/src/pages-standard/PlansPage.vue +91 -59
  114. package/src/pages-standard/PlatformEmailPage.vue +120 -53
  115. package/src/pages-standard/PromoCodeDetailPage.vue +66 -63
  116. package/src/pages-standard/PromoCodesPage.vue +51 -54
  117. package/src/pages-standard/SubscriptionsPage.vue +14 -9
  118. package/src/pages-standard/SuperAdminLoginPage.vue +19 -16
  119. package/src/pages-standard/SuperAdminSetupWizard.vue +53 -49
  120. package/src/pages-standard/TenantDetailPage.vue +54 -40
  121. package/src/pages-standard/TenantsPage.vue +57 -44
  122. package/src/pages-standard/UsersPage.vue +70 -52
  123. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -8
  124. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +28 -18
  125. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +4 -1
  126. package/src/pages-standard/bundles-page/BundlesHeader.vue +9 -8
  127. package/src/pages-standard/bundles-page/BundlesKpis.vue +29 -11
  128. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +20 -8
  129. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -3
  130. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +43 -24
  131. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +7 -4
  132. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +28 -15
  133. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +20 -10
  134. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +9 -6
  135. package/src/pages-standard/discovery-page/discovery-ui.ts +40 -52
  136. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +81 -49
  137. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +13 -10
  138. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +24 -20
  139. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +6 -3
  140. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -6
  141. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +42 -26
  142. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +46 -27
  143. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +8 -6
  144. package/src/pages-standard/plan-versions/PlanVersionsList.vue +40 -17
  145. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +1 -1
  146. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +25 -13
  147. package/src/pages-standard/plan-versions/format.ts +33 -17
  148. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +12 -8
  149. package/src/pages-standard/plans-page/PlanBundleOverview.vue +15 -6
  150. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +18 -7
  151. package/src/pages-standard/plans-page/PlanPublishDialog.vue +23 -12
  152. package/src/pages-standard/tenants/format.ts +3 -2
  153. package/src/pages-tenant/MySubscriptionBundlesPage.vue +73 -38
  154. package/src/pages-tenant/OnboardingConfigurator.vue +3 -3
  155. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  156. package/src/pages-tenant/PlanChangeWizard.vue +10 -6
  157. package/src/pages-tenant/TenantPlanSection.vue +20 -16
  158. package/src/pages-tenant/default-i18n.ts +217 -1
  159. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +9 -7
  160. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +11 -12
  161. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +4 -1
  162. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +1 -1
  163. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +1 -1
  164. package/src/quasar/create-super-admin-app.ts +192 -0
  165. package/src/quasar/index.ts +11 -0
  166. package/src/quasar/notify.ts +30 -0
  167. package/src/testing-e2e/admin-pages-suite.ts +14 -11
  168. package/src/{manifest-store-factory.ts → vue/manifest-store-factory.ts} +1 -1
  169. package/src/{platform-loaders.ts → vue/platform-loaders.ts} +4 -4
  170. package/src/{project-page-host.ts → vue/project-page-host.ts} +6 -4
  171. package/src/{create-super-admin-app.ts → vue/super-admin-context.ts} +20 -139
  172. package/src/vue/ui-notify.ts +30 -0
  173. package/src/{use-actions.ts → vue/use-actions.ts} +1 -1
  174. package/src/{use-api-list.ts → vue/use-api-list.ts} +1 -1
  175. package/src/{use-audit-entries.ts → vue/use-audit-entries.ts} +3 -3
  176. package/src/{use-batch-columns.ts → vue/use-batch-columns.ts} +1 -1
  177. package/src/{use-bulk-publish.ts → vue/use-bulk-publish.ts} +7 -5
  178. package/src/{use-bundle-versions-map.ts → vue/use-bundle-versions-map.ts} +7 -5
  179. package/src/{use-bundles.ts → vue/use-bundles.ts} +14 -14
  180. package/src/{use-catalog-entries.ts → vue/use-catalog-entries.ts} +14 -11
  181. package/src/{use-discovery.ts → vue/use-discovery.ts} +9 -6
  182. package/src/{use-entitlement.ts → vue/use-entitlement.ts} +4 -4
  183. package/src/{use-live-plan-versions.ts → vue/use-live-plan-versions.ts} +5 -5
  184. package/src/{use-manifest.ts → vue/use-manifest.ts} +1 -1
  185. package/src/{use-marketing-projections.ts → vue/use-marketing-projections.ts} +10 -7
  186. package/src/{use-nav.ts → vue/use-nav.ts} +9 -3
  187. package/src/{use-plan-editor.ts → vue/use-plan-editor.ts} +2 -2
  188. package/src/{use-plan-versions.ts → vue/use-plan-versions.ts} +3 -3
  189. package/src/{use-plans.ts → vue/use-plans.ts} +13 -13
  190. package/src/{use-platform-tenant-actions.ts → vue/use-platform-tenant-actions.ts} +9 -6
  191. package/src/{use-promotions.ts → vue/use-promotions.ts} +10 -11
  192. package/src/{use-public-boot.ts → vue/use-public-boot.ts} +1 -1
  193. package/src/{use-subscription-draft.ts → vue/use-subscription-draft.ts} +2 -3
  194. package/src/{use-super-admin-context.ts → vue/use-super-admin-context.ts} +5 -5
  195. package/src/vue/use-super-admin-i18n.ts +82 -0
  196. package/src/{use-tenant-action-flow.ts → vue/use-tenant-action-flow.ts} +12 -7
  197. package/src/{use-tenant-billing-catalog.ts → vue/use-tenant-billing-catalog.ts} +2 -2
  198. package/src/{use-tenant-billing.ts → vue/use-tenant-billing.ts} +2 -2
  199. package/src/{use-tenant-manifest.ts → vue/use-tenant-manifest.ts} +2 -4
  200. package/src/{use-tenant-subscription-bundles.ts → vue/use-tenant-subscription-bundles.ts} +5 -5
  201. package/src/{use-tenants.ts → vue/use-tenants.ts} +3 -3
  202. package/dist/testing-e2e/admin-pages-suite.d.cts +0 -37
  203. package/dist/testing-e2e/admin-pages-suite.d.ts +0 -37
  204. package/src/components/BusinessTypeVersionEditorDialog.vue +0 -417
  205. package/src/components/BusinessTypeVersionPublishDialog.vue +0 -258
  206. package/src/pages-standard/BusinessTypesPage.vue +0 -662
  207. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +0 -167
  208. package/src/use-business-types.ts +0 -269
  209. /package/src/{http-json.ts → client/http-json.ts} +0 -0
  210. /package/src/{types.ts → client/types.ts} +0 -0
  211. /package/src/{version.ts → client/version.ts} +0 -0
  212. /package/src/{create-admin-routes.ts → vue/create-admin-routes.ts} +0 -0
  213. /package/src/{entitlement-provider.ts → vue/entitlement-provider.ts} +0 -0
  214. /package/src/{feature-router-guard.ts → vue/feature-router-guard.ts} +0 -0
@@ -2,7 +2,7 @@
2
2
  <div class="sa-tenants">
3
3
  <header class="sa-tenants__head">
4
4
  <div>
5
- <h1 class="sa-tenants__title">Mandanten</h1>
5
+ <h1 class="sa-tenants__title">{{ msg.list.title }}</h1>
6
6
  <p v-if="subtitle" class="sa-tenants__sub">{{ subtitle }}</p>
7
7
  </div>
8
8
  </header>
@@ -12,7 +12,7 @@
12
12
  <q-icon name="search" size="15px" />
13
13
  <input
14
14
  v-model="searchInput"
15
- placeholder="Slug oder Name …"
15
+ :placeholder="msg.list.searchPlaceholder"
16
16
  @input="reloadDebounced"
17
17
  />
18
18
  <button v-if="searchInput" class="sa-tenants__clear" @click="onClearSearch">
@@ -20,8 +20,8 @@
20
20
  </button>
21
21
  </div>
22
22
  <select v-model="statusFilter" class="sa-tenants__select" @change="applyFilter">
23
- <option :value="null">Alle Aktiv-Status</option>
24
- <option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">
23
+ <option :value="null">{{ msg.list.allStatuses }}</option>
24
+ <option v-for="opt in resolvedStatusOptions" :key="opt.value" :value="opt.value">
25
25
  {{ opt.label }}
26
26
  </option>
27
27
  </select>
@@ -31,14 +31,14 @@
31
31
  class="sa-tenants__select"
32
32
  @change="applyFilter"
33
33
  >
34
- <option :value="null">{{ planFilterLabel }}</option>
34
+ <option :value="null">{{ resolvedPlanFilterLabel }}</option>
35
35
  <option v-for="p in planOptions" :key="p" :value="p">{{ p }}</option>
36
36
  </select>
37
37
  <slot name="filters-extra" />
38
38
  </div>
39
39
 
40
40
  <q-banner v-if="error" class="bg-red-1 text-red-9 q-mb-md" rounded>
41
- <strong>Fehler:</strong> {{ error.message }}
41
+ <strong>{{ common.error }}:</strong> {{ error.message }}
42
42
  </q-banner>
43
43
 
44
44
  <div class="sa-tenants__card">
@@ -46,11 +46,13 @@
46
46
  <table>
47
47
  <thead>
48
48
  <tr>
49
- <th>Mandant</th>
50
- <th v-if="showPlanColumn">{{ planColumnLabel }}</th>
51
- <th>Status</th>
52
- <th v-if="usageFields.length > 0" class="num">Verbrauch</th>
53
- <th class="num">Angelegt</th>
49
+ <th>{{ msg.tenant }}</th>
50
+ <th v-if="showPlanColumn">{{ resolvedPlanColumnLabel }}</th>
51
+ <th>{{ common.status }}</th>
52
+ <th v-if="usageFields.length > 0" class="num">
53
+ {{ msg.list.columnUsage }}
54
+ </th>
55
+ <th class="num">{{ msg.list.columnCreatedAt }}</th>
54
56
  <th v-if="hasActions" class="num"></th>
55
57
  </tr>
56
58
  </thead>
@@ -100,7 +102,7 @@
100
102
  </div>
101
103
  </td>
102
104
  <td class="num sa-tenants__mono">
103
- {{ formatDateDe(usageStr(row, 'createdAt')) }}
105
+ {{ formatCreatedAt(row) }}
104
106
  </td>
105
107
  <td v-if="hasActions" class="num sa-tenants__actions">
106
108
  <slot name="row-actions" :row="row" :actions="visibleActions(row)">
@@ -127,17 +129,16 @@
127
129
  </table>
128
130
  <div v-if="items.length === 0 && !loading" class="sa-tenants__empty">
129
131
  <q-icon name="search_off" size="32px" />
130
- <div>Keine Mandanten mit diesen Filtern.</div>
132
+ <div>{{ msg.list.empty }}</div>
131
133
  </div>
132
134
  <div v-if="loading" class="sa-tenants__loading">
133
- <q-spinner size="20px" /> wird geladen…
135
+ <q-spinner size="20px" /> {{ common.loading }}
134
136
  </div>
135
137
  </div>
136
138
  </div>
137
139
 
138
140
  <footer class="sa-tenants__foot">
139
- {{ total }} Mandanten · Seite {{ page }} /
140
- {{ Math.max(1, Math.ceil(total / pageSize)) }}
141
+ {{ paginationLabel }}
141
142
  </footer>
142
143
 
143
144
  <!-- Manifest-Driven Action Flow: only mounted when the `manifest` prop
@@ -166,27 +167,19 @@
166
167
 
167
168
  <script setup lang="ts">
168
169
  import { computed, ref, watch } from 'vue';
169
- import type {
170
- AdminManifest,
171
- TenantActionDef,
172
- TenantDto,
173
- TenantListFilter,
174
- } from '@saasicat/types';
175
- import type { HttpClient } from '../types.js';
176
- import { useTenants } from '../use-tenants.js';
170
+ import type { AdminManifest, TenantActionDef, TenantDto, TenantListFilter } from '@saasicat/types';
171
+ import type { HttpClient } from '../client/types.js';
172
+ import { formatMessage } from '../client/i18n/format.js';
173
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
174
+ import { useTenants } from '../vue/use-tenants.js';
177
175
  import {
178
176
  usePlatformTenantActions,
179
177
  type PlatformTenantActionTone,
180
- } from '../use-platform-tenant-actions.js';
178
+ } from '../vue/use-platform-tenant-actions.js';
181
179
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
182
180
  import TenantActionConfirmDialog from '../components/TenantActionConfirmDialog.vue';
183
181
  import StatusPill, { type PillTone } from './tenants/StatusPill.vue';
184
- import {
185
- DEFAULT_PLAN_ACCENTS,
186
- formatDateDe,
187
- planAccent,
188
- tenantInitials,
189
- } from './tenants/format.js';
182
+ import { DEFAULT_PLAN_ACCENTS, formatDate, planAccent, tenantInitials } from './tenants/format.js';
190
183
 
191
184
  // Platform standard page: tenant list.
192
185
  //
@@ -196,7 +189,7 @@ import {
196
189
  //
197
190
  // App-specific bits via props:
198
191
  // - `subtitle` : header subtitle
199
- // - `statusOptions` : filter dropdown values (default: Aktiv/Suspendiert)
192
+ // - `statusOptions` : filter dropdown values (default: active/suspended)
200
193
  // - `planOptions` : filter dropdown values for plan; omitted = hides dropdown
201
194
  // - `planAccents` : plan id → accent color (avatar + plan dot)
202
195
  // - `planLabelField` : field for the plan display (e.g. "plan" or "bundleKey")
@@ -331,12 +324,6 @@ const props = withDefaults(
331
324
  }>(),
332
325
  {
333
326
  pageSize: 25,
334
- statusOptions: () => [
335
- { value: 'ACTIVE', label: 'Aktiv' },
336
- { value: 'INACTIVE', label: 'Suspendiert' },
337
- ],
338
- planFilterLabel: 'Alle Pläne',
339
- planColumnLabel: 'Plan',
340
327
  showPlanColumn: true,
341
328
  planLabelField: 'plan',
342
329
  planAccents: () => DEFAULT_PLAN_ACCENTS,
@@ -345,6 +332,20 @@ const props = withDefaults(
345
332
  },
346
333
  );
347
334
 
335
+ const msg = useSaMessages('tenants');
336
+ const common = useSaMessages('common');
337
+ const { intlLocale } = useSuperAdminI18n();
338
+
339
+ const resolvedStatusOptions = computed<ReadonlyArray<{ value: string; label: string }>>(
340
+ () =>
341
+ props.statusOptions ?? [
342
+ { value: 'ACTIVE', label: common.value.active },
343
+ { value: 'INACTIVE', label: msg.value.list.suspended },
344
+ ],
345
+ );
346
+ const resolvedPlanFilterLabel = computed(() => props.planFilterLabel ?? msg.value.list.allPlans);
347
+ const resolvedPlanColumnLabel = computed(() => props.planColumnLabel ?? msg.value.plan);
348
+
348
349
  const filter = ref<TenantListFilter>({});
349
350
  const searchInput = ref('');
350
351
  const statusFilter = ref<string | null>(null);
@@ -360,6 +361,14 @@ const { items, page, total, loading, error, goToPage, setPageSize } = list;
360
361
 
361
362
  setPageSize(props.pageSize);
362
363
 
364
+ const paginationLabel = computed(() =>
365
+ formatMessage(msg.value.list.pagination, {
366
+ total: total.value,
367
+ page: page.value,
368
+ pages: Math.max(1, Math.ceil(total.value / props.pageSize)),
369
+ }),
370
+ );
371
+
363
372
  let searchTimer: ReturnType<typeof setTimeout> | null = null;
364
373
  function reloadDebounced(): void {
365
374
  if (searchTimer) clearTimeout(searchTimer);
@@ -427,7 +436,7 @@ if (manifestFlow && typeof window !== 'undefined') {
427
436
  if (orphans.length > 0) {
428
437
  // eslint-disable-next-line no-console
429
438
  console.warn(
430
- '[PlatformTenantsPage] Manifest-Actions ohne Handler in createSuperAdminApp({ actions }):',
439
+ '[PlatformTenantsPage] Manifest actions without a handler in createSuperAdminApp({ actions }):',
431
440
  orphans,
432
441
  );
433
442
  }
@@ -449,8 +458,8 @@ const combinedActions = computed<TenantRowAction[]>(() => {
449
458
  if (typeof window !== 'undefined') {
450
459
  // eslint-disable-next-line no-console
451
460
  console.warn(
452
- `[PlatformTenantsPage] Custom-Action "${a.id}" (actionKey="${a.actionKey}") ` +
453
- `kollidiert mit Manifest-ActionManifest gewinnt.`,
461
+ `[PlatformTenantsPage] Custom action "${a.id}" (actionKey="${a.actionKey}") ` +
462
+ `collides with a manifest action the manifest wins.`,
454
463
  );
455
464
  }
456
465
  return false;
@@ -488,14 +497,18 @@ function usageStr(row: TenantRow, key: string): string | undefined {
488
497
  return typeof v === 'string' ? v : undefined;
489
498
  }
490
499
 
500
+ function formatCreatedAt(row: TenantRow): string {
501
+ return formatDate(usageStr(row, 'createdAt'), intlLocale.value);
502
+ }
503
+
491
504
  function resolvedPills(row: TenantRow): StatusPillDef[] {
492
505
  if (props.pillsForRow) return props.pillsForRow(row);
493
- // Default: only Aktiv/Suspendiert.
506
+ // Default: only active/suspended.
494
507
  const isActive = (row as TenantDto).isActive;
495
508
  return [
496
509
  isActive
497
- ? { label: 'Aktiv', icon: 'check_circle', tone: 'positive' }
498
- : { label: 'Suspendiert', icon: 'block', tone: 'negative' },
510
+ ? { label: common.value.active, icon: 'check_circle', tone: 'positive' }
511
+ : { label: msg.value.list.suspended, icon: 'block', tone: 'negative' },
499
512
  ];
500
513
  }
501
514
 
@@ -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>
@@ -106,7 +112,10 @@
106
112
  <script setup lang="ts">
107
113
  import { computed, onMounted, reactive, ref } from 'vue';
108
114
  import { useQuasar } from 'quasar';
115
+ import { useSuperAdminNotify } from '../quasar/notify.js';
109
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';
110
119
 
111
120
  // Platform standard page: user search. Data-agnostic.
112
121
  //
@@ -165,13 +174,19 @@ const props = withDefaults(
165
174
  submitDeactivate?: (id: string, reason: string, mfaCode?: string) => Promise<void>;
166
175
  }>(),
167
176
  {
168
- title: 'Benutzer',
169
177
  requireMfaForResetPassword: false,
170
178
  requireMfaForDeactivate: false,
171
179
  },
172
180
  );
173
181
 
174
182
  const q = useQuasar();
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);
175
190
  const rows = ref<UserRow[]>([]);
176
191
  const loading = ref(false);
177
192
  const filter = reactive({ q: '', tenant: '' });
@@ -254,42 +269,42 @@ const statTiles = computed<
254
269
  if (isSuperAdmin(r)) supr++;
255
270
  }
256
271
  return [
257
- { id: 'all', label: 'Alle', count: rows.value.length },
258
- { id: 'active', label: 'Aktiv', count: active, tone: 'positive' },
259
- { id: 'blocked', label: 'Gesperrt', count: blocked, tone: 'muted' },
260
- { id: 'never', label: 'Nie eingeloggt', count: never, tone: 'warn' },
261
- { 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' },
262
277
  ];
263
278
  });
264
279
 
265
- const baseColumns = [
280
+ const baseColumns = computed(() => [
266
281
  {
267
282
  name: 'email',
268
- label: 'E-Mail',
283
+ label: msg.value.columns.email,
269
284
  field: 'email',
270
285
  align: 'left' as const,
271
286
  sortable: true,
272
287
  },
273
288
  {
274
289
  name: 'name',
275
- label: 'Name',
290
+ label: common.value.name,
276
291
  field: (r: UserRow) => `${r.firstName} ${r.lastName}`,
277
292
  align: 'left' as const,
278
293
  },
279
294
  {
280
295
  name: 'tenant',
281
- label: 'Tenant',
282
- field: (r: UserRow) => r.tenantSlug ?? '— (SuperAdmin)',
296
+ label: msg.value.columns.tenant,
297
+ field: (r: UserRow) => r.tenantSlug ?? msg.value.tenantFallback,
283
298
  align: 'left' as const,
284
299
  },
285
- { name: 'role', label: 'Rolle', field: 'role', align: 'left' as const },
286
- { 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 },
287
302
  {
288
303
  name: 'lastLogin',
289
- label: 'Letzter Login',
304
+ label: msg.value.columns.lastLogin,
290
305
  field: (r: UserRow) =>
291
306
  r.lastLoginAt
292
- ? new Date(r.lastLoginAt).toLocaleDateString('de-DE', {
307
+ ? new Date(r.lastLoginAt).toLocaleDateString(intlLocale.value, {
293
308
  day: '2-digit',
294
309
  month: 'short',
295
310
  year: 'numeric',
@@ -297,7 +312,7 @@ const baseColumns = [
297
312
  : '—',
298
313
  align: 'left' as const,
299
314
  },
300
- ];
315
+ ]);
301
316
 
302
317
  // Built-in default actions — APPENDED to consumer actions, not replaced.
303
318
  const bakedActions = computed<UserRowAction[]>(() => {
@@ -305,7 +320,7 @@ const bakedActions = computed<UserRowAction[]>(() => {
305
320
  if (props.enableResetPassword && props.submitResetPassword) {
306
321
  out.push({
307
322
  id: 'reset-password',
308
- label: 'Passwort zurücksetzen',
323
+ label: msg.value.resetPassword.action,
309
324
  icon: 'lock_reset',
310
325
  color: 'primary',
311
326
  handler: (row) => onResetPasswordClick(row),
@@ -314,7 +329,7 @@ const bakedActions = computed<UserRowAction[]>(() => {
314
329
  if (props.enableDeactivate && props.submitDeactivate) {
315
330
  out.push({
316
331
  id: 'deactivate',
317
- label: 'Deaktivieren',
332
+ label: msg.value.deactivate.action,
318
333
  icon: 'block',
319
334
  color: 'negative',
320
335
  condition: (row) => (row as UserRow & { status?: string }).status !== 'suspended',
@@ -330,7 +345,7 @@ const mergedActions = computed<readonly UserRowAction[]>(() => [
330
345
  ]);
331
346
 
332
347
  const effectiveColumns = computed(() => {
333
- const cols = [...baseColumns];
348
+ const cols = [...baseColumns.value];
334
349
  if (mergedActions.value.length > 0) {
335
350
  cols.push({
336
351
  name: 'actions',
@@ -369,7 +384,7 @@ function errMsg(err: unknown): string {
369
384
  return (
370
385
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
371
386
  (err as Error)?.message ??
372
- 'Aktion fehlgeschlagen'
387
+ msg.value.errorAction
373
388
  );
374
389
  }
375
390
 
@@ -387,7 +402,7 @@ async function runAction<R>(
387
402
  const result = await invoke('');
388
403
  onSuccess(result);
389
404
  } catch (err) {
390
- q.notify({ type: 'negative', message: errMsg(err), position: 'top' });
405
+ notify('negative', errMsg(err));
391
406
  }
392
407
  return;
393
408
  }
@@ -402,11 +417,11 @@ async function runAction<R>(
402
417
  } catch (err) {
403
418
  const status = (err as { response?: { status?: number } })?.response?.status;
404
419
  if (status === 401) {
405
- mfaError.value = 'TOTP-Code ungültig oder MFA nicht eingerichtet.';
420
+ mfaError.value = shell.value.mfa.invalidCode;
406
421
  continue;
407
422
  }
408
423
  showMfa.value = false;
409
- q.notify({ type: 'negative', message: errMsg(err), position: 'top' });
424
+ notify('negative', errMsg(err));
410
425
  return;
411
426
  }
412
427
  }
@@ -416,15 +431,18 @@ function onResetPasswordClick(row: UserRow): void {
416
431
  if (!props.submitResetPassword) return;
417
432
  const submit = props.submitResetPassword;
418
433
  q.dialog({
419
- title: `Passwort für "${row.email}" zurücksetzen`,
420
- message: 'Grund für Audit (Pflichtfeld):',
434
+ title: formatMessage(msg.value.resetPassword.dialogTitle, { email: row.email }),
435
+ message: msg.value.reasonPrompt,
421
436
  prompt: { model: '', type: 'text' },
422
- cancel: 'Abbrechen',
423
- ok: { label: 'Zurücksetzen', color: 'primary' },
437
+ cancel: common.value.cancel,
438
+ ok: { label: common.value.reset, color: 'primary' },
424
439
  }).onOk(async (reason: string) => {
425
440
  if (!reason || reason.trim().length === 0) return;
426
441
  await runAction(
427
- `Passwort für "${row.email}" zurücksetzen — ${reason.trim()}.`,
442
+ formatMessage(msg.value.resetPassword.mfaDescription, {
443
+ email: row.email,
444
+ reason: reason.trim(),
445
+ }),
428
446
  !!props.requireMfaForResetPassword,
429
447
  (code) => submit(row.id, reason, code),
430
448
  (data) => {
@@ -433,16 +451,14 @@ function onResetPasswordClick(row: UserRow): void {
433
451
  // void → Notify only.
434
452
  if (data && typeof data === 'object' && 'oneTimePassword' in data) {
435
453
  q.dialog({
436
- title: 'Einmal-Passwort generiert',
437
- message: `Bitte sicher übermitteln: ${data.oneTimePassword}`,
438
- 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 },
439
459
  });
440
460
  } else {
441
- q.notify({
442
- type: 'positive',
443
- message: 'Passwort-Reset ausgelöst.',
444
- position: 'top',
445
- });
461
+ notify('positive', msg.value.resetPassword.success);
446
462
  }
447
463
  void reload();
448
464
  },
@@ -454,23 +470,25 @@ function onDeactivateClick(row: UserRow): void {
454
470
  if (!props.submitDeactivate) return;
455
471
  const submit = props.submitDeactivate;
456
472
  q.dialog({
457
- title: `User "${row.email}" deaktivieren`,
458
- message: 'Grund für Audit (Pflichtfeld):',
473
+ title: formatMessage(msg.value.deactivate.dialogTitle, { email: row.email }),
474
+ message: msg.value.reasonPrompt,
459
475
  prompt: { model: '', type: 'text' },
460
- cancel: 'Abbrechen',
461
- ok: { label: 'Deaktivieren', color: 'negative' },
476
+ cancel: common.value.cancel,
477
+ ok: { label: msg.value.deactivate.action, color: 'negative' },
462
478
  }).onOk(async (reason: string) => {
463
479
  if (!reason || reason.trim().length === 0) return;
464
480
  await runAction(
465
- `User "${row.email}" deaktivieren — ${reason.trim()}.`,
481
+ formatMessage(msg.value.deactivate.mfaDescription, {
482
+ email: row.email,
483
+ reason: reason.trim(),
484
+ }),
466
485
  !!props.requireMfaForDeactivate,
467
486
  (code) => submit(row.id, reason, code),
468
487
  () => {
469
- q.notify({
470
- type: 'positive',
471
- message: `${row.email} deaktiviert.`,
472
- position: 'top',
473
- });
488
+ notify(
489
+ 'positive',
490
+ formatMessage(msg.value.deactivate.success, { email: row.email }),
491
+ );
474
492
  void reload();
475
493
  },
476
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