@saasicat/ui-vue 0.21.0 → 0.22.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-BY_t9prv.d.cts → catalog-DJcfm5N2.d.cts} +20 -7
- package/dist/{catalog-BY_t9prv.d.ts → catalog-DJcfm5N2.d.ts} +20 -7
- package/dist/{chunk-JN6HZGLV.js → chunk-F7NRM6KI.js} +54 -28
- package/dist/{chunk-7IFKVGZV.js → chunk-LY52T4N4.js} +21 -2
- package/dist/{chunk-OTP3OSSW.js → chunk-XMMFN2WJ.js} +74 -3
- package/dist/client/index.cjs +132 -29
- package/dist/client/index.d.cts +53 -3
- package/dist/client/index.d.ts +53 -3
- package/dist/client/index.js +14 -2
- package/dist/index.cjs +319 -162
- package/dist/index.d.cts +30 -8
- package/dist/index.d.ts +30 -8
- package/dist/index.js +176 -133
- package/dist/quasar/index.cjs +76 -29
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +6 -2
- package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
- package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
- package/dist/{use-super-admin-i18n-c_Eqsu9u.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
- package/dist/{use-super-admin-i18n-DlJRGIVq.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
- package/package.json +18 -8
- package/src/client/i18n/messages/bundles.ts +0 -2
- package/src/client/i18n/messages/common.ts +22 -0
- package/src/client/i18n/messages/discovery.ts +2 -2
- package/src/client/i18n/messages/marketing.ts +2 -4
- package/src/client/i18n/messages/plan-detail.ts +0 -4
- package/src/client/i18n/messages/plans.ts +20 -8
- package/src/client/i18n/messages/tenants.ts +0 -2
- package/src/client/index.ts +1 -0
- package/src/client/manifest-loader.ts +1 -1
- package/src/client/resolve-plans.ts +144 -0
- package/src/components/BundleVersionPublishDialog.vue +6 -6
- package/src/components/KvBlock.vue +4 -4
- package/src/components/MarketingPromotionsTab.vue +34 -48
- package/src/components/MfaPromptDialog.vue +8 -2
- package/src/components/TenantActionConfirmDialog.vue +4 -4
- package/src/components/VersionDiffPreview.vue +7 -7
- package/src/components/admin-page/AdminFilters.vue +12 -0
- package/src/components/admin-page/AdminHero.vue +22 -5
- package/src/components/admin-page/AdminKpi.vue +84 -0
- package/src/components/admin-page/AdminPaginator.vue +195 -0
- package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
- package/src/components/admin-page/AdminSection.vue +31 -16
- package/src/components/admin-page/AdminStatistics.vue +40 -0
- package/src/components/admin-page/AdminTable.vue +175 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
- package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
- package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
- package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
- package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
- package/src/components/dialogs/PilotCreateDialog.vue +12 -12
- package/src/components/dialogs/PilotEditDialog.vue +7 -7
- package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
- package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
- package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
- package/src/components/plan/PlanGrid.vue +1 -1
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
- package/src/components/plan-detail/PlanDetail.vue +92 -302
- package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
- package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
- package/src/components/plan-list/PlanList.vue +72 -290
- package/src/components/plan-matrix/PlanMatrix.vue +66 -183
- package/src/components/plan-review/PlanReview.vue +191 -275
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
- package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
- package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
- package/src/index.ts +1 -0
- package/src/pages-standard/AdminLayout.vue +34 -10
- package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
- package/src/pages-standard/AuditPage.vue +53 -72
- package/src/pages-standard/BundlesPage.vue +128 -144
- package/src/pages-standard/DashboardPage.vue +43 -107
- package/src/pages-standard/DiscoveryPage.vue +158 -200
- package/src/pages-standard/EmailHistoryPage.vue +98 -88
- package/src/pages-standard/MarketingCatalogPage.vue +237 -267
- package/src/pages-standard/PilotsPage.vue +44 -60
- package/src/pages-standard/PlansPage.vue +230 -105
- package/src/pages-standard/PlatformEmailPage.vue +37 -40
- package/src/pages-standard/PromoCodeDetailPage.vue +16 -30
- package/src/pages-standard/PromoCodesPage.vue +86 -112
- package/src/pages-standard/SubscriptionsPage.vue +10 -17
- package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
- package/src/pages-standard/SuperAdminSetupWizard.vue +20 -17
- package/src/pages-standard/TenantDetailPage.vue +45 -41
- package/src/pages-standard/TenantsPage.vue +184 -264
- package/src/pages-standard/UsersPage.vue +69 -104
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
- package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
- package/src/pages-standard/bundles-page/BundlesToolbar.vue +6 -10
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
- package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
- package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +1 -1
- package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +2 -13
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
- package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
- package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
- package/src/pages-standard/plans-page/PlansPageToast.vue +1 -1
- package/src/pages-standard/sa-theme.css +481 -71
- package/src/pages-standard/tenant-detail/TenantUsers.vue +2 -8
- package/src/pages-standard/tenants/StatusPill.vue +2 -2
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
- package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
- package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
- package/src/pages-tenant/TenantPlanSection.vue +12 -20
- package/src/quasar/create-super-admin-app.ts +4 -0
- package/src/vue/entitlement-provider.ts +0 -1
- package/src/vue/feature-router-guard.ts +0 -1
- package/src/vue/super-admin-context.ts +106 -1
- package/src/vue/use-api-list.ts +2 -1
- package/src/vue/use-pagination.ts +63 -0
- package/src/vue/use-plans.ts +1 -2
- package/src/vue/use-sign-out.ts +90 -0
- package/src/vue/use-tenant-billing.ts +1 -1
- package/src/vue/use-tenant-manifest.ts +0 -1
- package/src/components/KpiCard.vue +0 -53
- package/src/components/plan-cockpit/PlanCockpit.vue +0 -1027
- package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
- package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -99
- package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
- package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
- package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
- package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -249
- package/src/components/plan-cockpit/types.ts +0 -72
- package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
|
@@ -6,52 +6,46 @@
|
|
|
6
6
|
>
|
|
7
7
|
<template #actions>
|
|
8
8
|
<slot name="head-actions">
|
|
9
|
-
<
|
|
9
|
+
<button
|
|
10
10
|
v-if="enableCreate"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
icon="add"
|
|
14
|
-
:label="resolvedCreateLabel"
|
|
11
|
+
class="sa-btn sa-btn--primary"
|
|
12
|
+
type="button"
|
|
15
13
|
@click="showCreate = true"
|
|
16
|
-
|
|
14
|
+
>
|
|
15
|
+
<q-icon name="add" size="16px" />
|
|
16
|
+
<span>{{ resolvedCreateLabel }}</span>
|
|
17
|
+
</button>
|
|
17
18
|
</slot>
|
|
18
19
|
</template>
|
|
19
20
|
</AdminHero>
|
|
20
21
|
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tile.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
row-key="id"
|
|
49
|
-
:pagination="{ rowsPerPage: 0 }"
|
|
50
|
-
:loading="loading"
|
|
51
|
-
hide-pagination
|
|
52
|
-
>
|
|
53
|
-
<template #body-cell-actions="{ row }">
|
|
54
|
-
<q-td>
|
|
22
|
+
<AdminBody>
|
|
23
|
+
<AdminStatistics :label="msg.title">
|
|
24
|
+
<AdminKpi
|
|
25
|
+
v-for="tile in statTiles"
|
|
26
|
+
:key="tile.id"
|
|
27
|
+
:label="tile.label"
|
|
28
|
+
:value="tile.count"
|
|
29
|
+
:sub="tile.hint"
|
|
30
|
+
:tone="tile.tone"
|
|
31
|
+
:selected="filter === tile.id"
|
|
32
|
+
:action="() => (filter = tile.id)"
|
|
33
|
+
/>
|
|
34
|
+
</AdminStatistics>
|
|
35
|
+
|
|
36
|
+
<q-banner v-if="reviewSoon.length" class="bg-amber-2 text-grey-9 q-mb-md" rounded>
|
|
37
|
+
<template #avatar><q-icon name="event" color="amber-9" /></template>
|
|
38
|
+
{{ formatMessage(msg.list.reviewSoonBanner, { count: reviewSoon.length }) }}
|
|
39
|
+
</q-banner>
|
|
40
|
+
|
|
41
|
+
<AdminSection class="sa-pilots__card">
|
|
42
|
+
<AdminTable
|
|
43
|
+
:rows="filteredRows"
|
|
44
|
+
:columns="effectiveColumns"
|
|
45
|
+
:loading="loading"
|
|
46
|
+
storage-key="pilots"
|
|
47
|
+
>
|
|
48
|
+
<template #row-actions="{ row }">
|
|
55
49
|
<slot name="row-actions" :row="row">
|
|
56
50
|
<q-btn
|
|
57
51
|
v-for="action in visibleActions(row)"
|
|
@@ -64,10 +58,10 @@
|
|
|
64
58
|
@click="action.handler(row)"
|
|
65
59
|
/>
|
|
66
60
|
</slot>
|
|
67
|
-
</
|
|
68
|
-
</
|
|
69
|
-
</
|
|
70
|
-
</
|
|
61
|
+
</template>
|
|
62
|
+
</AdminTable>
|
|
63
|
+
</AdminSection>
|
|
64
|
+
</AdminBody>
|
|
71
65
|
|
|
72
66
|
<PilotCreateDialog
|
|
73
67
|
v-if="enableCreate && submitCreate"
|
|
@@ -106,14 +100,19 @@
|
|
|
106
100
|
</template>
|
|
107
101
|
|
|
108
102
|
<script setup lang="ts">
|
|
103
|
+
import AdminTable from '../components/admin-page/AdminTable.vue';
|
|
109
104
|
import { computed, onMounted, ref } from 'vue';
|
|
110
105
|
import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
|
|
111
106
|
import { useQuasar } from 'quasar';
|
|
112
107
|
import { formatMessage } from '../client/i18n/format.js';
|
|
113
108
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
114
109
|
import { useSuperAdminNotify } from '../quasar/notify.js';
|
|
110
|
+
import AdminBody from '../components/admin-page/AdminBody.vue';
|
|
115
111
|
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
112
|
+
import AdminSection from '../components/admin-page/AdminSection.vue';
|
|
113
|
+
import AdminKpi from '../components/admin-page/AdminKpi.vue';
|
|
116
114
|
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
115
|
+
import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
|
|
117
116
|
import PilotCreateDialog from '../components/dialogs/PilotCreateDialog.vue';
|
|
118
117
|
import PilotEditDialog from '../components/dialogs/PilotEditDialog.vue';
|
|
119
118
|
import MfaPromptDialog from '../components/MfaPromptDialog.vue';
|
|
@@ -361,14 +360,6 @@ const mergedActions = computed<readonly PilotRowAction[]>(() => [
|
|
|
361
360
|
|
|
362
361
|
const effectiveColumns = computed(() => {
|
|
363
362
|
const cols = [...baseColumns.value];
|
|
364
|
-
if (mergedActions.value.length > 0) {
|
|
365
|
-
cols.push({
|
|
366
|
-
name: 'actions',
|
|
367
|
-
label: '',
|
|
368
|
-
field: ((r: PilotRow) => r.id) as never,
|
|
369
|
-
align: 'right' as 'left',
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
363
|
return cols;
|
|
373
364
|
});
|
|
374
365
|
|
|
@@ -547,11 +538,4 @@ function formatDate(iso: string | null | undefined): string | null {
|
|
|
547
538
|
}
|
|
548
539
|
</script>
|
|
549
540
|
|
|
550
|
-
<style scoped>
|
|
551
|
-
.sa-pilots__card {
|
|
552
|
-
background: #fff;
|
|
553
|
-
border: 1px solid var(--sa-border, #e2e8f0);
|
|
554
|
-
border-radius: 12px;
|
|
555
|
-
overflow: hidden;
|
|
556
|
-
}
|
|
557
|
-
</style>
|
|
541
|
+
<style scoped></style>
|
|
@@ -1,21 +1,72 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<AdminPage class="sa-plans">
|
|
3
|
-
<AdminHero :title="
|
|
4
|
-
<template v-if="mode === '
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@click="mode = 'matrix'"
|
|
10
|
-
/>
|
|
11
|
-
<q-btn
|
|
12
|
-
unelevated
|
|
13
|
-
color="primary"
|
|
14
|
-
icon="add"
|
|
15
|
-
:label="msg.list.newPlan"
|
|
16
|
-
@click="openCreate"
|
|
3
|
+
<AdminHero v-if="!viewOwnsHero" :title="heroTitle" :subtitle="heroSubtitle">
|
|
4
|
+
<template v-if="mode === 'cockpit' && selectedPlan" #title>
|
|
5
|
+
<PlanTitleEdit
|
|
6
|
+
:plan="selectedPlan"
|
|
7
|
+
:editable="detailDraftVersion !== null"
|
|
8
|
+
@update-plan="onUpdatePlanFromDetail"
|
|
17
9
|
/>
|
|
18
10
|
</template>
|
|
11
|
+
|
|
12
|
+
<template v-if="mode === 'list'" #actions>
|
|
13
|
+
<button class="sa-btn" type="button" @click="mode = 'matrix'">
|
|
14
|
+
<q-icon name="grid_view" size="16px" />
|
|
15
|
+
<span>{{ msg.list.matrixView }}</span>
|
|
16
|
+
</button>
|
|
17
|
+
<button class="sa-btn sa-btn--primary" type="button" @click="openCreate">
|
|
18
|
+
<q-icon name="add" size="16px" />
|
|
19
|
+
<span>{{ msg.list.newPlan }}</span>
|
|
20
|
+
</button>
|
|
21
|
+
</template>
|
|
22
|
+
<template v-else-if="mode === 'matrix'" #actions>
|
|
23
|
+
<button class="sa-btn" type="button" @click="mode = 'list'">
|
|
24
|
+
<q-icon name="storefront" size="16px" />
|
|
25
|
+
<span>{{ msg.matrix.catalogPreview }}</span>
|
|
26
|
+
</button>
|
|
27
|
+
<button class="sa-btn sa-btn--primary" type="button" @click="openCreate">
|
|
28
|
+
<q-icon name="add" size="16px" />
|
|
29
|
+
<span>{{ msg.matrix.createPlan }}</span>
|
|
30
|
+
</button>
|
|
31
|
+
</template>
|
|
32
|
+
<template v-else-if="mode === 'cockpit' && selectedPlan" #actions>
|
|
33
|
+
<button class="sa-btn" type="button" @click="onBackToList">
|
|
34
|
+
<q-icon name="arrow_back" size="16px" />
|
|
35
|
+
<span>{{ common.back }}</span>
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
v-if="detailIsDeletable"
|
|
39
|
+
class="sa-btn sa-btn--danger"
|
|
40
|
+
type="button"
|
|
41
|
+
:title="planDetailMsg.header.deletePlanTitle"
|
|
42
|
+
@click="onArchiveSelectedPlan"
|
|
43
|
+
>
|
|
44
|
+
<q-icon name="delete_outline" size="16px" />
|
|
45
|
+
<span>{{ planDetailMsg.header.deletePlan }}</span>
|
|
46
|
+
</button>
|
|
47
|
+
<button class="sa-btn" type="button" @click="onCloneSelectedPlan">
|
|
48
|
+
<q-icon name="content_copy" size="16px" />
|
|
49
|
+
<span>{{ planDetailMsg.header.clonePlan }}</span>
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
v-if="!detailDraftVersion"
|
|
53
|
+
class="sa-btn sa-btn--primary"
|
|
54
|
+
type="button"
|
|
55
|
+
@click="openCreateDraft"
|
|
56
|
+
>
|
|
57
|
+
<q-icon name="add" size="16px" />
|
|
58
|
+
<span>{{ planDetailMsg.header.newDraftVersion }}</span>
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
v-else
|
|
62
|
+
class="sa-btn sa-btn--primary"
|
|
63
|
+
type="button"
|
|
64
|
+
@click="openPublish(detailDraftVersion)"
|
|
65
|
+
>
|
|
66
|
+
<q-icon name="bolt" size="16px" />
|
|
67
|
+
<span>{{ publishDraftLabel }}</span>
|
|
68
|
+
</button>
|
|
69
|
+
</template>
|
|
19
70
|
</AdminHero>
|
|
20
71
|
|
|
21
72
|
<q-banner v-if="error" class="bg-negative text-white q-ma-md" rounded>
|
|
@@ -28,22 +79,44 @@
|
|
|
28
79
|
</template>
|
|
29
80
|
</q-banner>
|
|
30
81
|
|
|
31
|
-
<AdminBody
|
|
82
|
+
<AdminBody
|
|
83
|
+
v-if="!viewOwnsHero"
|
|
84
|
+
:loading="loading && plans.length === 0"
|
|
85
|
+
:loading-text="msg.page.loading"
|
|
86
|
+
>
|
|
32
87
|
<!-- Default: list view (plan simulation) + Bundle overview -->
|
|
33
88
|
<template v-if="mode === 'list'">
|
|
34
|
-
<
|
|
35
|
-
:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
<AdminSection>
|
|
90
|
+
<AdminStatistics :columns="4">
|
|
91
|
+
<AdminKpi :label="msg.list.statPlans" :value="planCounts.plans" />
|
|
92
|
+
<AdminKpi
|
|
93
|
+
:label="msg.list.statLive"
|
|
94
|
+
:value="planCounts.live"
|
|
95
|
+
tone="positive"
|
|
96
|
+
/>
|
|
97
|
+
<AdminKpi
|
|
98
|
+
:label="msg.list.statDrafts"
|
|
99
|
+
:value="planCounts.drafts"
|
|
100
|
+
:tone="planCounts.drafts > 0 ? 'warn' : 'neutral'"
|
|
101
|
+
/>
|
|
102
|
+
<AdminKpi :label="msg.list.statTenants" :value="planCounts.tenants" />
|
|
103
|
+
</AdminStatistics>
|
|
104
|
+
</AdminSection>
|
|
105
|
+
<AdminSection>
|
|
106
|
+
<PlanList
|
|
107
|
+
:plans="plans"
|
|
108
|
+
:versions-by-plan-id="versionsByPlanId"
|
|
109
|
+
:tenant-counts-by-plan-key="tenantCountsByPlanKey"
|
|
110
|
+
:plan-accents="props.planAccents"
|
|
111
|
+
:highlight-plan-key="highlightPlanKey"
|
|
112
|
+
@open-plan="onOpenPlan"
|
|
113
|
+
@clone-plan="onClonePlan"
|
|
114
|
+
@new-version="onNewVersionFromList"
|
|
115
|
+
@edit-draft="onEditDraftFromList"
|
|
116
|
+
@discard-draft="onDiscardDraftFromList"
|
|
117
|
+
@archive-plan="onArchivePlanFromList"
|
|
118
|
+
/>
|
|
119
|
+
</AdminSection>
|
|
47
120
|
<PlanBundleOverview
|
|
48
121
|
:bundles="availableBundles"
|
|
49
122
|
:plans="plans"
|
|
@@ -52,21 +125,21 @@
|
|
|
52
125
|
</template>
|
|
53
126
|
|
|
54
127
|
<!-- V1 matrix: component comparison -->
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
128
|
+
<AdminSection v-else-if="mode === 'matrix'">
|
|
129
|
+
<PlanMatrix
|
|
130
|
+
:plans="plans"
|
|
131
|
+
:versions-by-plan-id="versionsByPlanId"
|
|
132
|
+
:available-quotas="availableQuotas"
|
|
133
|
+
:available-bundles="availableBundles"
|
|
134
|
+
:feature-registry="featureRegistry"
|
|
135
|
+
:tenant-counts-by-plan-key="tenantCountsByPlanKey"
|
|
136
|
+
:plan-accents="props.planAccents"
|
|
137
|
+
:loading="bulkVersionsLoading"
|
|
138
|
+
@open-plan="onOpenPlan"
|
|
139
|
+
@create-plan="openCreate"
|
|
140
|
+
@clone-plan="onClonePlan"
|
|
141
|
+
/>
|
|
142
|
+
</AdminSection>
|
|
70
143
|
|
|
71
144
|
<!-- Plan detail: drill-in for a single Plan (plan simulation) -->
|
|
72
145
|
<PlanDetail
|
|
@@ -80,51 +153,47 @@
|
|
|
80
153
|
:audit-rows="auditRows"
|
|
81
154
|
:loading-audit="loadingAudit"
|
|
82
155
|
:submit-terminate="onSubmitTerminate"
|
|
83
|
-
@back="onBackToList"
|
|
84
156
|
@create-draft="openCreateDraft"
|
|
85
157
|
@edit-draft="openEditDraft"
|
|
86
158
|
@publish="openPublish"
|
|
87
|
-
@clone-plan="onClonePlan(selectedPlan)"
|
|
88
|
-
@delete-plan="onArchivePlanFromList(selectedPlan, false)"
|
|
89
|
-
@update-plan="onUpdatePlanFromDetail"
|
|
90
159
|
/>
|
|
160
|
+
</AdminBody>
|
|
91
161
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
<!-- V2 split-view editor as a full-screen view (plan simulation) -->
|
|
163
|
+
<PlanVersionEditor
|
|
164
|
+
v-if="mode === 'editor' && draftEditing && selectedPlan"
|
|
165
|
+
:plan-key="selectedPlan.planKey"
|
|
166
|
+
:editing-id="draftEditing.editingId"
|
|
167
|
+
:initial-form="draftEditing.initialForm"
|
|
168
|
+
:saving="draftSaving"
|
|
169
|
+
:available-features="availableFeatures"
|
|
170
|
+
:available-quotas="availableQuotas"
|
|
171
|
+
:available-bundles="availableBundles"
|
|
172
|
+
:feature-registry="featureRegistry"
|
|
173
|
+
:plan-display-name="selectedPlan.label"
|
|
174
|
+
:save-error="draftSaveError"
|
|
175
|
+
:predecessor-version="editorPredecessor"
|
|
176
|
+
@save="onEditorNext"
|
|
177
|
+
@cancel="onCancelEditor"
|
|
178
|
+
/>
|
|
109
179
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
</AdminBody>
|
|
180
|
+
<!-- V3 Review & Publish — wizard step 3 (plan simulation) -->
|
|
181
|
+
<PlanReview
|
|
182
|
+
v-else-if="mode === 'review' && reviewDraft && selectedPlan"
|
|
183
|
+
:plan="selectedPlan"
|
|
184
|
+
:version="reviewDraft"
|
|
185
|
+
:predecessor="reviewPredecessor"
|
|
186
|
+
:available-quotas="availableQuotas"
|
|
187
|
+
:available-bundles="availableBundles"
|
|
188
|
+
:feature-registry="featureRegistry"
|
|
189
|
+
:tenant-impact-count="tenantCountsByPlanKey[selectedPlan.planKey] ?? 0"
|
|
190
|
+
:saving="draftSaving"
|
|
191
|
+
:publishing="publishing"
|
|
192
|
+
:publish-error="reviewError"
|
|
193
|
+
@back="onReviewBack"
|
|
194
|
+
@save-and-exit="onReviewSaveExit"
|
|
195
|
+
@publish="onReviewPublish"
|
|
196
|
+
/>
|
|
128
197
|
|
|
129
198
|
<PlansPageToast :message="toastMessage" />
|
|
130
199
|
|
|
@@ -174,8 +243,12 @@
|
|
|
174
243
|
<script setup lang="ts">
|
|
175
244
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
|
176
245
|
import AdminBody from '../components/admin-page/AdminBody.vue';
|
|
246
|
+
import { countPlans, resolvePlans } from '../client/resolve-plans.js';
|
|
177
247
|
import AdminHero from '../components/admin-page/AdminHero.vue';
|
|
248
|
+
import AdminSection from '../components/admin-page/AdminSection.vue';
|
|
178
249
|
import AdminPage from '../components/admin-page/AdminPage.vue';
|
|
250
|
+
import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
|
|
251
|
+
import AdminKpi from '../components/admin-page/AdminKpi.vue';
|
|
179
252
|
import type {
|
|
180
253
|
FeatureCatalogEntryRow,
|
|
181
254
|
PlanRow,
|
|
@@ -196,9 +269,11 @@ import type { HttpClient } from '../client/types.js';
|
|
|
196
269
|
import type { SaMessages } from '../client/i18n/index.js';
|
|
197
270
|
import { formatMessage } from '../client/i18n/format.js';
|
|
198
271
|
import { useSaMessages } from '../vue/use-super-admin-i18n.js';
|
|
272
|
+
import { useSuperAdminHttp } from '../vue/use-super-admin-context.js';
|
|
199
273
|
import PlanVersionEditor from '../components/plan-version-editor/PlanVersionEditor.vue';
|
|
200
274
|
import PlanMatrix from '../components/plan-matrix/PlanMatrix.vue';
|
|
201
275
|
import PlanDetail from '../components/plan-detail/PlanDetail.vue';
|
|
276
|
+
import PlanTitleEdit from '../components/plan-detail/PlanTitleEdit.vue';
|
|
202
277
|
import PlanList from '../components/plan-list/PlanList.vue';
|
|
203
278
|
import PlanCreateDialog, {
|
|
204
279
|
type PlanCreateSubmit,
|
|
@@ -265,6 +340,64 @@ const props = defineProps<{
|
|
|
265
340
|
}>();
|
|
266
341
|
|
|
267
342
|
const msg = useSaMessages('plans');
|
|
343
|
+
// The plan detail's own strings: its header moved into this page's hero.
|
|
344
|
+
const planDetailMsg = useSaMessages('planDetail');
|
|
345
|
+
|
|
346
|
+
// Fallback for the requests this page issues directly. It resolves to the
|
|
347
|
+
// client the app registered via `createSuperAdminApp({ http })`, or to
|
|
348
|
+
// `defaultHttpClient()` — never to a bare `fetch()`, which would drop the
|
|
349
|
+
// app's Authorization header on exactly these calls.
|
|
350
|
+
const shellHttp = useSuperAdminHttp();
|
|
351
|
+
|
|
352
|
+
// The same derivation PlanList uses for its rows, so the counts above the list
|
|
353
|
+
// and the list itself cannot drift apart.
|
|
354
|
+
const planCounts = computed(() =>
|
|
355
|
+
countPlans(
|
|
356
|
+
resolvePlans({
|
|
357
|
+
plans: plans.value,
|
|
358
|
+
versionsByPlanId: versionsByPlanId.value,
|
|
359
|
+
tenantCountsByPlanKey: tenantCountsByPlanKey.value,
|
|
360
|
+
}),
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// The editor and the review render the hero themselves: their actions hang on
|
|
365
|
+
// state that lives in those views — the publish checklist, the force flags, the
|
|
366
|
+
// draft's validity — so the page steps aside rather than lifting all of it up.
|
|
367
|
+
const viewOwnsHero = computed(() => mode.value === 'editor' || mode.value === 'review');
|
|
368
|
+
|
|
369
|
+
// One page, one heading. Every view here — the list, the matrix, a single plan
|
|
370
|
+
// — is a mode of the same page, so each swaps the hero's title and actions
|
|
371
|
+
// instead of rendering a second header underneath it. The plan detail used to
|
|
372
|
+
// do the latter, which put a competing <h1> on the page.
|
|
373
|
+
const heroTitle = computed(() => {
|
|
374
|
+
if (mode.value === 'cockpit' && selectedPlan.value) return selectedPlan.value.label;
|
|
375
|
+
return mode.value === 'matrix' ? msg.value.matrix.title : msg.value.list.title;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const heroSubtitle = computed(() => {
|
|
379
|
+
if (mode.value !== 'cockpit' || !selectedPlan.value) return msg.value.page.subtitle;
|
|
380
|
+
return selectedPlan.value.description || planDetailMsg.value.header.noDescription;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const detailDraftVersion = computed(
|
|
384
|
+
() => versions.value.find((v) => v.publishedAt === null) ?? null,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Deleting a plan is only offered while nothing depends on it: no tenants, no
|
|
388
|
+
// published version, no open draft.
|
|
389
|
+
const detailIsDeletable = computed(
|
|
390
|
+
() =>
|
|
391
|
+
detailDraftVersion.value === null &&
|
|
392
|
+
versions.value.every((v) => v.publishedAt === null) &&
|
|
393
|
+
Object.values(impactByVersion.value).every((n) => n === 0),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const publishDraftLabel = computed(() =>
|
|
397
|
+
formatMessage(planDetailMsg.value.header.publishDraft, {
|
|
398
|
+
version: detailDraftVersion.value?.version ?? '',
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
268
401
|
const common = useSaMessages('common');
|
|
269
402
|
|
|
270
403
|
const composable: UsePlansResult = usePlans({
|
|
@@ -329,22 +462,6 @@ const tenantCountsByPlanKey = computed<Record<string, number>>(
|
|
|
329
462
|
() => props.tenantCountsByPlanKey ?? loadedTenantCounts.value,
|
|
330
463
|
);
|
|
331
464
|
|
|
332
|
-
function planAccent(planKey: string): string {
|
|
333
|
-
return props.planAccents?.[planKey] ?? defaultAccent(planKey);
|
|
334
|
-
}
|
|
335
|
-
const DEFAULT_ACCENTS: Record<string, string> = {
|
|
336
|
-
STARTER: '#64748b',
|
|
337
|
-
STANDARD: '#2563eb',
|
|
338
|
-
PRO: '#7c3aed',
|
|
339
|
-
PROFESSIONAL: '#7c3aed',
|
|
340
|
-
BUSINESS: '#0ea5e9',
|
|
341
|
-
ENTERPRISE: '#0f766e',
|
|
342
|
-
BASIC: '#475569',
|
|
343
|
-
};
|
|
344
|
-
function defaultAccent(planKey: string): string {
|
|
345
|
-
return DEFAULT_ACCENTS[planKey] ?? '#2563eb';
|
|
346
|
-
}
|
|
347
|
-
|
|
348
465
|
// ─── Plan master creation (V1 plan simulation step 1) ───
|
|
349
466
|
const createDialogOpen = ref(false);
|
|
350
467
|
const creatingPlan = ref(false);
|
|
@@ -432,7 +549,7 @@ async function reloadAllVersions(): Promise<void> {
|
|
|
432
549
|
if (plans.value.length === 0) return;
|
|
433
550
|
bulkVersionsLoading.value = true;
|
|
434
551
|
try {
|
|
435
|
-
const http = props.http ??
|
|
552
|
+
const http = props.http ?? shellHttp;
|
|
436
553
|
const token = props.getAuthToken?.();
|
|
437
554
|
const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
|
|
438
555
|
const results = await Promise.all(
|
|
@@ -553,6 +670,18 @@ async function onEditDraftFromList(plan: PlanRow, draft: PlanVersionRow): Promis
|
|
|
553
670
|
openEditDraft(draft);
|
|
554
671
|
}
|
|
555
672
|
|
|
673
|
+
// The hero's detail actions only render while a plan is selected, but a
|
|
674
|
+
// template condition on the surrounding slot does not narrow `selectedPlan`
|
|
675
|
+
// inside a handler expression — a consumer's stricter build rejects it. These
|
|
676
|
+
// carry the guard in code, where the type checker can see it.
|
|
677
|
+
function onCloneSelectedPlan(): void {
|
|
678
|
+
if (selectedPlan.value) onClonePlan(selectedPlan.value);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function onArchiveSelectedPlan(): void {
|
|
682
|
+
if (selectedPlan.value) onArchivePlanFromList(selectedPlan.value, false);
|
|
683
|
+
}
|
|
684
|
+
|
|
556
685
|
function onBackToList(): void {
|
|
557
686
|
mode.value = 'list';
|
|
558
687
|
selectedPlan.value = null;
|
|
@@ -569,7 +698,6 @@ async function onUpdatePlanFromDetail(patch: { label: string }): Promise<void> {
|
|
|
569
698
|
const updated = await update(selectedPlan.value.id, patch);
|
|
570
699
|
selectedPlan.value = updated;
|
|
571
700
|
} catch (err: unknown) {
|
|
572
|
-
// eslint-disable-next-line no-console
|
|
573
701
|
console.error('[PlansPage] Plan rename failed', err);
|
|
574
702
|
}
|
|
575
703
|
}
|
|
@@ -592,7 +720,7 @@ const featureRegistry = computed<Record<string, { label?: string; group?: string
|
|
|
592
720
|
}));
|
|
593
721
|
|
|
594
722
|
async function loadEditorSources(): Promise<void> {
|
|
595
|
-
const http = props.http ??
|
|
723
|
+
const http = props.http ?? shellHttp;
|
|
596
724
|
const token = props.getAuthToken?.();
|
|
597
725
|
const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
|
|
598
726
|
try {
|
|
@@ -940,7 +1068,6 @@ async function onReviewSaveExit(): Promise<void> {
|
|
|
940
1068
|
draftEditing.value = null;
|
|
941
1069
|
mode.value = selectedPlan.value ? 'cockpit' : 'list';
|
|
942
1070
|
} catch (err: unknown) {
|
|
943
|
-
// eslint-disable-next-line no-console
|
|
944
1071
|
console.error('[PlansPage] Saving the draft failed', err);
|
|
945
1072
|
reviewError.value = describeDraftSaveError(err);
|
|
946
1073
|
} finally {
|
|
@@ -977,7 +1104,6 @@ async function onReviewPublish(payload: {
|
|
|
977
1104
|
draftEditing.value = null;
|
|
978
1105
|
mode.value = 'cockpit';
|
|
979
1106
|
} catch (err: unknown) {
|
|
980
|
-
// eslint-disable-next-line no-console
|
|
981
1107
|
console.error('[PlansPage] Publishing failed', err);
|
|
982
1108
|
reviewError.value = describePublishError(err);
|
|
983
1109
|
} finally {
|
|
@@ -1011,7 +1137,6 @@ async function executeArchive(): Promise<void> {
|
|
|
1011
1137
|
archiveOpen.value = false;
|
|
1012
1138
|
flashToast(formatMessage(msg.value.page.toastPlanDeleted, { planKey: plan.planKey }));
|
|
1013
1139
|
} catch (err: unknown) {
|
|
1014
|
-
// eslint-disable-next-line no-console
|
|
1015
1140
|
console.error('[PlansPage] Archive/purge failed', err);
|
|
1016
1141
|
const status = (err as { status?: number })?.status;
|
|
1017
1142
|
const body = (err as { body?: { code?: string; message?: string } })?.body;
|
|
@@ -1078,7 +1203,7 @@ async function executeDiscard(): Promise<void> {
|
|
|
1078
1203
|
} catch (err: unknown) {
|
|
1079
1204
|
// Full error object to the console — in the diagnostic case you can see
|
|
1080
1205
|
// e.g. CORS preflight errors or the real network stack there.
|
|
1081
|
-
|
|
1206
|
+
|
|
1082
1207
|
console.error('[PlansPage] Discard failed', err);
|
|
1083
1208
|
|
|
1084
1209
|
const status = (err as { status?: number })?.status;
|