@saasicat/ui-vue 0.18.1 → 0.21.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.
- package/dist/{catalog-JXdKdIqG.d.cts → catalog-BY_t9prv.d.cts} +1 -0
- package/dist/{catalog-JXdKdIqG.d.ts → catalog-BY_t9prv.d.ts} +1 -0
- package/dist/{chunk-Y5MWMWMD.js → chunk-7IFKVGZV.js} +1 -1
- package/dist/{chunk-CC4WK3DC.js → chunk-JN6HZGLV.js} +4 -4
- package/dist/{chunk-VRCVJTY4.js → chunk-OTP3OSSW.js} +1 -1
- package/dist/client/index.cjs +4 -4
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/index.cjs +6 -6
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/quasar/index.cjs +5 -0
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +2 -2
- package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-DlJRGIVq.d.cts} +1 -1
- package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-c_Eqsu9u.d.ts} +1 -1
- package/package.json +2 -2
- package/src/client/i18n/format.ts +7 -4
- package/src/client/i18n/messages/common.ts +2 -0
- package/src/components/admin-page/AdminBody.vue +40 -0
- package/src/components/admin-page/AdminHero.vue +32 -0
- package/src/components/admin-page/AdminPage.vue +16 -0
- package/src/components/admin-page/AdminSection.vue +38 -0
- package/src/components/plan-cockpit/PlanCockpit.vue +15 -0
- package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +8 -8
- package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +10 -11
- package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +8 -8
- package/src/components/plan-cockpit/PlanCockpitVersions.vue +9 -11
- package/src/components/plan-detail/PlanAuditLog.vue +7 -8
- package/src/components/plan-detail/PlanDetail.vue +14 -1
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
- package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
- package/src/components/plan-list/PlanList.vue +224 -257
- package/src/pages-standard/AdminLayout.vue +4 -2
- package/src/pages-standard/AuditPage.vue +18 -43
- package/src/pages-standard/BundlesPage.vue +17 -34
- package/src/pages-standard/DashboardPage.vue +20 -51
- package/src/pages-standard/DiscoveryPage.vue +38 -51
- package/src/pages-standard/EmailHistoryPage.vue +20 -71
- package/src/pages-standard/MarketingCatalogPage.vue +304 -315
- package/src/pages-standard/PilotsPage.vue +24 -56
- package/src/pages-standard/PlansPage.vue +25 -22
- package/src/pages-standard/PlatformEmailPage.vue +19 -51
- package/src/pages-standard/PromoCodeDetailPage.vue +39 -50
- package/src/pages-standard/PromoCodesPage.vue +8 -15
- package/src/pages-standard/SubscriptionsPage.vue +9 -35
- package/src/pages-standard/SuperAdminSetupWizard.vue +2 -1
- package/src/pages-standard/TenantDetailPage.vue +79 -174
- package/src/pages-standard/TenantsPage.vue +8 -33
- package/src/pages-standard/UsersPage.vue +21 -54
- package/src/pages-standard/bundles-page/BundlesToolbar.vue +67 -0
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
- package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +54 -62
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
- package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
- package/src/pages-standard/plans-page/PlansPageToast.vue +5 -5
- package/src/pages-standard/sa-theme.css +85 -29
- package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
- package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
- package/src/pages-standard/tenant-detail/TenantUsers.vue +21 -0
- package/src/pages-standard/tenant-detail/types.ts +38 -0
- package/src/vue/use-mfa-prompt.ts +72 -0
- package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
- package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
</p>
|
|
9
|
-
</div>
|
|
10
|
-
<div class="sa-page-head__actions">
|
|
2
|
+
<AdminPage class="sa-pilots">
|
|
3
|
+
<AdminHero
|
|
4
|
+
:title="msg.title"
|
|
5
|
+
:subtitle="formatMessage(msg.subtitleCount, { count: rows.length })"
|
|
6
|
+
>
|
|
7
|
+
<template #actions>
|
|
11
8
|
<slot name="head-actions">
|
|
12
9
|
<q-btn
|
|
13
10
|
v-if="enableCreate"
|
|
@@ -18,8 +15,8 @@
|
|
|
18
15
|
@click="showCreate = true"
|
|
19
16
|
/>
|
|
20
17
|
</slot>
|
|
21
|
-
</
|
|
22
|
-
</
|
|
18
|
+
</template>
|
|
19
|
+
</AdminHero>
|
|
23
20
|
|
|
24
21
|
<div class="sa-stats">
|
|
25
22
|
<button
|
|
@@ -98,22 +95,25 @@
|
|
|
98
95
|
|
|
99
96
|
<MfaPromptDialog
|
|
100
97
|
v-if="needsMfaDialog"
|
|
101
|
-
:model-value="
|
|
102
|
-
:description="
|
|
103
|
-
:error="
|
|
98
|
+
:model-value="mfa.show.value"
|
|
99
|
+
:description="mfa.description.value"
|
|
100
|
+
:error="mfa.error.value"
|
|
104
101
|
:setup-hint="mfaSetupHint"
|
|
105
|
-
@update:model-value="
|
|
106
|
-
@confirm="
|
|
102
|
+
@update:model-value="mfa.onVisibility"
|
|
103
|
+
@confirm="mfa.onConfirm"
|
|
107
104
|
/>
|
|
108
|
-
</
|
|
105
|
+
</AdminPage>
|
|
109
106
|
</template>
|
|
110
107
|
|
|
111
108
|
<script setup lang="ts">
|
|
112
109
|
import { computed, onMounted, ref } from 'vue';
|
|
110
|
+
import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
|
|
113
111
|
import { useQuasar } from 'quasar';
|
|
114
112
|
import { formatMessage } from '../client/i18n/format.js';
|
|
115
113
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
116
114
|
import { useSuperAdminNotify } from '../quasar/notify.js';
|
|
115
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
116
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
117
117
|
import PilotCreateDialog from '../components/dialogs/PilotCreateDialog.vue';
|
|
118
118
|
import PilotEditDialog from '../components/dialogs/PilotEditDialog.vue';
|
|
119
119
|
import MfaPromptDialog from '../components/MfaPromptDialog.vue';
|
|
@@ -219,38 +219,11 @@ const showEdit = ref(false);
|
|
|
219
219
|
const editRow = ref<PilotRow | null>(null);
|
|
220
220
|
const bakedPlanOptions = ref<PilotPlanOption[]>([]);
|
|
221
221
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
// (or cancellation via `update:modelValue=false`).
|
|
226
|
-
const showMfa = ref(false);
|
|
227
|
-
const mfaError = ref('');
|
|
228
|
-
const mfaDescription = ref('');
|
|
229
|
-
let pendingMfaResolve: ((code: string | null) => void) | null = null;
|
|
222
|
+
// Per-flow MFA for extend/revoke: `runWithMfa` awaits `mfa.prompt`, which
|
|
223
|
+
// resolves with the code or with null when the user closes the dialog.
|
|
224
|
+
const mfa = useMfaPrompt();
|
|
230
225
|
const needsMfaDialog = computed(() => props.requireMfaForExtend || props.requireMfaForRevoke);
|
|
231
226
|
|
|
232
|
-
function promptMfa(description: string): Promise<string | null> {
|
|
233
|
-
return new Promise((resolve) => {
|
|
234
|
-
mfaDescription.value = description;
|
|
235
|
-
mfaError.value = '';
|
|
236
|
-
showMfa.value = true;
|
|
237
|
-
pendingMfaResolve = (code) => {
|
|
238
|
-
pendingMfaResolve = null;
|
|
239
|
-
resolve(code);
|
|
240
|
-
};
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function onMfaConfirm(code: string): void {
|
|
245
|
-
pendingMfaResolve?.(code);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function onMfaDialogVisibility(open: boolean): void {
|
|
249
|
-
showMfa.value = open;
|
|
250
|
-
if (!open && pendingMfaResolve) {
|
|
251
|
-
pendingMfaResolve(null);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
227
|
// Consumers can set `createPlanOptions` instead of `loadPlanOptions`
|
|
255
228
|
// (e.g. apps with a hard-coded plan list).
|
|
256
229
|
const effectiveCreatePlanOptions = computed<readonly (string | PilotPlanOption)[]>(() => {
|
|
@@ -498,21 +471,21 @@ async function runAction(
|
|
|
498
471
|
// MFA loop: as long as the server returns 401, keep the dialog open and
|
|
499
472
|
// wait again for a code. Cancelling (resolver === null) ends it.
|
|
500
473
|
while (true) {
|
|
501
|
-
const code = await
|
|
474
|
+
const code = await mfa.prompt(actionLabel);
|
|
502
475
|
if (code === null) return;
|
|
503
476
|
try {
|
|
504
477
|
await invoke(code);
|
|
505
|
-
|
|
478
|
+
mfa.show.value = false;
|
|
506
479
|
notify('positive', successMessage);
|
|
507
480
|
await reload();
|
|
508
481
|
return;
|
|
509
482
|
} catch (err) {
|
|
510
483
|
const status = (err as { response?: { status?: number } })?.response?.status;
|
|
511
484
|
if (status === 401) {
|
|
512
|
-
|
|
485
|
+
mfa.error.value = msg.value.mfa.invalidOrNotSetUp;
|
|
513
486
|
continue;
|
|
514
487
|
}
|
|
515
|
-
|
|
488
|
+
mfa.show.value = false;
|
|
516
489
|
notify('negative', errMsg(err));
|
|
517
490
|
return;
|
|
518
491
|
}
|
|
@@ -575,11 +548,6 @@ function formatDate(iso: string | null | undefined): string | null {
|
|
|
575
548
|
</script>
|
|
576
549
|
|
|
577
550
|
<style scoped>
|
|
578
|
-
.sa-pilots {
|
|
579
|
-
min-height: calc(100vh - 56px);
|
|
580
|
-
background: var(--sa-bg-app, #f1f5f9);
|
|
581
|
-
padding: 20px 28px 28px;
|
|
582
|
-
}
|
|
583
551
|
.sa-pilots__card {
|
|
584
552
|
background: #fff;
|
|
585
553
|
border: 1px solid var(--sa-border, #e2e8f0);
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<AdminPage class="sa-plans">
|
|
3
|
+
<AdminHero :title="msg.list.title">
|
|
4
|
+
<template v-if="mode === 'list'" #actions>
|
|
5
|
+
<q-btn
|
|
6
|
+
flat
|
|
7
|
+
icon="grid_view"
|
|
8
|
+
:label="msg.list.matrixView"
|
|
9
|
+
@click="mode = 'matrix'"
|
|
10
|
+
/>
|
|
11
|
+
<q-btn
|
|
12
|
+
unelevated
|
|
13
|
+
color="primary"
|
|
14
|
+
icon="add"
|
|
15
|
+
:label="msg.list.newPlan"
|
|
16
|
+
@click="openCreate"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
19
|
+
</AdminHero>
|
|
20
|
+
|
|
3
21
|
<q-banner v-if="error" class="bg-negative text-white q-ma-md" rounded>
|
|
4
22
|
<template #avatar>
|
|
5
23
|
<q-icon name="error" />
|
|
@@ -10,12 +28,7 @@
|
|
|
10
28
|
</template>
|
|
11
29
|
</q-banner>
|
|
12
30
|
|
|
13
|
-
<
|
|
14
|
-
<q-spinner size="32px" />
|
|
15
|
-
<span>{{ msg.page.loading }}</span>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<template v-else>
|
|
31
|
+
<AdminBody :loading="loading && plans.length === 0" :loading-text="msg.page.loading">
|
|
19
32
|
<!-- Default: list view (plan simulation) + Bundle overview -->
|
|
20
33
|
<template v-if="mode === 'list'">
|
|
21
34
|
<PlanList
|
|
@@ -25,13 +38,11 @@
|
|
|
25
38
|
:plan-accents="props.planAccents"
|
|
26
39
|
:highlight-plan-key="highlightPlanKey"
|
|
27
40
|
@open-plan="onOpenPlan"
|
|
28
|
-
@create-plan="openCreate"
|
|
29
41
|
@clone-plan="onClonePlan"
|
|
30
42
|
@new-version="onNewVersionFromList"
|
|
31
43
|
@edit-draft="onEditDraftFromList"
|
|
32
44
|
@discard-draft="onDiscardDraftFromList"
|
|
33
45
|
@archive-plan="onArchivePlanFromList"
|
|
34
|
-
@show-matrix="mode = 'matrix'"
|
|
35
46
|
/>
|
|
36
47
|
<PlanBundleOverview
|
|
37
48
|
:bundles="availableBundles"
|
|
@@ -113,7 +124,7 @@
|
|
|
113
124
|
@save-and-exit="onReviewSaveExit"
|
|
114
125
|
@publish="onReviewPublish"
|
|
115
126
|
/>
|
|
116
|
-
</
|
|
127
|
+
</AdminBody>
|
|
117
128
|
|
|
118
129
|
<PlansPageToast :message="toastMessage" />
|
|
119
130
|
|
|
@@ -157,11 +168,14 @@
|
|
|
157
168
|
:format-change-value="formatChangeValue"
|
|
158
169
|
@execute="executePublish"
|
|
159
170
|
/>
|
|
160
|
-
</
|
|
171
|
+
</AdminPage>
|
|
161
172
|
</template>
|
|
162
173
|
|
|
163
174
|
<script setup lang="ts">
|
|
164
175
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
|
176
|
+
import AdminBody from '../components/admin-page/AdminBody.vue';
|
|
177
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
178
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
165
179
|
import type {
|
|
166
180
|
FeatureCatalogEntryRow,
|
|
167
181
|
PlanRow,
|
|
@@ -1238,17 +1252,6 @@ onMounted(async () => {
|
|
|
1238
1252
|
|
|
1239
1253
|
<style scoped>
|
|
1240
1254
|
.sa-plans {
|
|
1241
|
-
min-height: calc(100vh - 56px);
|
|
1242
|
-
background: #f6f7f9;
|
|
1243
1255
|
position: relative;
|
|
1244
1256
|
}
|
|
1245
|
-
.sa-plans__loading {
|
|
1246
|
-
display: flex;
|
|
1247
|
-
align-items: center;
|
|
1248
|
-
justify-content: center;
|
|
1249
|
-
gap: 12px;
|
|
1250
|
-
padding: 48px;
|
|
1251
|
-
color: #64748b;
|
|
1252
|
-
font-size: 14px;
|
|
1253
|
-
}
|
|
1254
1257
|
</style>
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<h1 class="sa-page-head__title">{{ resolvedTitle }}</h1>
|
|
6
|
-
<p class="sa-page-head__sub">{{ msg.provider.subtitle }}</p>
|
|
7
|
-
</div>
|
|
8
|
-
<div class="sa-page-head__actions">
|
|
2
|
+
<AdminPage class="sa-pemail">
|
|
3
|
+
<AdminHero :title="resolvedTitle" :subtitle="msg.provider.subtitle">
|
|
4
|
+
<template #actions>
|
|
9
5
|
<q-btn
|
|
10
6
|
v-if="rows.length === 0"
|
|
11
7
|
unelevated
|
|
@@ -15,8 +11,8 @@
|
|
|
15
11
|
@click="openCreate"
|
|
16
12
|
/>
|
|
17
13
|
<q-btn flat icon="refresh" :label="common.reload" @click="reload" />
|
|
18
|
-
</
|
|
19
|
-
</
|
|
14
|
+
</template>
|
|
15
|
+
</AdminHero>
|
|
20
16
|
|
|
21
17
|
<div class="sa-pemail__card">
|
|
22
18
|
<q-table
|
|
@@ -185,19 +181,22 @@
|
|
|
185
181
|
|
|
186
182
|
<MfaPromptDialog
|
|
187
183
|
v-if="requireMfaForWrite"
|
|
188
|
-
:model-value="
|
|
189
|
-
:description="
|
|
190
|
-
:error="
|
|
184
|
+
:model-value="mfa.show.value"
|
|
185
|
+
:description="mfa.description.value"
|
|
186
|
+
:error="mfa.error.value"
|
|
191
187
|
:setup-hint="mfaSetupHint"
|
|
192
|
-
@update:model-value="
|
|
193
|
-
@confirm="
|
|
188
|
+
@update:model-value="mfa.onVisibility"
|
|
189
|
+
@confirm="mfa.onConfirm"
|
|
194
190
|
/>
|
|
195
|
-
</
|
|
191
|
+
</AdminPage>
|
|
196
192
|
</template>
|
|
197
193
|
|
|
198
194
|
<script setup lang="ts">
|
|
199
195
|
import { computed, reactive, ref } from 'vue';
|
|
196
|
+
import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
|
|
200
197
|
import { useQuasar } from 'quasar';
|
|
198
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
199
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
201
200
|
import { useSuperAdminNotify } from '../quasar/notify.js';
|
|
202
201
|
import MfaPromptDialog from '../components/MfaPromptDialog.vue';
|
|
203
202
|
import { formatMessage } from '../client/i18n/format.js';
|
|
@@ -279,10 +278,7 @@ const testResult = ref<PlatformEmailTestResult | null>(null);
|
|
|
279
278
|
const testing = ref(false);
|
|
280
279
|
|
|
281
280
|
// MFA loop analogous to UsersPage (promise-resolver pattern).
|
|
282
|
-
const
|
|
283
|
-
const mfaError = ref('');
|
|
284
|
-
const mfaDescription = ref('');
|
|
285
|
-
let pendingMfaResolve: ((code: string | null) => void) | null = null;
|
|
281
|
+
const mfa = useMfaPrompt();
|
|
286
282
|
|
|
287
283
|
function emptyForm(): PlatformEmailWriteInput {
|
|
288
284
|
return {
|
|
@@ -298,29 +294,6 @@ function emptyForm(): PlatformEmailWriteInput {
|
|
|
298
294
|
};
|
|
299
295
|
}
|
|
300
296
|
|
|
301
|
-
function promptMfa(description: string): Promise<string | null> {
|
|
302
|
-
return new Promise((resolve) => {
|
|
303
|
-
mfaDescription.value = description;
|
|
304
|
-
mfaError.value = '';
|
|
305
|
-
showMfa.value = true;
|
|
306
|
-
pendingMfaResolve = (code) => {
|
|
307
|
-
pendingMfaResolve = null;
|
|
308
|
-
resolve(code);
|
|
309
|
-
};
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function onMfaConfirm(code: string): void {
|
|
314
|
-
pendingMfaResolve?.(code);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function onMfaDialogVisibility(open: boolean): void {
|
|
318
|
-
showMfa.value = open;
|
|
319
|
-
if (!open && pendingMfaResolve) {
|
|
320
|
-
pendingMfaResolve(null);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
297
|
function errMsg(err: unknown): string {
|
|
325
298
|
return (
|
|
326
299
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
|
@@ -381,19 +354,19 @@ async function runWrite(
|
|
|
381
354
|
}
|
|
382
355
|
}
|
|
383
356
|
for (;;) {
|
|
384
|
-
const code = await
|
|
357
|
+
const code = await mfa.prompt(label);
|
|
385
358
|
if (code === null) return false;
|
|
386
359
|
try {
|
|
387
360
|
await invoke(code);
|
|
388
|
-
|
|
361
|
+
mfa.show.value = false;
|
|
389
362
|
return true;
|
|
390
363
|
} catch (err) {
|
|
391
364
|
const status = (err as { response?: { status?: number } })?.response?.status;
|
|
392
365
|
if (status === 401) {
|
|
393
|
-
|
|
366
|
+
mfa.error.value = shell.value.mfa.invalidCode;
|
|
394
367
|
continue;
|
|
395
368
|
}
|
|
396
|
-
|
|
369
|
+
mfa.show.value = false;
|
|
397
370
|
notify('negative', errMsg(err));
|
|
398
371
|
return false;
|
|
399
372
|
}
|
|
@@ -484,11 +457,6 @@ async function onTest(): Promise<void> {
|
|
|
484
457
|
</script>
|
|
485
458
|
|
|
486
459
|
<style scoped>
|
|
487
|
-
.sa-pemail {
|
|
488
|
-
min-height: calc(100vh - 56px);
|
|
489
|
-
background: var(--sa-bg-app, #f1f5f9);
|
|
490
|
-
padding: 20px 28px 28px;
|
|
491
|
-
}
|
|
492
460
|
.sa-pemail__card {
|
|
493
461
|
background: #fff;
|
|
494
462
|
border: 1px solid var(--sa-border, #e2e8f0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
2
|
+
<AdminPage class="sa-promo-detail">
|
|
3
|
+
<AdminHero :title="data?.promo.code ?? labels.promoCode">
|
|
4
|
+
<template #before-title>
|
|
5
5
|
<q-btn
|
|
6
6
|
flat
|
|
7
7
|
dense
|
|
@@ -10,19 +10,18 @@
|
|
|
10
10
|
:to="backRoute"
|
|
11
11
|
class="sa-promo-detail__back"
|
|
12
12
|
/>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div class="sa-promo-detail__head-actions">
|
|
13
|
+
</template>
|
|
14
|
+
<template #subtitle>
|
|
15
|
+
<template v-if="data">
|
|
16
|
+
{{ data.promo.valueType }} · {{ data.promo.value }} ·
|
|
17
|
+
<q-badge
|
|
18
|
+
:color="resolveStatusColor(String(data.promo.status))"
|
|
19
|
+
:label="String(data.promo.status)"
|
|
20
|
+
/>
|
|
21
|
+
</template>
|
|
22
|
+
<template v-else>—</template>
|
|
23
|
+
</template>
|
|
24
|
+
<template #actions>
|
|
26
25
|
<q-btn
|
|
27
26
|
v-if="editSubmit && data"
|
|
28
27
|
color="primary"
|
|
@@ -32,36 +31,34 @@
|
|
|
32
31
|
@click="openEdit"
|
|
33
32
|
/>
|
|
34
33
|
<slot name="header-actions" />
|
|
35
|
-
</
|
|
36
|
-
</
|
|
37
|
-
|
|
38
|
-
<div class="sa-promo-detail__body">
|
|
39
|
-
<div v-if="loading" class="sa-promo-detail__state">
|
|
40
|
-
<q-spinner size="32px" /> {{ msg.detail.loading }}
|
|
41
|
-
</div>
|
|
42
|
-
<div v-else-if="!data" class="sa-promo-detail__state">{{ labels.empty }}</div>
|
|
34
|
+
</template>
|
|
35
|
+
</AdminHero>
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
<AdminBody
|
|
38
|
+
:loading="loading"
|
|
39
|
+
:empty="!data"
|
|
40
|
+
:loading-text="msg.detail.loading"
|
|
41
|
+
:empty-text="labels.empty"
|
|
42
|
+
>
|
|
43
|
+
<template v-if="data">
|
|
44
|
+
<AdminSection :title="labels.config" class="sa-card sa-promo-detail__section">
|
|
47
45
|
<slot name="config" :promo="data.promo">
|
|
48
46
|
<pre class="sa-promo-detail__kv">{{ resolveFormatPromo(data.promo) }}</pre>
|
|
49
47
|
</slot>
|
|
50
|
-
</
|
|
48
|
+
</AdminSection>
|
|
51
49
|
|
|
52
|
-
<
|
|
53
|
-
<div class="sa-promo-detail__section-head">{{ labels.stats }}</div>
|
|
50
|
+
<AdminSection :title="labels.stats" class="sa-card sa-promo-detail__section">
|
|
54
51
|
<slot name="stats" :stats="data.stats">
|
|
55
52
|
<pre class="sa-promo-detail__kv">{{
|
|
56
53
|
JSON.stringify(data.stats, null, 2)
|
|
57
54
|
}}</pre>
|
|
58
55
|
</slot>
|
|
59
|
-
</
|
|
56
|
+
</AdminSection>
|
|
60
57
|
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
<AdminSection
|
|
59
|
+
:title="`${labels.redemptions} (${data.redemptions.length})`"
|
|
60
|
+
class="sa-card sa-promo-detail__section"
|
|
61
|
+
>
|
|
65
62
|
<slot name="redemptions" :redemptions="data.redemptions">
|
|
66
63
|
<q-table
|
|
67
64
|
flat
|
|
@@ -72,11 +69,11 @@
|
|
|
72
69
|
hide-pagination
|
|
73
70
|
/>
|
|
74
71
|
</slot>
|
|
75
|
-
</
|
|
72
|
+
</AdminSection>
|
|
76
73
|
|
|
77
74
|
<slot name="extra-sections" :data="data" />
|
|
78
75
|
</template>
|
|
79
|
-
</
|
|
76
|
+
</AdminBody>
|
|
80
77
|
|
|
81
78
|
<PromoCodeEditDialog
|
|
82
79
|
v-if="editSubmit"
|
|
@@ -86,11 +83,15 @@
|
|
|
86
83
|
:submit="editSubmit"
|
|
87
84
|
@updated="onEditUpdated"
|
|
88
85
|
/>
|
|
89
|
-
</
|
|
86
|
+
</AdminPage>
|
|
90
87
|
</template>
|
|
91
88
|
|
|
92
89
|
<script setup lang="ts">
|
|
93
90
|
import { computed, onMounted, ref } from 'vue';
|
|
91
|
+
import AdminBody from '../components/admin-page/AdminBody.vue';
|
|
92
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
93
|
+
import AdminSection from '../components/admin-page/AdminSection.vue';
|
|
94
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
94
95
|
import type { RouteLocationRaw } from 'vue-router';
|
|
95
96
|
import type { QTableColumn } from 'quasar';
|
|
96
97
|
import PromoCodeEditDialog, {
|
|
@@ -283,10 +284,6 @@ const defaultColumns = computed<QTableColumn[]>(() => [
|
|
|
283
284
|
</script>
|
|
284
285
|
|
|
285
286
|
<style scoped>
|
|
286
|
-
.sa-promo-detail {
|
|
287
|
-
min-height: calc(100vh - 56px);
|
|
288
|
-
background: var(--sa-bg-app);
|
|
289
|
-
}
|
|
290
287
|
.sa-promo-detail__back {
|
|
291
288
|
margin-bottom: 6px;
|
|
292
289
|
}
|
|
@@ -297,7 +294,6 @@ const defaultColumns = computed<QTableColumn[]>(() => [
|
|
|
297
294
|
margin-left: auto;
|
|
298
295
|
}
|
|
299
296
|
.sa-promo-detail__body {
|
|
300
|
-
padding: 12px 28px 28px;
|
|
301
297
|
display: flex;
|
|
302
298
|
flex-direction: column;
|
|
303
299
|
gap: 14px;
|
|
@@ -324,11 +320,4 @@ const defaultColumns = computed<QTableColumn[]>(() => [
|
|
|
324
320
|
overflow-x: auto;
|
|
325
321
|
font-family: var(--sa-font-mono);
|
|
326
322
|
}
|
|
327
|
-
.sa-promo-detail__state {
|
|
328
|
-
padding: 24px;
|
|
329
|
-
display: flex;
|
|
330
|
-
align-items: center;
|
|
331
|
-
gap: 12px;
|
|
332
|
-
color: var(--sa-muted);
|
|
333
|
-
}
|
|
334
323
|
</style>
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<h1 class="sa-page-head__title">{{ msg.title }}</h1>
|
|
6
|
-
<p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
|
|
7
|
-
</div>
|
|
8
|
-
<div class="sa-page-head__actions">
|
|
2
|
+
<AdminPage class="sa-promo-codes">
|
|
3
|
+
<AdminHero :title="msg.title" :subtitle="subtitle">
|
|
4
|
+
<template #actions>
|
|
9
5
|
<slot name="head-actions">
|
|
10
6
|
<q-btn
|
|
11
7
|
v-if="enableCreate"
|
|
@@ -16,8 +12,8 @@
|
|
|
16
12
|
@click="showCreate = true"
|
|
17
13
|
/>
|
|
18
14
|
</slot>
|
|
19
|
-
</
|
|
20
|
-
</
|
|
15
|
+
</template>
|
|
16
|
+
</AdminHero>
|
|
21
17
|
|
|
22
18
|
<div class="sa-stats">
|
|
23
19
|
<button
|
|
@@ -108,7 +104,7 @@
|
|
|
108
104
|
:submit="submitEdit"
|
|
109
105
|
@updated="onUpdated"
|
|
110
106
|
/>
|
|
111
|
-
</
|
|
107
|
+
</AdminPage>
|
|
112
108
|
</template>
|
|
113
109
|
|
|
114
110
|
<script lang="ts">
|
|
@@ -137,6 +133,8 @@ export function computePlanColors(
|
|
|
137
133
|
|
|
138
134
|
<script setup lang="ts">
|
|
139
135
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
136
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
137
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
140
138
|
import { useQuasar } from 'quasar';
|
|
141
139
|
import { formatMessage } from '../client/i18n/format.js';
|
|
142
140
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
@@ -562,11 +560,6 @@ function formatDate(iso: string | Date | null | undefined): string | null {
|
|
|
562
560
|
</script>
|
|
563
561
|
|
|
564
562
|
<style scoped>
|
|
565
|
-
.sa-promo-codes {
|
|
566
|
-
min-height: calc(100vh - 56px);
|
|
567
|
-
background: var(--sa-bg-app, #f1f5f9);
|
|
568
|
-
padding: 20px 28px 28px;
|
|
569
|
-
}
|
|
570
563
|
.sa-promo-codes__filter {
|
|
571
564
|
display: flex;
|
|
572
565
|
gap: 10px;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<slot name="head-actions" />
|
|
9
|
-
</header>
|
|
2
|
+
<AdminPage class="sa-subscriptions">
|
|
3
|
+
<AdminHero :title="msg.subscriptions.title" :subtitle="subtitle">
|
|
4
|
+
<template #actions>
|
|
5
|
+
<slot name="head-actions" />
|
|
6
|
+
</template>
|
|
7
|
+
</AdminHero>
|
|
10
8
|
|
|
11
9
|
<div class="sa-subscriptions__body">
|
|
12
10
|
<div class="sa-subscriptions__card">
|
|
@@ -21,11 +19,13 @@
|
|
|
21
19
|
/>
|
|
22
20
|
</div>
|
|
23
21
|
</div>
|
|
24
|
-
</
|
|
22
|
+
</AdminPage>
|
|
25
23
|
</template>
|
|
26
24
|
|
|
27
25
|
<script setup lang="ts">
|
|
28
26
|
import { computed, onMounted, ref } from 'vue';
|
|
27
|
+
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
28
|
+
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
29
29
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
30
30
|
|
|
31
31
|
// Platform standard page: subscriptions (minimal). Apps pass
|
|
@@ -129,32 +129,6 @@ function formatDate(iso: string | null | undefined): string | null {
|
|
|
129
129
|
</script>
|
|
130
130
|
|
|
131
131
|
<style scoped>
|
|
132
|
-
.sa-subscriptions {
|
|
133
|
-
min-height: calc(100vh - 56px);
|
|
134
|
-
background: var(--sa-bg-app, #f1f5f9);
|
|
135
|
-
}
|
|
136
|
-
.sa-subscriptions__head {
|
|
137
|
-
padding: 20px 28px 8px;
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: flex-start;
|
|
140
|
-
justify-content: space-between;
|
|
141
|
-
gap: 16px;
|
|
142
|
-
}
|
|
143
|
-
.sa-subscriptions__title {
|
|
144
|
-
margin: 0;
|
|
145
|
-
font-family: var(--sa-font-head, system-ui, sans-serif);
|
|
146
|
-
font-weight: 700;
|
|
147
|
-
font-size: 22px;
|
|
148
|
-
color: var(--sa-heading, #0f172a);
|
|
149
|
-
}
|
|
150
|
-
.sa-subscriptions__sub {
|
|
151
|
-
margin: 4px 0 0;
|
|
152
|
-
color: var(--sa-muted-dark, #475569);
|
|
153
|
-
font-size: 13.5px;
|
|
154
|
-
}
|
|
155
|
-
.sa-subscriptions__body {
|
|
156
|
-
padding: 12px 28px 28px;
|
|
157
|
-
}
|
|
158
132
|
.sa-subscriptions__card {
|
|
159
133
|
background: #fff;
|
|
160
134
|
border: 1px solid var(--sa-border, #e2e8f0);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="sa-setup-head">
|
|
5
5
|
<div class="sa-setup-badge">{{ iconText }}</div>
|
|
6
6
|
<div>
|
|
7
|
-
<
|
|
7
|
+
<h1 class="sa-setup-title">{{ msg.setup.title }}</h1>
|
|
8
8
|
<div class="sa-setup-sub">{{ subtitleText }}</div>
|
|
9
9
|
</div>
|
|
10
10
|
<LocaleSwitcher class="sa-setup-locale" />
|
|
@@ -309,6 +309,7 @@ async function submitConfirm(): Promise<void> {
|
|
|
309
309
|
text-transform: uppercase;
|
|
310
310
|
}
|
|
311
311
|
.sa-setup-title {
|
|
312
|
+
margin: 0;
|
|
312
313
|
font-weight: 800;
|
|
313
314
|
font-size: 18px;
|
|
314
315
|
color: #0f172a;
|