@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,10 +2,8 @@
2
2
  <div class="sa-pemail">
3
3
  <header class="sa-page-head">
4
4
  <div>
5
- <h1 class="sa-page-head__title">{{ title }}</h1>
6
- <p class="sa-page-head__sub">
7
- Zentraler Absender für Registrierungs- und System-Mails dieser Plattform.
8
- </p>
5
+ <h1 class="sa-page-head__title">{{ resolvedTitle }}</h1>
6
+ <p class="sa-page-head__sub">{{ msg.provider.subtitle }}</p>
9
7
  </div>
10
8
  <div class="sa-page-head__actions">
11
9
  <q-btn
@@ -13,10 +11,10 @@
13
11
  unelevated
14
12
  color="primary"
15
13
  icon="add"
16
- label="Absender"
14
+ :label="msg.sender"
17
15
  @click="openCreate"
18
16
  />
19
- <q-btn flat icon="refresh" label="Neu laden" @click="reload" />
17
+ <q-btn flat icon="refresh" :label="common.reload" @click="reload" />
20
18
  </div>
21
19
  </header>
22
20
 
@@ -34,7 +32,9 @@
34
32
  <q-td>
35
33
  <q-badge
36
34
  :color="row.active ? 'positive' : 'grey'"
37
- :label="row.active ? 'aktiv' : 'inaktiv'"
35
+ :label="
36
+ row.active ? msg.provider.statusActive : msg.provider.statusInactive
37
+ "
38
38
  />
39
39
  </q-td>
40
40
  </template>
@@ -45,7 +45,7 @@
45
45
  dense
46
46
  icon="send"
47
47
  color="primary"
48
- title="Test-Mail senden"
48
+ :title="msg.provider.sendTestMail"
49
49
  @click="openTest(row)"
50
50
  />
51
51
  <q-btn
@@ -53,7 +53,7 @@
53
53
  dense
54
54
  icon="edit"
55
55
  color="grey-7"
56
- title="Bearbeiten"
56
+ :title="common.edit"
57
57
  @click="openEdit(row)"
58
58
  />
59
59
  <q-btn
@@ -61,7 +61,7 @@
61
61
  dense
62
62
  icon="delete"
63
63
  color="negative"
64
- title="Löschen"
64
+ :title="common.delete"
65
65
  @click="onDelete(row)"
66
66
  />
67
67
  </q-td>
@@ -73,41 +73,72 @@
73
73
  <q-card class="sa-pemail__dialog">
74
74
  <q-card-section>
75
75
  <div class="text-h6">
76
- {{ editing ? 'Absender bearbeiten' : 'Absender anlegen' }}
76
+ {{
77
+ editing ? msg.provider.dialogEditTitle : msg.provider.dialogCreateTitle
78
+ }}
77
79
  </div>
78
80
  </q-card-section>
79
81
  <q-card-section class="q-gutter-sm">
80
- <q-input v-model="form.name" outlined dense label="Name" />
81
- <q-input v-model="form.smtpHost" outlined dense label="SMTP-Host" />
82
+ <q-input v-model="form.name" outlined dense :label="common.name" />
83
+ <q-input
84
+ v-model="form.smtpHost"
85
+ outlined
86
+ dense
87
+ :label="msg.provider.fieldSmtpHost"
88
+ />
82
89
  <q-input
83
90
  v-model.number="form.smtpPort"
84
91
  outlined
85
92
  dense
86
93
  type="number"
87
- label="SMTP-Port"
94
+ :label="msg.provider.fieldSmtpPort"
95
+ />
96
+ <q-input
97
+ v-model="form.smtpUser"
98
+ outlined
99
+ dense
100
+ :label="msg.provider.fieldSmtpUser"
88
101
  />
89
- <q-input v-model="form.smtpUser" outlined dense label="SMTP-Benutzer" />
90
102
  <q-input
91
103
  v-model="form.smtpPassword"
92
104
  outlined
93
105
  dense
94
106
  type="password"
95
- :label="editing ? 'SMTP-Passwort (leer = unverändert)' : 'SMTP-Passwort'"
107
+ :label="
108
+ editing
109
+ ? msg.provider.fieldSmtpPasswordEdit
110
+ : msg.provider.fieldSmtpPassword
111
+ "
96
112
  />
97
113
  <q-select
98
114
  v-model="form.encryption"
99
115
  outlined
100
116
  dense
101
- label="Verschlüsselung"
117
+ :label="msg.provider.fieldEncryption"
102
118
  :options="encryptionOptions"
103
119
  />
104
- <q-input v-model="form.fromEmail" outlined dense label="Absender-Adresse" />
105
- <q-input v-model="form.fromName" outlined dense label="Absender-Name" />
106
- <q-toggle v-if="editing" v-model="form.active" label="Aktiv" />
120
+ <q-input
121
+ v-model="form.fromEmail"
122
+ outlined
123
+ dense
124
+ :label="msg.provider.fieldFromEmail"
125
+ />
126
+ <q-input
127
+ v-model="form.fromName"
128
+ outlined
129
+ dense
130
+ :label="msg.provider.fieldFromName"
131
+ />
132
+ <q-toggle v-if="editing" v-model="form.active" :label="common.active" />
107
133
  </q-card-section>
108
134
  <q-card-actions align="right">
109
- <q-btn flat label="Abbrechen" @click="showForm = false" />
110
- <q-btn unelevated color="primary" :label="editing ? 'Speichern' : 'Anlegen'" @click="onSubmit" />
135
+ <q-btn flat :label="common.cancel" @click="showForm = false" />
136
+ <q-btn
137
+ unelevated
138
+ color="primary"
139
+ :label="editing ? common.save : common.create"
140
+ @click="onSubmit"
141
+ />
111
142
  </q-card-actions>
112
143
  </q-card>
113
144
  </q-dialog>
@@ -115,22 +146,36 @@
115
146
  <q-dialog v-model="showTest">
116
147
  <q-card class="sa-pemail__dialog">
117
148
  <q-card-section>
118
- <div class="text-h6">Test-Mail senden</div>
149
+ <div class="text-h6">{{ msg.provider.sendTestMail }}</div>
119
150
  <div class="text-caption text-grey-7">{{ testTarget?.name }}</div>
120
151
  </q-card-section>
121
152
  <q-card-section class="q-gutter-sm">
122
- <q-input v-model="testForm.toEmail" outlined dense label="Empfänger" type="email" />
123
- <q-input v-model="testForm.subject" outlined dense label="Betreff (optional)" />
124
- <q-banner v-if="testResult" :class="testResult.success ? 'bg-green-1' : 'bg-red-1'">
153
+ <q-input
154
+ v-model="testForm.toEmail"
155
+ outlined
156
+ dense
157
+ :label="msg.recipient"
158
+ type="email"
159
+ />
160
+ <q-input
161
+ v-model="testForm.subject"
162
+ outlined
163
+ dense
164
+ :label="msg.provider.fieldSubject"
165
+ />
166
+ <q-banner
167
+ v-if="testResult"
168
+ :class="testResult.success ? 'bg-green-1' : 'bg-red-1'"
169
+ >
125
170
  {{ testResult.message }}
126
171
  </q-banner>
127
172
  </q-card-section>
128
173
  <q-card-actions align="right">
129
- <q-btn flat label="Schließen" @click="showTest = false" />
174
+ <q-btn flat :label="common.close" @click="showTest = false" />
130
175
  <q-btn
131
176
  unelevated
132
177
  color="primary"
133
- label="Senden"
178
+ :label="msg.provider.send"
134
179
  :loading="testing"
135
180
  @click="onTest"
136
181
  />
@@ -151,10 +196,12 @@
151
196
  </template>
152
197
 
153
198
  <script setup lang="ts">
154
- import { reactive, ref } from 'vue';
199
+ import { computed, reactive, ref } from 'vue';
155
200
  import { useQuasar } from 'quasar';
156
201
  import { useSuperAdminNotify } from '../quasar/notify.js';
157
202
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
203
+ import { formatMessage } from '../client/i18n/format.js';
204
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
158
205
  import type {
159
206
  PlatformEmailProvider,
160
207
  PlatformEmailWriteInput,
@@ -177,37 +224,49 @@ const props = withDefaults(
177
224
  mfaCode?: string,
178
225
  ) => Promise<unknown>;
179
226
  deleteProvider: (id: string, mfaCode?: string) => Promise<unknown>;
180
- testProvider: (id: string, input: PlatformEmailTestInput) => Promise<PlatformEmailTestResult>;
227
+ testProvider: (
228
+ id: string,
229
+ input: PlatformEmailTestInput,
230
+ ) => Promise<PlatformEmailTestResult>;
181
231
  title?: string;
182
232
  requireMfaForWrite?: boolean;
183
233
  mfaSetupHint?: string;
184
234
  }>(),
185
235
  {
186
- title: 'Plattform-E-Mail',
187
236
  requireMfaForWrite: false,
188
237
  },
189
238
  );
190
239
 
191
240
  const q = useQuasar();
192
241
  const notify = useSuperAdminNotify();
242
+ const msg = useSaMessages('email');
243
+ const common = useSaMessages('common');
244
+ const shell = useSaMessages('shell');
193
245
  const rows = ref<PlatformEmailProvider[]>([]);
194
246
  const loading = ref(false);
195
247
 
248
+ const resolvedTitle = computed(() => props.title ?? msg.value.provider.title);
249
+
196
250
  const encryptionOptions = ['NONE', 'SSL', 'TLS', 'STARTTLS'];
197
251
 
198
- const columns = [
199
- { name: 'name', label: 'Name', field: 'name', align: 'left' as const },
252
+ const columns = computed(() => [
253
+ { name: 'name', label: common.value.name, field: 'name', align: 'left' as const },
200
254
  {
201
255
  name: 'host',
202
- label: 'Host',
256
+ label: msg.value.provider.columnHost,
203
257
  field: (r: PlatformEmailProvider) => `${r.smtpHost}:${r.smtpPort}`,
204
258
  align: 'left' as const,
205
259
  },
206
- { name: 'fromEmail', label: 'Absender', field: 'fromEmail', align: 'left' as const },
207
- { name: 'encryption', label: 'Krypto', field: 'encryption', align: 'left' as const },
208
- { name: 'active', label: 'Status', field: 'active', align: 'left' as const },
260
+ { name: 'fromEmail', label: msg.value.sender, field: 'fromEmail', align: 'left' as const },
261
+ {
262
+ name: 'encryption',
263
+ label: msg.value.provider.columnEncryption,
264
+ field: 'encryption',
265
+ align: 'left' as const,
266
+ },
267
+ { name: 'active', label: common.value.status, field: 'active', align: 'left' as const },
209
268
  { name: 'actions', label: '', field: 'id' as never, align: 'right' as 'left' },
210
- ];
269
+ ]);
211
270
 
212
271
  const showForm = ref(false);
213
272
  const editing = ref<PlatformEmailProvider | null>(null);
@@ -266,7 +325,7 @@ function errMsg(err: unknown): string {
266
325
  return (
267
326
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
268
327
  (err as Error)?.message ??
269
- 'Aktion fehlgeschlagen'
328
+ msg.value.errorAction
270
329
  );
271
330
  }
272
331
 
@@ -308,7 +367,10 @@ function openEdit(row: PlatformEmailProvider): void {
308
367
  }
309
368
 
310
369
  // MFA loop: on 401 the dialog stays open and asks again.
311
- async function runWrite(label: string, invoke: (code: string) => Promise<unknown>): Promise<boolean> {
370
+ async function runWrite(
371
+ label: string,
372
+ invoke: (code: string) => Promise<unknown>,
373
+ ): Promise<boolean> {
312
374
  if (!props.requireMfaForWrite) {
313
375
  try {
314
376
  await invoke('');
@@ -328,7 +390,7 @@ async function runWrite(label: string, invoke: (code: string) => Promise<unknown
328
390
  } catch (err) {
329
391
  const status = (err as { response?: { status?: number } })?.response?.status;
330
392
  if (status === 401) {
331
- mfaError.value = 'TOTP-Code ungültig oder MFA nicht eingerichtet.';
393
+ mfaError.value = shell.value.mfa.invalidCode;
332
394
  continue;
333
395
  }
334
396
  showMfa.value = false;
@@ -362,7 +424,9 @@ async function onSubmit(): Promise<void> {
362
424
  const current = editing.value;
363
425
  const input = buildWriteInput();
364
426
  const ok = await runWrite(
365
- current ? `Absender "${input.name}" speichern.` : `Absender "${input.name}" anlegen.`,
427
+ formatMessage(current ? msg.value.provider.mfaSave : msg.value.provider.mfaCreate, {
428
+ name: input.name,
429
+ }),
366
430
  (code) =>
367
431
  current
368
432
  ? props.updateProvider(current.id, input, code || undefined)
@@ -370,23 +434,24 @@ async function onSubmit(): Promise<void> {
370
434
  );
371
435
  if (ok) {
372
436
  showForm.value = false;
373
- notify('positive', 'Gespeichert.');
437
+ notify('positive', msg.value.provider.saved);
374
438
  void reload();
375
439
  }
376
440
  }
377
441
 
378
442
  function onDelete(row: PlatformEmailProvider): void {
379
443
  q.dialog({
380
- title: 'Absender löschen',
381
- message: `"${row.name}" wirklich löschen?`,
382
- cancel: 'Abbrechen',
383
- ok: { label: 'Löschen', color: 'negative' },
444
+ title: msg.value.provider.deleteDialogTitle,
445
+ message: formatMessage(msg.value.provider.deleteDialogMessage, { name: row.name }),
446
+ cancel: common.value.cancel,
447
+ ok: { label: common.value.delete, color: 'negative' },
384
448
  }).onOk(async () => {
385
- const ok = await runWrite(`Absender "${row.name}" löschen.`, (code) =>
386
- props.deleteProvider(row.id, code || undefined),
449
+ const ok = await runWrite(
450
+ formatMessage(msg.value.provider.mfaDelete, { name: row.name }),
451
+ (code) => props.deleteProvider(row.id, code || undefined),
387
452
  );
388
453
  if (ok) {
389
- notify('positive', 'Gelöscht.');
454
+ notify('positive', msg.value.provider.deleted);
390
455
  void reload();
391
456
  }
392
457
  });
@@ -6,11 +6,11 @@
6
6
  flat
7
7
  dense
8
8
  icon="arrow_back"
9
- :label="backLabel"
9
+ :label="labels.back"
10
10
  :to="backRoute"
11
11
  class="sa-promo-detail__back"
12
12
  />
13
- <h1 class="sa-page-head__title">{{ data?.promo.code ?? promoCodeLabel }}</h1>
13
+ <h1 class="sa-page-head__title">{{ data?.promo.code ?? labels.promoCode }}</h1>
14
14
  <p class="sa-page-head__sub">
15
15
  <template v-if="data">
16
16
  {{ data.promo.valueType }} · {{ data.promo.value }} ·
@@ -28,7 +28,7 @@
28
28
  color="primary"
29
29
  unelevated
30
30
  icon="edit"
31
- :label="editLabel"
31
+ :label="labels.edit"
32
32
  @click="openEdit"
33
33
  />
34
34
  <slot name="header-actions" />
@@ -37,20 +37,20 @@
37
37
 
38
38
  <div class="sa-promo-detail__body">
39
39
  <div v-if="loading" class="sa-promo-detail__state">
40
- <q-spinner size="32px" /> wird geladen…
40
+ <q-spinner size="32px" /> {{ msg.detail.loading }}
41
41
  </div>
42
- <div v-else-if="!data" class="sa-promo-detail__state">{{ emptyLabel }}</div>
42
+ <div v-else-if="!data" class="sa-promo-detail__state">{{ labels.empty }}</div>
43
43
 
44
44
  <template v-else>
45
45
  <div class="sa-card sa-promo-detail__section">
46
- <div class="sa-promo-detail__section-head">{{ configLabel }}</div>
46
+ <div class="sa-promo-detail__section-head">{{ labels.config }}</div>
47
47
  <slot name="config" :promo="data.promo">
48
48
  <pre class="sa-promo-detail__kv">{{ resolveFormatPromo(data.promo) }}</pre>
49
49
  </slot>
50
50
  </div>
51
51
 
52
52
  <div class="sa-card sa-promo-detail__section">
53
- <div class="sa-promo-detail__section-head">{{ statsLabel }}</div>
53
+ <div class="sa-promo-detail__section-head">{{ labels.stats }}</div>
54
54
  <slot name="stats" :stats="data.stats">
55
55
  <pre class="sa-promo-detail__kv">{{
56
56
  JSON.stringify(data.stats, null, 2)
@@ -60,13 +60,13 @@
60
60
 
61
61
  <div class="sa-card sa-promo-detail__section">
62
62
  <div class="sa-promo-detail__section-head">
63
- {{ redemptionsLabel }} ({{ data.redemptions.length }})
63
+ {{ labels.redemptions }} ({{ data.redemptions.length }})
64
64
  </div>
65
65
  <slot name="redemptions" :redemptions="data.redemptions">
66
66
  <q-table
67
67
  flat
68
68
  :rows="data.redemptions"
69
- :columns="redemptionsColumns ?? DEFAULT_COLUMNS"
69
+ :columns="redemptionsColumns ?? defaultColumns"
70
70
  row-key="id"
71
71
  :pagination="{ rowsPerPage: 0 }"
72
72
  hide-pagination
@@ -97,6 +97,7 @@ import PromoCodeEditDialog, {
97
97
  type PromoCodeEditRow,
98
98
  } from '../components/dialogs/PromoCodeEditDialog.vue';
99
99
  import type { PromoCodePlanOption, PromoCodeUpdatePayload } from '../components/dialogs/types.js';
100
+ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
100
101
 
101
102
  export interface PromoDetailData {
102
103
  promo: Record<string, unknown> & {
@@ -109,53 +110,55 @@ export interface PromoDetailData {
109
110
  redemptions: Array<Record<string, unknown> & { id: string }>;
110
111
  }
111
112
 
112
- const props = withDefaults(
113
- defineProps<{
114
- loadDetail: () => Promise<PromoDetailData>;
115
- backRoute: RouteLocationRaw;
116
- backLabel?: string;
117
- promoCodeLabel?: string;
118
- configLabel?: string;
119
- statsLabel?: string;
120
- redemptionsLabel?: string;
121
- emptyLabel?: string;
122
- redemptionsColumns?: QTableColumn[];
123
- /**
124
- * Maps a status string to a Quasar color name. Default: ACTIVE→positive,
125
- * PAUSED→amber-7, EXPIRED→grey, else negative.
126
- */
127
- statusColor?: (status: string) => string;
128
- /**
129
- * Optional formatter for the Konfiguration JSON block. Default:
130
- * `JSON.stringify(promo, null, 2)` — the consumer can pass a curated
131
- * subset (e.g. only marketing-relevant fields).
132
- */
133
- formatPromo?: (promo: Record<string, unknown>) => string;
134
- /**
135
- * When set: enables the "Edit" button in the header and opens the
136
- * {@link PromoCodeEditDialog}. The consumer provides the app-specific
137
- * PATCH function (e.g. `adminApi.promoCodes.update`). After a
138
- * successful save, the detail page automatically reloads via
139
- * `loadDetail()` — the consumer needs to trigger nothing further.
140
- */
141
- editSubmit?: (id: string, payload: PromoCodeUpdatePayload) => Promise<void>;
142
- editLabel?: string;
143
- /**
144
- * Plan list for the plan picker in the edit dialog. When empty, the
145
- * dialog hides the plan selection.
146
- */
147
- editPlans?: readonly PromoCodePlanOption[];
148
- }>(),
149
- {
150
- backLabel: 'Zurück',
151
- promoCodeLabel: 'Promo-Code',
152
- configLabel: 'Konfiguration',
153
- statsLabel: 'Statistik',
154
- redemptionsLabel: 'Einlösungen',
155
- emptyLabel: 'Code nicht gefunden.',
156
- editLabel: 'Bearbeiten',
157
- },
158
- );
113
+ const props = defineProps<{
114
+ loadDetail: () => Promise<PromoDetailData>;
115
+ backRoute: RouteLocationRaw;
116
+ backLabel?: string;
117
+ promoCodeLabel?: string;
118
+ configLabel?: string;
119
+ statsLabel?: string;
120
+ redemptionsLabel?: string;
121
+ emptyLabel?: string;
122
+ redemptionsColumns?: QTableColumn[];
123
+ /**
124
+ * Maps a status string to a Quasar color name. Default: ACTIVE→positive,
125
+ * PAUSED→amber-7, EXPIRED→grey, else negative.
126
+ */
127
+ statusColor?: (status: string) => string;
128
+ /**
129
+ * Optional formatter for the Konfiguration JSON block. Default:
130
+ * `JSON.stringify(promo, null, 2)` the consumer can pass a curated
131
+ * subset (e.g. only marketing-relevant fields).
132
+ */
133
+ formatPromo?: (promo: Record<string, unknown>) => string;
134
+ /**
135
+ * When set: enables the "Edit" button in the header and opens the
136
+ * {@link PromoCodeEditDialog}. The consumer provides the app-specific
137
+ * PATCH function (e.g. `adminApi.promoCodes.update`). After a
138
+ * successful save, the detail page automatically reloads via
139
+ * `loadDetail()` the consumer needs to trigger nothing further.
140
+ */
141
+ editSubmit?: (id: string, payload: PromoCodeUpdatePayload) => Promise<void>;
142
+ editLabel?: string;
143
+ /**
144
+ * Plan list for the plan picker in the edit dialog. When empty, the
145
+ * dialog hides the plan selection.
146
+ */
147
+ editPlans?: readonly PromoCodePlanOption[];
148
+ }>();
149
+
150
+ const msg = useSaMessages('promos');
151
+ const common = useSaMessages('common');
152
+
153
+ const labels = computed(() => ({
154
+ back: props.backLabel ?? common.value.back,
155
+ promoCode: props.promoCodeLabel ?? msg.value.detail.promoCodeLabel,
156
+ config: props.configLabel ?? msg.value.detail.configLabel,
157
+ stats: props.statsLabel ?? msg.value.detail.statsLabel,
158
+ redemptions: props.redemptionsLabel ?? msg.value.detail.redemptionsLabel,
159
+ empty: props.emptyLabel ?? msg.value.detail.emptyLabel,
160
+ edit: props.editLabel ?? common.value.edit,
161
+ }));
159
162
 
160
163
  const data = ref<PromoDetailData | null>(null);
161
164
  const loading = ref(false);
@@ -243,24 +246,24 @@ function resolveFormatPromo(promo: Record<string, unknown>): string {
243
246
  return JSON.stringify(promo, null, 2);
244
247
  }
245
248
 
246
- const DEFAULT_COLUMNS: QTableColumn[] = [
249
+ const defaultColumns = computed<QTableColumn[]>(() => [
247
250
  {
248
251
  name: 'tenant',
249
- label: 'Tenant',
252
+ label: msg.value.detail.columnTenant,
250
253
  field: (r: unknown) =>
251
254
  ((r as Record<string, unknown>).tenant as { slug?: string } | undefined)?.slug ?? '—',
252
255
  align: 'left',
253
256
  },
254
- { name: 'status', label: 'Status', field: 'status', align: 'left' },
257
+ { name: 'status', label: common.value.status, field: 'status', align: 'left' },
255
258
  {
256
259
  name: 'startsAt',
257
- label: 'Start',
260
+ label: msg.value.detail.columnStart,
258
261
  field: (r: unknown) => String((r as Record<string, unknown>).startsAt ?? '').slice(0, 10),
259
262
  align: 'left',
260
263
  },
261
264
  {
262
265
  name: 'endsAt',
263
- label: 'Ende',
266
+ label: msg.value.detail.columnEnd,
264
267
  field: (r: unknown) => {
265
268
  const v = (r as Record<string, unknown>).endsAt;
266
269
  return v ? String(v).slice(0, 10) : '∞';
@@ -269,14 +272,14 @@ const DEFAULT_COLUMNS: QTableColumn[] = [
269
272
  },
270
273
  {
271
274
  name: 'redeemedAt',
272
- label: 'Eingelöst',
275
+ label: msg.value.detail.columnRedeemedAt,
273
276
  field: (r: unknown) =>
274
277
  String((r as Record<string, unknown>).redeemedAt ?? '')
275
278
  .slice(0, 19)
276
279
  .replace('T', ' '),
277
280
  align: 'left',
278
281
  },
279
- ];
282
+ ]);
280
283
  </script>
281
284
 
282
285
  <style scoped>