@saasicat/ui-vue 0.2.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 (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,405 @@
1
+ <template>
2
+ <div class="sa-setup-wrap">
3
+ <div class="sa-setup-card">
4
+ <div class="sa-setup-head">
5
+ <div class="sa-setup-badge">{{ iconText }}</div>
6
+ <div>
7
+ <div class="sa-setup-title">Erst­einrichtung</div>
8
+ <div class="sa-setup-sub">{{ brandName }} · ersten SuperAdmin anlegen</div>
9
+ </div>
10
+ </div>
11
+
12
+ <q-banner v-if="errorMessage" class="sa-setup-error" rounded>
13
+ {{ errorMessage }}
14
+ </q-banner>
15
+
16
+ <!-- Step 1: create SuperAdmin -->
17
+ <q-form v-if="step === 'form'" @submit.prevent="submitCreate" class="sa-setup-form">
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.
21
+ </p>
22
+ <q-input
23
+ v-model="form.token"
24
+ label="Setup-Token"
25
+ type="password"
26
+ outlined
27
+ dense
28
+ autofocus
29
+ :disable="loading"
30
+ class="q-mb-sm"
31
+ :rules="[(v: string) => v.length > 0 || 'Setup-Token ist Pflicht']"
32
+ />
33
+ <q-input
34
+ v-model="form.email"
35
+ label="E-Mail"
36
+ type="email"
37
+ outlined
38
+ dense
39
+ :disable="loading"
40
+ class="q-mb-sm"
41
+ :rules="[(v: string) => /\S+@\S+\.\S+/.test(v) || 'Bitte gültige E-Mail']"
42
+ />
43
+ <q-input
44
+ v-model="form.password"
45
+ label="Passwort (leer = generieren)"
46
+ :type="showPw ? 'text' : 'password'"
47
+ outlined
48
+ dense
49
+ :disable="loading"
50
+ hint="Mindestens 8 Zeichen, oder leer lassen für ein generiertes Passwort."
51
+ class="q-mb-sm"
52
+ :rules="[(v: string) => v.length === 0 || v.length >= 8 || 'Mindestens 8 Zeichen']"
53
+ >
54
+ <template #append>
55
+ <q-icon
56
+ :name="showPw ? 'visibility_off' : 'visibility'"
57
+ class="cursor-pointer"
58
+ @click="showPw = !showPw"
59
+ />
60
+ </template>
61
+ </q-input>
62
+ <q-btn
63
+ unelevated
64
+ color="primary"
65
+ icon="person_add"
66
+ label="SuperAdmin anlegen"
67
+ :loading="loading"
68
+ :disable="!canCreate"
69
+ class="full-width q-mt-sm"
70
+ type="submit"
71
+ />
72
+ </q-form>
73
+
74
+ <!-- Step 2: set up MFA -->
75
+ <div v-else-if="step === 'mfa' && result" class="sa-setup-mfa">
76
+ <p class="sa-setup-hint">
77
+ Account <strong>{{ result.email }}</strong> angelegt. Richte jetzt die
78
+ Zwei-Faktor-Authentisierung (TOTP) ein.
79
+ </p>
80
+
81
+ <div v-if="result.generatedPassword" class="sa-setup-secret">
82
+ <div class="sa-setup-secret__label">Generiertes Passwort (einmalig sichern)</div>
83
+ <code class="sa-setup-secret__value">{{ result.generatedPassword }}</code>
84
+ </div>
85
+
86
+ <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>
89
+ </div>
90
+
91
+ <div class="sa-setup-secret">
92
+ <div class="sa-setup-secret__label">
93
+ In Authenticator-App als „Schlüssel manuell eingeben"
94
+ </div>
95
+ <code class="sa-setup-secret__value">{{ result.secret }}</code>
96
+ </div>
97
+ <details class="sa-setup-uri">
98
+ <summary>otpauth-URI (für QR-Generator)</summary>
99
+ <code class="sa-setup-uri__value">{{ result.otpauthUri }}</code>
100
+ </details>
101
+
102
+ <q-form @submit.prevent="submitConfirm" class="sa-setup-form q-mt-md">
103
+ <q-input
104
+ v-model="mfaCode"
105
+ label="6-stelliger Code aus der App"
106
+ outlined
107
+ dense
108
+ autofocus
109
+ inputmode="numeric"
110
+ maxlength="6"
111
+ :disable="loading"
112
+ :rules="[(v: string) => /^\d{6}$/.test(v) || '6-stelliger Code']"
113
+ />
114
+ <q-btn
115
+ unelevated
116
+ color="primary"
117
+ icon="verified_user"
118
+ label="MFA bestätigen"
119
+ :loading="loading"
120
+ :disable="!/^\d{6}$/.test(mfaCode) || loading"
121
+ class="full-width q-mt-sm"
122
+ type="submit"
123
+ />
124
+ </q-form>
125
+ <button class="sa-setup-skip" type="button" @click="step = 'done'">
126
+ Überspringen — Code beim ersten Login eingeben
127
+ </button>
128
+ </div>
129
+
130
+ <!-- Step 3: done -->
131
+ <div v-else-if="step === 'done'" class="sa-setup-done">
132
+ <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>
136
+ <q-btn
137
+ unelevated
138
+ color="primary"
139
+ icon="login"
140
+ label="Zum Login"
141
+ class="full-width"
142
+ @click="emit('done')"
143
+ />
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script setup lang="ts">
150
+ import { computed, reactive, ref } from 'vue';
151
+ import {
152
+ SETUP_ERROR_CODES,
153
+ type SetupConfirmMfaResponse,
154
+ type SetupResult,
155
+ } from '@saasicat/types';
156
+
157
+ import {
158
+ useSuperAdminBrand,
159
+ useSuperAdminEndpoints,
160
+ useSuperAdminHttp,
161
+ } from '../use-super-admin-context.js';
162
+ import { HttpJsonError, postJson as httpPostJson } from '../http-json.js';
163
+
164
+ interface Props {
165
+ /** Display name + badge abbreviation (override the app branding, e.g. from PublicBoot). */
166
+ displayName?: string;
167
+ icon?: string;
168
+ }
169
+ const props = defineProps<Props>();
170
+
171
+ // `done` signals the parent screen (LoginPage) that the form should be
172
+ // shown again — the freshly created SuperAdmin then signs in.
173
+ const emit = defineEmits<{ done: [] }>();
174
+
175
+ const brand = useSuperAdminBrand();
176
+ const endpoints = useSuperAdminEndpoints();
177
+ const http = useSuperAdminHttp();
178
+
179
+ const brandName = computed(() => props.displayName ?? brand.name);
180
+ const iconText = computed(() => props.icon ?? brand.logoText);
181
+
182
+ const step = ref<'form' | 'mfa' | 'done'>('form');
183
+ const form = reactive({ token: '', email: '', password: '' });
184
+ const showPw = ref(false);
185
+ const mfaCode = ref('');
186
+ const result = ref<SetupResult | null>(null);
187
+ const loading = ref(false);
188
+ const errorMessage = ref<string | null>(null);
189
+
190
+ const canCreate = computed(
191
+ () =>
192
+ form.token.length > 0 &&
193
+ /\S+@\S+\.\S+/.test(form.email) &&
194
+ (form.password.length === 0 || form.password.length >= 8) &&
195
+ !loading.value,
196
+ );
197
+
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
+ };
207
+
208
+ // Goes through the injected HttpClient (consumer's auth/baseURL apply);
209
+ // maps the error code from the body to a readable message.
210
+ async function postJson<T>(path: string, body: unknown): Promise<T> {
211
+ try {
212
+ return await httpPostJson<T>(http, `${endpoints.apiBase}${path}`, body);
213
+ } catch (err) {
214
+ if (err instanceof HttpJsonError) {
215
+ throw new Error(
216
+ (err.code && ERROR_BY_CODE[err.code]) || `Fehler (HTTP ${err.status}).`,
217
+ );
218
+ }
219
+ throw err;
220
+ }
221
+ }
222
+
223
+ async function submitCreate(): Promise<void> {
224
+ if (!canCreate.value) return;
225
+ loading.value = true;
226
+ errorMessage.value = null;
227
+ try {
228
+ result.value = await postJson<SetupResult>('/setup', {
229
+ token: form.token,
230
+ email: form.email.toLowerCase(),
231
+ ...(form.password ? { password: form.password } : {}),
232
+ });
233
+ step.value = 'mfa';
234
+ } catch (err) {
235
+ errorMessage.value = err instanceof Error ? err.message : 'Setup fehlgeschlagen.';
236
+ } finally {
237
+ loading.value = false;
238
+ }
239
+ }
240
+
241
+ async function submitConfirm(): Promise<void> {
242
+ if (!result.value || !/^\d{6}$/.test(mfaCode.value)) return;
243
+ loading.value = true;
244
+ errorMessage.value = null;
245
+ try {
246
+ const res = await postJson<SetupConfirmMfaResponse>('/setup/confirm-mfa', {
247
+ token: form.token,
248
+ userId: result.value.userId,
249
+ code: mfaCode.value,
250
+ });
251
+ if (res.ok) {
252
+ step.value = 'done';
253
+ } else {
254
+ errorMessage.value = 'Code ungültig — bitte erneut versuchen.';
255
+ }
256
+ } catch (err) {
257
+ errorMessage.value = err instanceof Error ? err.message : 'Bestätigung fehlgeschlagen.';
258
+ } finally {
259
+ loading.value = false;
260
+ }
261
+ }
262
+ </script>
263
+
264
+ <style scoped>
265
+ .sa-setup-wrap {
266
+ min-height: 100vh;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ padding: 24px;
271
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
272
+ }
273
+ .sa-setup-card {
274
+ width: 460px;
275
+ max-width: 92vw;
276
+ background: #fff;
277
+ border-radius: 18px;
278
+ padding: 32px;
279
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.25);
280
+ }
281
+ .sa-setup-head {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 12px;
285
+ margin-bottom: 20px;
286
+ }
287
+ .sa-setup-badge {
288
+ width: 44px;
289
+ height: 44px;
290
+ border-radius: 10px;
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ background: linear-gradient(135deg, #475569, #1e293b);
295
+ color: #fff;
296
+ font-weight: 800;
297
+ font-size: 18px;
298
+ text-transform: uppercase;
299
+ }
300
+ .sa-setup-title {
301
+ font-weight: 800;
302
+ font-size: 18px;
303
+ color: #0f172a;
304
+ line-height: 1.1;
305
+ }
306
+ .sa-setup-sub {
307
+ font-size: 12px;
308
+ color: #475569;
309
+ }
310
+ .sa-setup-hint {
311
+ font-size: 13px;
312
+ color: #475569;
313
+ line-height: 1.5;
314
+ margin: 0 0 16px;
315
+ }
316
+ .sa-setup-hint code {
317
+ background: #e2e8f0;
318
+ padding: 1px 5px;
319
+ border-radius: 4px;
320
+ }
321
+ .sa-setup-form {
322
+ display: flex;
323
+ flex-direction: column;
324
+ }
325
+ .full-width {
326
+ width: 100%;
327
+ }
328
+ .sa-setup-error {
329
+ background: #fef2f2;
330
+ border: 1px solid #fecaca;
331
+ color: #b91c1c;
332
+ font-size: 13px;
333
+ margin-bottom: 16px;
334
+ padding: 8px 12px;
335
+ border-radius: 8px;
336
+ }
337
+ .sa-setup-qr {
338
+ display: flex;
339
+ flex-direction: column;
340
+ align-items: center;
341
+ gap: 6px;
342
+ margin-bottom: 12px;
343
+ }
344
+ .sa-setup-qr img {
345
+ border: 1px solid #e2e8f0;
346
+ border-radius: 8px;
347
+ padding: 8px;
348
+ background: #fff;
349
+ }
350
+ .sa-setup-qr__hint {
351
+ font-size: 12px;
352
+ color: #64748b;
353
+ }
354
+ .sa-setup-secret {
355
+ background: #f1f5f9;
356
+ border-radius: 8px;
357
+ padding: 10px 12px;
358
+ margin-bottom: 10px;
359
+ }
360
+ .sa-setup-secret__label {
361
+ font-size: 11px;
362
+ text-transform: uppercase;
363
+ letter-spacing: 0.06em;
364
+ color: #64748b;
365
+ margin-bottom: 4px;
366
+ }
367
+ .sa-setup-secret__value {
368
+ font-size: 15px;
369
+ font-weight: 700;
370
+ color: #0f172a;
371
+ word-break: break-all;
372
+ letter-spacing: 0.04em;
373
+ }
374
+ .sa-setup-uri {
375
+ margin: 4px 0 4px;
376
+ font-size: 12px;
377
+ color: #475569;
378
+ }
379
+ .sa-setup-uri__value {
380
+ display: block;
381
+ margin-top: 6px;
382
+ word-break: break-all;
383
+ color: #334155;
384
+ background: #f8fafc;
385
+ padding: 6px 8px;
386
+ border-radius: 6px;
387
+ }
388
+ .sa-setup-skip {
389
+ margin-top: 14px;
390
+ width: 100%;
391
+ background: none;
392
+ border: none;
393
+ color: #64748b;
394
+ font-size: 12px;
395
+ cursor: pointer;
396
+ text-decoration: underline;
397
+ }
398
+ .sa-setup-done {
399
+ display: flex;
400
+ flex-direction: column;
401
+ align-items: center;
402
+ text-align: center;
403
+ gap: 10px;
404
+ }
405
+ </style>