@saasicat/ui-vue 0.19.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 (145) hide show
  1. package/dist/{catalog-JXdKdIqG.d.cts → catalog-DJcfm5N2.d.cts} +21 -7
  2. package/dist/{catalog-JXdKdIqG.d.ts → catalog-DJcfm5N2.d.ts} +21 -7
  3. package/dist/{chunk-DPTIOQXX.js → chunk-F7NRM6KI.js} +56 -28
  4. package/dist/{chunk-RVG6LQHL.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-ORX3ZG47.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +134 -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 +321 -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 +78 -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-BlqcD0N7.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-1nCOkvgi.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 +24 -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/AdminBody.vue +40 -0
  40. package/src/components/admin-page/AdminFilters.vue +12 -0
  41. package/src/components/admin-page/AdminHero.vue +49 -0
  42. package/src/components/admin-page/AdminKpi.vue +84 -0
  43. package/src/components/admin-page/AdminPage.vue +16 -0
  44. package/src/components/admin-page/AdminPaginator.vue +195 -0
  45. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  46. package/src/components/admin-page/AdminSection.vue +53 -0
  47. package/src/components/admin-page/AdminStatistics.vue +40 -0
  48. package/src/components/admin-page/AdminTable.vue +175 -0
  49. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  50. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  51. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  52. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  53. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  54. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  55. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  56. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  57. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  58. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  59. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  60. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  61. package/src/components/plan/PlanGrid.vue +1 -1
  62. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  63. package/src/components/plan-detail/PlanAuditLog.vue +7 -8
  64. package/src/components/plan-detail/PlanDetail.vue +95 -292
  65. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  66. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  67. package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
  68. package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
  69. package/src/components/plan-list/PlanList.vue +206 -457
  70. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  71. package/src/components/plan-review/PlanReview.vue +191 -275
  72. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  73. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  74. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  75. package/src/index.ts +1 -0
  76. package/src/pages-standard/AdminLayout.vue +38 -12
  77. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  78. package/src/pages-standard/AuditPage.vue +65 -109
  79. package/src/pages-standard/BundlesPage.vue +146 -179
  80. package/src/pages-standard/DashboardPage.vue +59 -154
  81. package/src/pages-standard/DiscoveryPage.vue +172 -227
  82. package/src/pages-standard/EmailHistoryPage.vue +118 -159
  83. package/src/pages-standard/MarketingCatalogPage.vue +399 -440
  84. package/src/pages-standard/PilotsPage.vue +69 -117
  85. package/src/pages-standard/PlansPage.vue +240 -112
  86. package/src/pages-standard/PlatformEmailPage.vue +56 -91
  87. package/src/pages-standard/PromoCodeDetailPage.vue +49 -74
  88. package/src/pages-standard/PromoCodesPage.vue +95 -128
  89. package/src/pages-standard/SubscriptionsPage.vue +19 -52
  90. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  91. package/src/pages-standard/SuperAdminSetupWizard.vue +22 -18
  92. package/src/pages-standard/TenantDetailPage.vue +100 -191
  93. package/src/pages-standard/TenantsPage.vue +190 -295
  94. package/src/pages-standard/UsersPage.vue +89 -157
  95. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  96. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  97. package/src/pages-standard/bundles-page/BundlesToolbar.vue +63 -0
  98. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  99. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  100. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  101. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  102. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  103. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
  104. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +43 -62
  105. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
  106. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
  107. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  108. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  109. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  110. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  111. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  112. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  113. package/src/pages-standard/sa-theme.css +552 -86
  114. package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
  115. package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
  116. package/src/pages-standard/tenant-detail/TenantUsers.vue +15 -0
  117. package/src/pages-standard/tenant-detail/types.ts +38 -0
  118. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  119. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  120. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  121. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  122. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  123. package/src/quasar/create-super-admin-app.ts +4 -0
  124. package/src/vue/entitlement-provider.ts +0 -1
  125. package/src/vue/feature-router-guard.ts +0 -1
  126. package/src/vue/super-admin-context.ts +106 -1
  127. package/src/vue/use-api-list.ts +2 -1
  128. package/src/vue/use-mfa-prompt.ts +72 -0
  129. package/src/vue/use-pagination.ts +63 -0
  130. package/src/vue/use-plans.ts +1 -2
  131. package/src/vue/use-sign-out.ts +90 -0
  132. package/src/vue/use-tenant-billing.ts +1 -1
  133. package/src/vue/use-tenant-manifest.ts +0 -1
  134. package/src/components/KpiCard.vue +0 -53
  135. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1012
  136. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  137. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -100
  138. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  139. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  140. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  141. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -251
  142. package/src/components/plan-cockpit/types.ts +0 -72
  143. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
  144. package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
  145. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
@@ -1,5 +1,74 @@
1
1
  <template>
2
- <q-page class="sa-plans">
2
+ <AdminPage class="sa-plans">
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"
9
+ />
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>
70
+ </AdminHero>
71
+
3
72
  <q-banner v-if="error" class="bg-negative text-white q-ma-md" rounded>
4
73
  <template #avatar>
5
74
  <q-icon name="error" />
@@ -10,29 +79,44 @@
10
79
  </template>
11
80
  </q-banner>
12
81
 
13
- <div v-if="loading && plans.length === 0" class="sa-plans__loading">
14
- <q-spinner size="32px" />
15
- <span>{{ msg.page.loading }}</span>
16
- </div>
17
-
18
- <template v-else>
82
+ <AdminBody
83
+ v-if="!viewOwnsHero"
84
+ :loading="loading && plans.length === 0"
85
+ :loading-text="msg.page.loading"
86
+ >
19
87
  <!-- Default: list view (plan simulation) + Bundle overview -->
20
88
  <template v-if="mode === 'list'">
21
- <PlanList
22
- :plans="plans"
23
- :versions-by-plan-id="versionsByPlanId"
24
- :tenant-counts-by-plan-key="tenantCountsByPlanKey"
25
- :plan-accents="props.planAccents"
26
- :highlight-plan-key="highlightPlanKey"
27
- @open-plan="onOpenPlan"
28
- @create-plan="openCreate"
29
- @clone-plan="onClonePlan"
30
- @new-version="onNewVersionFromList"
31
- @edit-draft="onEditDraftFromList"
32
- @discard-draft="onDiscardDraftFromList"
33
- @archive-plan="onArchivePlanFromList"
34
- @show-matrix="mode = 'matrix'"
35
- />
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>
36
120
  <PlanBundleOverview
37
121
  :bundles="availableBundles"
38
122
  :plans="plans"
@@ -41,21 +125,21 @@
41
125
  </template>
42
126
 
43
127
  <!-- V1 matrix: component comparison -->
44
- <PlanMatrix
45
- v-else-if="mode === 'matrix'"
46
- :plans="plans"
47
- :versions-by-plan-id="versionsByPlanId"
48
- :available-quotas="availableQuotas"
49
- :available-bundles="availableBundles"
50
- :feature-registry="featureRegistry"
51
- :tenant-counts-by-plan-key="tenantCountsByPlanKey"
52
- :plan-accents="props.planAccents"
53
- :loading="bulkVersionsLoading"
54
- @open-plan="onOpenPlan"
55
- @create-plan="openCreate"
56
- @clone-plan="onClonePlan"
57
- @view-catalog="mode = 'list'"
58
- />
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>
59
143
 
60
144
  <!-- Plan detail: drill-in for a single Plan (plan simulation) -->
61
145
  <PlanDetail
@@ -69,51 +153,47 @@
69
153
  :audit-rows="auditRows"
70
154
  :loading-audit="loadingAudit"
71
155
  :submit-terminate="onSubmitTerminate"
72
- @back="onBackToList"
73
156
  @create-draft="openCreateDraft"
74
157
  @edit-draft="openEditDraft"
75
158
  @publish="openPublish"
76
- @clone-plan="onClonePlan(selectedPlan)"
77
- @delete-plan="onArchivePlanFromList(selectedPlan, false)"
78
- @update-plan="onUpdatePlanFromDetail"
79
- />
80
-
81
- <!-- V2 split-view editor as a full-screen view (plan simulation) -->
82
- <PlanVersionEditor
83
- v-else-if="mode === 'editor' && draftEditing && selectedPlan"
84
- :plan-key="selectedPlan.planKey"
85
- :editing-id="draftEditing.editingId"
86
- :initial-form="draftEditing.initialForm"
87
- :saving="draftSaving"
88
- :available-features="availableFeatures"
89
- :available-quotas="availableQuotas"
90
- :available-bundles="availableBundles"
91
- :feature-registry="featureRegistry"
92
- :plan-display-name="selectedPlan.label"
93
- :save-error="draftSaveError"
94
- :predecessor-version="editorPredecessor"
95
- @save="onEditorNext"
96
- @cancel="onCancelEditor"
97
159
  />
160
+ </AdminBody>
161
+
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
+ />
98
179
 
99
- <!-- V3 Review & Publish — wizard step 3 (plan simulation) -->
100
- <PlanReview
101
- v-else-if="mode === 'review' && reviewDraft && selectedPlan"
102
- :plan="selectedPlan"
103
- :version="reviewDraft"
104
- :predecessor="reviewPredecessor"
105
- :available-quotas="availableQuotas"
106
- :available-bundles="availableBundles"
107
- :feature-registry="featureRegistry"
108
- :tenant-impact-count="tenantCountsByPlanKey[selectedPlan.planKey] ?? 0"
109
- :saving="draftSaving"
110
- :publishing="publishing"
111
- :publish-error="reviewError"
112
- @back="onReviewBack"
113
- @save-and-exit="onReviewSaveExit"
114
- @publish="onReviewPublish"
115
- />
116
- </template>
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
+ />
117
197
 
118
198
  <PlansPageToast :message="toastMessage" />
119
199
 
@@ -157,11 +237,18 @@
157
237
  :format-change-value="formatChangeValue"
158
238
  @execute="executePublish"
159
239
  />
160
- </q-page>
240
+ </AdminPage>
161
241
  </template>
162
242
 
163
243
  <script setup lang="ts">
164
244
  import { computed, onMounted, ref, shallowRef, watch } from 'vue';
245
+ import AdminBody from '../components/admin-page/AdminBody.vue';
246
+ import { countPlans, resolvePlans } from '../client/resolve-plans.js';
247
+ import AdminHero from '../components/admin-page/AdminHero.vue';
248
+ import AdminSection from '../components/admin-page/AdminSection.vue';
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';
165
252
  import type {
166
253
  FeatureCatalogEntryRow,
167
254
  PlanRow,
@@ -182,9 +269,11 @@ import type { HttpClient } from '../client/types.js';
182
269
  import type { SaMessages } from '../client/i18n/index.js';
183
270
  import { formatMessage } from '../client/i18n/format.js';
184
271
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
272
+ import { useSuperAdminHttp } from '../vue/use-super-admin-context.js';
185
273
  import PlanVersionEditor from '../components/plan-version-editor/PlanVersionEditor.vue';
186
274
  import PlanMatrix from '../components/plan-matrix/PlanMatrix.vue';
187
275
  import PlanDetail from '../components/plan-detail/PlanDetail.vue';
276
+ import PlanTitleEdit from '../components/plan-detail/PlanTitleEdit.vue';
188
277
  import PlanList from '../components/plan-list/PlanList.vue';
189
278
  import PlanCreateDialog, {
190
279
  type PlanCreateSubmit,
@@ -251,6 +340,64 @@ const props = defineProps<{
251
340
  }>();
252
341
 
253
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
+ );
254
401
  const common = useSaMessages('common');
255
402
 
256
403
  const composable: UsePlansResult = usePlans({
@@ -315,22 +462,6 @@ const tenantCountsByPlanKey = computed<Record<string, number>>(
315
462
  () => props.tenantCountsByPlanKey ?? loadedTenantCounts.value,
316
463
  );
317
464
 
318
- function planAccent(planKey: string): string {
319
- return props.planAccents?.[planKey] ?? defaultAccent(planKey);
320
- }
321
- const DEFAULT_ACCENTS: Record<string, string> = {
322
- STARTER: '#64748b',
323
- STANDARD: '#2563eb',
324
- PRO: '#7c3aed',
325
- PROFESSIONAL: '#7c3aed',
326
- BUSINESS: '#0ea5e9',
327
- ENTERPRISE: '#0f766e',
328
- BASIC: '#475569',
329
- };
330
- function defaultAccent(planKey: string): string {
331
- return DEFAULT_ACCENTS[planKey] ?? '#2563eb';
332
- }
333
-
334
465
  // ─── Plan master creation (V1 plan simulation step 1) ───
335
466
  const createDialogOpen = ref(false);
336
467
  const creatingPlan = ref(false);
@@ -418,7 +549,7 @@ async function reloadAllVersions(): Promise<void> {
418
549
  if (plans.value.length === 0) return;
419
550
  bulkVersionsLoading.value = true;
420
551
  try {
421
- const http = props.http ?? ((url: string, init?: RequestInit) => fetch(url, init));
552
+ const http = props.http ?? shellHttp;
422
553
  const token = props.getAuthToken?.();
423
554
  const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
424
555
  const results = await Promise.all(
@@ -539,6 +670,18 @@ async function onEditDraftFromList(plan: PlanRow, draft: PlanVersionRow): Promis
539
670
  openEditDraft(draft);
540
671
  }
541
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
+
542
685
  function onBackToList(): void {
543
686
  mode.value = 'list';
544
687
  selectedPlan.value = null;
@@ -555,7 +698,6 @@ async function onUpdatePlanFromDetail(patch: { label: string }): Promise<void> {
555
698
  const updated = await update(selectedPlan.value.id, patch);
556
699
  selectedPlan.value = updated;
557
700
  } catch (err: unknown) {
558
- // eslint-disable-next-line no-console
559
701
  console.error('[PlansPage] Plan rename failed', err);
560
702
  }
561
703
  }
@@ -578,7 +720,7 @@ const featureRegistry = computed<Record<string, { label?: string; group?: string
578
720
  }));
579
721
 
580
722
  async function loadEditorSources(): Promise<void> {
581
- const http = props.http ?? ((url: string, init?: RequestInit) => fetch(url, init));
723
+ const http = props.http ?? shellHttp;
582
724
  const token = props.getAuthToken?.();
583
725
  const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
584
726
  try {
@@ -926,7 +1068,6 @@ async function onReviewSaveExit(): Promise<void> {
926
1068
  draftEditing.value = null;
927
1069
  mode.value = selectedPlan.value ? 'cockpit' : 'list';
928
1070
  } catch (err: unknown) {
929
- // eslint-disable-next-line no-console
930
1071
  console.error('[PlansPage] Saving the draft failed', err);
931
1072
  reviewError.value = describeDraftSaveError(err);
932
1073
  } finally {
@@ -963,7 +1104,6 @@ async function onReviewPublish(payload: {
963
1104
  draftEditing.value = null;
964
1105
  mode.value = 'cockpit';
965
1106
  } catch (err: unknown) {
966
- // eslint-disable-next-line no-console
967
1107
  console.error('[PlansPage] Publishing failed', err);
968
1108
  reviewError.value = describePublishError(err);
969
1109
  } finally {
@@ -997,7 +1137,6 @@ async function executeArchive(): Promise<void> {
997
1137
  archiveOpen.value = false;
998
1138
  flashToast(formatMessage(msg.value.page.toastPlanDeleted, { planKey: plan.planKey }));
999
1139
  } catch (err: unknown) {
1000
- // eslint-disable-next-line no-console
1001
1140
  console.error('[PlansPage] Archive/purge failed', err);
1002
1141
  const status = (err as { status?: number })?.status;
1003
1142
  const body = (err as { body?: { code?: string; message?: string } })?.body;
@@ -1064,7 +1203,7 @@ async function executeDiscard(): Promise<void> {
1064
1203
  } catch (err: unknown) {
1065
1204
  // Full error object to the console — in the diagnostic case you can see
1066
1205
  // e.g. CORS preflight errors or the real network stack there.
1067
- // eslint-disable-next-line no-console
1206
+
1068
1207
  console.error('[PlansPage] Discard failed', err);
1069
1208
 
1070
1209
  const status = (err as { status?: number })?.status;
@@ -1238,17 +1377,6 @@ onMounted(async () => {
1238
1377
 
1239
1378
  <style scoped>
1240
1379
  .sa-plans {
1241
- min-height: calc(100vh - 56px);
1242
- background: #f6f7f9;
1243
1380
  position: relative;
1244
1381
  }
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
1382
  </style>