@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.
Files changed (132) hide show
  1. package/dist/{catalog-BY_t9prv.d.cts → catalog-DJcfm5N2.d.cts} +20 -7
  2. package/dist/{catalog-BY_t9prv.d.ts → catalog-DJcfm5N2.d.ts} +20 -7
  3. package/dist/{chunk-JN6HZGLV.js → chunk-F7NRM6KI.js} +54 -28
  4. package/dist/{chunk-7IFKVGZV.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-OTP3OSSW.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +132 -29
  7. package/dist/client/index.d.cts +53 -3
  8. package/dist/client/index.d.ts +53 -3
  9. package/dist/client/index.js +14 -2
  10. package/dist/index.cjs +319 -162
  11. package/dist/index.d.cts +30 -8
  12. package/dist/index.d.ts +30 -8
  13. package/dist/index.js +176 -133
  14. package/dist/quasar/index.cjs +76 -29
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +6 -2
  18. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  19. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  20. package/dist/{use-super-admin-i18n-c_Eqsu9u.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-DlJRGIVq.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
  22. package/package.json +18 -8
  23. package/src/client/i18n/messages/bundles.ts +0 -2
  24. package/src/client/i18n/messages/common.ts +22 -0
  25. package/src/client/i18n/messages/discovery.ts +2 -2
  26. package/src/client/i18n/messages/marketing.ts +2 -4
  27. package/src/client/i18n/messages/plan-detail.ts +0 -4
  28. package/src/client/i18n/messages/plans.ts +20 -8
  29. package/src/client/i18n/messages/tenants.ts +0 -2
  30. package/src/client/index.ts +1 -0
  31. package/src/client/manifest-loader.ts +1 -1
  32. package/src/client/resolve-plans.ts +144 -0
  33. package/src/components/BundleVersionPublishDialog.vue +6 -6
  34. package/src/components/KvBlock.vue +4 -4
  35. package/src/components/MarketingPromotionsTab.vue +34 -48
  36. package/src/components/MfaPromptDialog.vue +8 -2
  37. package/src/components/TenantActionConfirmDialog.vue +4 -4
  38. package/src/components/VersionDiffPreview.vue +7 -7
  39. package/src/components/admin-page/AdminFilters.vue +12 -0
  40. package/src/components/admin-page/AdminHero.vue +22 -5
  41. package/src/components/admin-page/AdminKpi.vue +84 -0
  42. package/src/components/admin-page/AdminPaginator.vue +195 -0
  43. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  44. package/src/components/admin-page/AdminSection.vue +31 -16
  45. package/src/components/admin-page/AdminStatistics.vue +40 -0
  46. package/src/components/admin-page/AdminTable.vue +175 -0
  47. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  48. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  49. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  50. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  51. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  52. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  53. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  54. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  55. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  56. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  57. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  58. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  59. package/src/components/plan/PlanGrid.vue +1 -1
  60. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  61. package/src/components/plan-detail/PlanDetail.vue +92 -302
  62. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  63. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  64. package/src/components/plan-list/PlanList.vue +72 -290
  65. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  66. package/src/components/plan-review/PlanReview.vue +191 -275
  67. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  70. package/src/index.ts +1 -0
  71. package/src/pages-standard/AdminLayout.vue +34 -10
  72. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  73. package/src/pages-standard/AuditPage.vue +53 -72
  74. package/src/pages-standard/BundlesPage.vue +128 -144
  75. package/src/pages-standard/DashboardPage.vue +43 -107
  76. package/src/pages-standard/DiscoveryPage.vue +158 -200
  77. package/src/pages-standard/EmailHistoryPage.vue +98 -88
  78. package/src/pages-standard/MarketingCatalogPage.vue +237 -267
  79. package/src/pages-standard/PilotsPage.vue +44 -60
  80. package/src/pages-standard/PlansPage.vue +230 -105
  81. package/src/pages-standard/PlatformEmailPage.vue +37 -40
  82. package/src/pages-standard/PromoCodeDetailPage.vue +16 -30
  83. package/src/pages-standard/PromoCodesPage.vue +86 -112
  84. package/src/pages-standard/SubscriptionsPage.vue +10 -17
  85. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  86. package/src/pages-standard/SuperAdminSetupWizard.vue +20 -17
  87. package/src/pages-standard/TenantDetailPage.vue +45 -41
  88. package/src/pages-standard/TenantsPage.vue +184 -264
  89. package/src/pages-standard/UsersPage.vue +69 -104
  90. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  91. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  92. package/src/pages-standard/bundles-page/BundlesToolbar.vue +6 -10
  93. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  94. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  95. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  96. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  97. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  98. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +1 -1
  99. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +2 -13
  100. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  101. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  102. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  103. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  104. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  105. package/src/pages-standard/plans-page/PlansPageToast.vue +1 -1
  106. package/src/pages-standard/sa-theme.css +481 -71
  107. package/src/pages-standard/tenant-detail/TenantUsers.vue +2 -8
  108. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  109. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  110. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  111. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  112. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  113. package/src/quasar/create-super-admin-app.ts +4 -0
  114. package/src/vue/entitlement-provider.ts +0 -1
  115. package/src/vue/feature-router-guard.ts +0 -1
  116. package/src/vue/super-admin-context.ts +106 -1
  117. package/src/vue/use-api-list.ts +2 -1
  118. package/src/vue/use-pagination.ts +63 -0
  119. package/src/vue/use-plans.ts +1 -2
  120. package/src/vue/use-sign-out.ts +90 -0
  121. package/src/vue/use-tenant-billing.ts +1 -1
  122. package/src/vue/use-tenant-manifest.ts +0 -1
  123. package/src/components/KpiCard.vue +0 -53
  124. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1027
  125. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  126. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -99
  127. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  128. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  129. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  130. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -249
  131. package/src/components/plan-cockpit/types.ts +0 -72
  132. 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
- <q-btn
9
+ <button
10
10
  v-if="enableCreate"
11
- unelevated
12
- color="primary"
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
- <div class="sa-stats">
22
- <button
23
- v-for="tile in statTiles"
24
- :key="tile.id"
25
- class="sa-stat"
26
- :class="[
27
- tile.tone ? `sa-stat--${tile.tone}` : null,
28
- filter === tile.id ? 'sa-stat--active' : null,
29
- ]"
30
- @click="filter = tile.id"
31
- >
32
- <span class="sa-stat__num">{{ tile.count }}</span>
33
- <span class="sa-stat__label">{{ tile.label }}</span>
34
- <span v-if="tile.hint" class="sa-stat__hint">{{ tile.hint }}</span>
35
- </button>
36
- </div>
37
-
38
- <q-banner v-if="reviewSoon.length" class="bg-amber-2 text-grey-9 q-mb-md" rounded>
39
- <template #avatar><q-icon name="event" color="amber-9" /></template>
40
- {{ formatMessage(msg.list.reviewSoonBanner, { count: reviewSoon.length }) }}
41
- </q-banner>
42
-
43
- <div class="sa-pilots__card">
44
- <q-table
45
- flat
46
- :rows="filteredRows"
47
- :columns="effectiveColumns"
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
- </q-td>
68
- </template>
69
- </q-table>
70
- </div>
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="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"
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 :loading="loading && plans.length === 0" :loading-text="msg.page.loading">
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
- <PlanList
35
- :plans="plans"
36
- :versions-by-plan-id="versionsByPlanId"
37
- :tenant-counts-by-plan-key="tenantCountsByPlanKey"
38
- :plan-accents="props.planAccents"
39
- :highlight-plan-key="highlightPlanKey"
40
- @open-plan="onOpenPlan"
41
- @clone-plan="onClonePlan"
42
- @new-version="onNewVersionFromList"
43
- @edit-draft="onEditDraftFromList"
44
- @discard-draft="onDiscardDraftFromList"
45
- @archive-plan="onArchivePlanFromList"
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
- <PlanMatrix
56
- v-else-if="mode === 'matrix'"
57
- :plans="plans"
58
- :versions-by-plan-id="versionsByPlanId"
59
- :available-quotas="availableQuotas"
60
- :available-bundles="availableBundles"
61
- :feature-registry="featureRegistry"
62
- :tenant-counts-by-plan-key="tenantCountsByPlanKey"
63
- :plan-accents="props.planAccents"
64
- :loading="bulkVersionsLoading"
65
- @open-plan="onOpenPlan"
66
- @create-plan="openCreate"
67
- @clone-plan="onClonePlan"
68
- @view-catalog="mode = 'list'"
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
- <!-- V2 split-view editor as a full-screen view (plan simulation) -->
93
- <PlanVersionEditor
94
- v-else-if="mode === 'editor' && draftEditing && selectedPlan"
95
- :plan-key="selectedPlan.planKey"
96
- :editing-id="draftEditing.editingId"
97
- :initial-form="draftEditing.initialForm"
98
- :saving="draftSaving"
99
- :available-features="availableFeatures"
100
- :available-quotas="availableQuotas"
101
- :available-bundles="availableBundles"
102
- :feature-registry="featureRegistry"
103
- :plan-display-name="selectedPlan.label"
104
- :save-error="draftSaveError"
105
- :predecessor-version="editorPredecessor"
106
- @save="onEditorNext"
107
- @cancel="onCancelEditor"
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
- <!-- V3 Review & Publish — wizard step 3 (plan simulation) -->
111
- <PlanReview
112
- v-else-if="mode === 'review' && reviewDraft && selectedPlan"
113
- :plan="selectedPlan"
114
- :version="reviewDraft"
115
- :predecessor="reviewPredecessor"
116
- :available-quotas="availableQuotas"
117
- :available-bundles="availableBundles"
118
- :feature-registry="featureRegistry"
119
- :tenant-impact-count="tenantCountsByPlanKey[selectedPlan.planKey] ?? 0"
120
- :saving="draftSaving"
121
- :publishing="publishing"
122
- :publish-error="reviewError"
123
- @back="onReviewBack"
124
- @save-and-exit="onReviewSaveExit"
125
- @publish="onReviewPublish"
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 ?? ((url: string, init?: RequestInit) => fetch(url, init));
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 ?? ((url: string, init?: RequestInit) => fetch(url, init));
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
- // eslint-disable-next-line no-console
1206
+
1082
1207
  console.error('[PlansPage] Discard failed', err);
1083
1208
 
1084
1209
  const status = (err as { status?: number })?.status;