@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,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,9 +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';
201
+ import { useSuperAdminNotify } from '../quasar/notify.js';
156
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';
157
205
  import type {
158
206
  PlatformEmailProvider,
159
207
  PlatformEmailWriteInput,
@@ -176,36 +224,49 @@ const props = withDefaults(
176
224
  mfaCode?: string,
177
225
  ) => Promise<unknown>;
178
226
  deleteProvider: (id: string, mfaCode?: string) => Promise<unknown>;
179
- testProvider: (id: string, input: PlatformEmailTestInput) => Promise<PlatformEmailTestResult>;
227
+ testProvider: (
228
+ id: string,
229
+ input: PlatformEmailTestInput,
230
+ ) => Promise<PlatformEmailTestResult>;
180
231
  title?: string;
181
232
  requireMfaForWrite?: boolean;
182
233
  mfaSetupHint?: string;
183
234
  }>(),
184
235
  {
185
- title: 'Plattform-E-Mail',
186
236
  requireMfaForWrite: false,
187
237
  },
188
238
  );
189
239
 
190
240
  const q = useQuasar();
241
+ const notify = useSuperAdminNotify();
242
+ const msg = useSaMessages('email');
243
+ const common = useSaMessages('common');
244
+ const shell = useSaMessages('shell');
191
245
  const rows = ref<PlatformEmailProvider[]>([]);
192
246
  const loading = ref(false);
193
247
 
248
+ const resolvedTitle = computed(() => props.title ?? msg.value.provider.title);
249
+
194
250
  const encryptionOptions = ['NONE', 'SSL', 'TLS', 'STARTTLS'];
195
251
 
196
- const columns = [
197
- { 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 },
198
254
  {
199
255
  name: 'host',
200
- label: 'Host',
256
+ label: msg.value.provider.columnHost,
201
257
  field: (r: PlatformEmailProvider) => `${r.smtpHost}:${r.smtpPort}`,
202
258
  align: 'left' as const,
203
259
  },
204
- { name: 'fromEmail', label: 'Absender', field: 'fromEmail', align: 'left' as const },
205
- { name: 'encryption', label: 'Krypto', field: 'encryption', align: 'left' as const },
206
- { 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 },
207
268
  { name: 'actions', label: '', field: 'id' as never, align: 'right' as 'left' },
208
- ];
269
+ ]);
209
270
 
210
271
  const showForm = ref(false);
211
272
  const editing = ref<PlatformEmailProvider | null>(null);
@@ -264,7 +325,7 @@ function errMsg(err: unknown): string {
264
325
  return (
265
326
  (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
266
327
  (err as Error)?.message ??
267
- 'Aktion fehlgeschlagen'
328
+ msg.value.errorAction
268
329
  );
269
330
  }
270
331
 
@@ -274,7 +335,7 @@ async function reload(): Promise<void> {
274
335
  rows.value = await props.loadProviders();
275
336
  } catch (err) {
276
337
  rows.value = [];
277
- q.notify({ type: 'negative', message: errMsg(err), position: 'top' });
338
+ notify('negative', errMsg(err));
278
339
  } finally {
279
340
  loading.value = false;
280
341
  }
@@ -306,13 +367,16 @@ function openEdit(row: PlatformEmailProvider): void {
306
367
  }
307
368
 
308
369
  // MFA loop: on 401 the dialog stays open and asks again.
309
- 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> {
310
374
  if (!props.requireMfaForWrite) {
311
375
  try {
312
376
  await invoke('');
313
377
  return true;
314
378
  } catch (err) {
315
- q.notify({ type: 'negative', message: errMsg(err), position: 'top' });
379
+ notify('negative', errMsg(err));
316
380
  return false;
317
381
  }
318
382
  }
@@ -326,11 +390,11 @@ async function runWrite(label: string, invoke: (code: string) => Promise<unknown
326
390
  } catch (err) {
327
391
  const status = (err as { response?: { status?: number } })?.response?.status;
328
392
  if (status === 401) {
329
- mfaError.value = 'TOTP-Code ungültig oder MFA nicht eingerichtet.';
393
+ mfaError.value = shell.value.mfa.invalidCode;
330
394
  continue;
331
395
  }
332
396
  showMfa.value = false;
333
- q.notify({ type: 'negative', message: errMsg(err), position: 'top' });
397
+ notify('negative', errMsg(err));
334
398
  return false;
335
399
  }
336
400
  }
@@ -360,7 +424,9 @@ async function onSubmit(): Promise<void> {
360
424
  const current = editing.value;
361
425
  const input = buildWriteInput();
362
426
  const ok = await runWrite(
363
- 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
+ }),
364
430
  (code) =>
365
431
  current
366
432
  ? props.updateProvider(current.id, input, code || undefined)
@@ -368,23 +434,24 @@ async function onSubmit(): Promise<void> {
368
434
  );
369
435
  if (ok) {
370
436
  showForm.value = false;
371
- q.notify({ type: 'positive', message: 'Gespeichert.', position: 'top' });
437
+ notify('positive', msg.value.provider.saved);
372
438
  void reload();
373
439
  }
374
440
  }
375
441
 
376
442
  function onDelete(row: PlatformEmailProvider): void {
377
443
  q.dialog({
378
- title: 'Absender löschen',
379
- message: `"${row.name}" wirklich löschen?`,
380
- cancel: 'Abbrechen',
381
- 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' },
382
448
  }).onOk(async () => {
383
- const ok = await runWrite(`Absender "${row.name}" löschen.`, (code) =>
384
- 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),
385
452
  );
386
453
  if (ok) {
387
- q.notify({ type: 'positive', message: 'Gelöscht.', position: 'top' });
454
+ notify('positive', msg.value.provider.deleted);
388
455
  void reload();
389
456
  }
390
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>