@saasicat/ui-vue 0.2.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 (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,215 @@
1
+ <template>
2
+ <div :class="['bd-features', { 'bd-locked': locked }]">
3
+ <template v-for="group in groupedFeatures" :key="group.key">
4
+ <div class="bd-features-group">
5
+ <div class="bd-features-group-label">{{ group.label }}</div>
6
+ <div class="bd-features-row">
7
+ <button
8
+ v-for="f in group.rows"
9
+ :key="f.featureKey"
10
+ type="button"
11
+ class="bd-feature-pill"
12
+ :class="{
13
+ on: features.includes(f.featureKey),
14
+ overlap: overlapKeys.includes(f.featureKey),
15
+ }"
16
+ :disabled="locked"
17
+ :title="pillTitle(f.featureKey)"
18
+ @click="onToggle(f.featureKey)"
19
+ >
20
+ <span class="bd-feature-tick" aria-hidden="true">
21
+ <svg
22
+ v-if="features.includes(f.featureKey)"
23
+ width="11"
24
+ height="11"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ stroke-width="3"
29
+ >
30
+ <path d="M5 13l4 4L19 7" />
31
+ </svg>
32
+ <svg
33
+ v-else
34
+ width="11"
35
+ height="11"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ stroke-width="2"
40
+ >
41
+ <path d="M12 5v14M5 12h14" />
42
+ </svg>
43
+ </span>
44
+ <span class="bd-feature-label">{{ featureLabel(f.featureKey) }}</span>
45
+ <span class="bd-feature-key">{{ f.featureKey }}</span>
46
+ </button>
47
+ </div>
48
+ </div>
49
+ </template>
50
+ <div v-if="groupedFeatures.length === 0" class="bd-features-empty">
51
+ Keine Features im Discovery-Snapshot.
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { computed } from 'vue';
58
+ import type { DiscoveredFeature } from '@saasicat/types';
59
+
60
+ // BundleFeaturesEditor — grouped pills for the feature selection of a
61
+ // bundle version (after plan simulation). The source of truth for the
62
+ // library is the discovery snapshot; the `featureRegistry` mapping from
63
+ // the wrapper provides only the label to display + the group bucket
64
+ // (analogous to `PlanVersionEditor`).
65
+ //
66
+ // `locked` = live/superseded version → the pills are disabled
67
+ // (contract protection; UI mirrors the backend rule `isVersionEditable`).
68
+ // `overlapKeys` = features already contained in a compatible plan
69
+ // → are marked as an "overlap" (warn about double billing).
70
+
71
+ export interface FeatureMeta {
72
+ /** Display name; fallback = featureKey. */
73
+ label?: string;
74
+ /** Library bucket (e.g. "Communication", "Finance"). Default "Allgemein". */
75
+ group?: string;
76
+ }
77
+
78
+ const props = defineProps<{
79
+ /** Library = discovery snapshot features. */
80
+ availableFeatures: DiscoveredFeature[];
81
+ /** Currently selected feature keys on the version. */
82
+ features: string[];
83
+ /** Switch to read-only (live/superseded). */
84
+ locked?: boolean;
85
+ /** Display mapping per feature key. */
86
+ featureRegistry?: Record<string, FeatureMeta>;
87
+ /** Features already contained in a selected compatible plan. */
88
+ overlapKeys?: string[];
89
+ }>();
90
+
91
+ const emit = defineEmits<{
92
+ (e: 'toggle', featureKey: string): void;
93
+ }>();
94
+
95
+ function featureLabel(key: string): string {
96
+ return props.featureRegistry?.[key]?.label ?? key;
97
+ }
98
+
99
+ function featureGroup(key: string): string {
100
+ return props.featureRegistry?.[key]?.group ?? 'Allgemein';
101
+ }
102
+
103
+ function pillTitle(key: string): string {
104
+ if (props.locked) return 'Live-Version ist read-only';
105
+ if (props.overlapKeys?.includes(key)) {
106
+ return 'Feature ist im kompatiblen Plan bereits enthalten — Doppel-Berechnung';
107
+ }
108
+ return props.features.includes(key) ? 'Aus Bundle entfernen' : 'In Bundle aufnehmen';
109
+ }
110
+
111
+ const overlapKeys = computed(() => props.overlapKeys ?? []);
112
+
113
+ const groupedFeatures = computed(() => {
114
+ const byGroup = new Map<string, DiscoveredFeature[]>();
115
+ for (const f of props.availableFeatures) {
116
+ const g = featureGroup(f.featureKey);
117
+ const list = byGroup.get(g) ?? [];
118
+ list.push(f);
119
+ byGroup.set(g, list);
120
+ }
121
+ return [...byGroup.entries()]
122
+ .sort(([a], [b]) => a.localeCompare(b, 'de-DE'))
123
+ .map(([key, rows]) => ({ key, label: key, rows }));
124
+ });
125
+
126
+ function onToggle(featureKey: string): void {
127
+ if (props.locked) return;
128
+ emit('toggle', featureKey);
129
+ }
130
+ </script>
131
+
132
+ <style scoped>
133
+ .bd-features {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 12px;
137
+ }
138
+ .bd-features.bd-locked {
139
+ opacity: 0.7;
140
+ }
141
+ .bd-features-group {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 6px;
145
+ }
146
+ .bd-features-group-label {
147
+ font-size: 10.5px;
148
+ font-weight: 700;
149
+ text-transform: uppercase;
150
+ letter-spacing: 0.08em;
151
+ color: #64748b;
152
+ }
153
+ .bd-features-row {
154
+ display: flex;
155
+ flex-wrap: wrap;
156
+ gap: 6px;
157
+ }
158
+ .bd-feature-pill {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 6px;
162
+ padding: 6px 10px;
163
+ background: #fff;
164
+ border: 1px solid #e2e8f0;
165
+ border-radius: 999px;
166
+ cursor: pointer;
167
+ font-family: inherit;
168
+ font-size: 12px;
169
+ color: #0f172a;
170
+ transition:
171
+ background 0.12s,
172
+ border-color 0.12s,
173
+ color 0.12s;
174
+ }
175
+ .bd-feature-pill:hover:not(:disabled) {
176
+ background: #f8fafc;
177
+ border-color: #cbd5e1;
178
+ }
179
+ .bd-feature-pill.on {
180
+ background: #dbeafe;
181
+ border-color: #93c5fd;
182
+ color: #1e40af;
183
+ }
184
+ .bd-feature-pill.overlap {
185
+ border-color: #fecaca;
186
+ background: #fef2f2;
187
+ color: #b91c1c;
188
+ }
189
+ .bd-feature-pill:disabled {
190
+ cursor: not-allowed;
191
+ }
192
+ .bd-feature-tick {
193
+ display: inline-flex;
194
+ }
195
+ .bd-feature-key {
196
+ font:
197
+ 600 10px 'JetBrains Mono',
198
+ ui-monospace,
199
+ monospace;
200
+ color: #94a3b8;
201
+ margin-left: 4px;
202
+ }
203
+ .bd-feature-pill.on .bd-feature-key {
204
+ color: #2563eb;
205
+ }
206
+ .bd-feature-pill.overlap .bd-feature-key {
207
+ color: #b91c1c;
208
+ }
209
+ .bd-features-empty {
210
+ padding: 12px;
211
+ color: #94a3b8;
212
+ font-style: italic;
213
+ font-size: 12.5px;
214
+ }
215
+ </style>
@@ -0,0 +1,316 @@
1
+ <template>
2
+ <div class="bv-compat">
3
+ <div class="bv-compat-hint">
4
+ Pläne, mit denen dieses Bundle als Add-On gebucht werden kann. Features/Quotas, die der
5
+ Plan bereits enthält, werden als Überschneidung markiert (Doppel-Berechnung).
6
+ </div>
7
+ <div class="bv-compat-grid">
8
+ <button
9
+ v-for="entry in entries"
10
+ :key="entry.plan.planKey"
11
+ type="button"
12
+ class="bv-compat-card"
13
+ :class="{
14
+ on: selectedKeys.includes(entry.plan.planKey),
15
+ warn: showOverlap(entry),
16
+ }"
17
+ :disabled="locked"
18
+ :title="
19
+ locked
20
+ ? 'Live-Version ist read-only'
21
+ : selectedKeys.includes(entry.plan.planKey)
22
+ ? 'Plan-Kompatibilität entfernen'
23
+ : 'Plan-Kompatibilität setzen'
24
+ "
25
+ @click="onToggle(entry.plan.planKey)"
26
+ >
27
+ <div class="bv-compat-head">
28
+ <span class="bv-compat-mark">{{ entry.plan.planKey.slice(0, 3) }}</span>
29
+ <div class="bv-compat-body">
30
+ <div class="bv-compat-name">{{ entry.plan.label }}</div>
31
+ <div class="bv-compat-key">{{ entry.plan.planKey }}</div>
32
+ </div>
33
+ <span
34
+ v-if="selectedKeys.includes(entry.plan.planKey)"
35
+ class="bv-compat-check"
36
+ aria-hidden="true"
37
+ >
38
+ <svg
39
+ width="14"
40
+ height="14"
41
+ viewBox="0 0 24 24"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ stroke-width="3"
45
+ >
46
+ <path d="M5 13l4 4L19 7" />
47
+ </svg>
48
+ </span>
49
+ </div>
50
+ <div v-if="showOverlap(entry)" class="bv-compat-overlap">
51
+ <div class="bv-compat-overlap-head">⚠ Überschneidung</div>
52
+ <div v-if="entry.overlap.features.length > 0" class="bv-compat-overlap-list">
53
+ <span class="bv-compat-overlap-kind">Features:</span>
54
+ <span
55
+ v-for="fk in entry.overlap.features"
56
+ :key="fk"
57
+ class="bv-compat-overlap-chip"
58
+ >
59
+ {{ featureLabel(fk) }}
60
+ </span>
61
+ </div>
62
+ <div v-if="entry.overlap.quotas.length > 0" class="bv-compat-overlap-list">
63
+ <span class="bv-compat-overlap-kind">Quotas:</span>
64
+ <span
65
+ v-for="qk in entry.overlap.quotas"
66
+ :key="qk"
67
+ class="bv-compat-overlap-chip"
68
+ >
69
+ {{ quotaLabel(qk) }}
70
+ </span>
71
+ </div>
72
+ </div>
73
+ </button>
74
+ <div v-if="plans.length === 0" class="bv-compat-empty">
75
+ Keine Pläne vorhanden — der Plan-Stamm muss zuerst angelegt werden.
76
+ </div>
77
+ </div>
78
+ <div v-if="overlapCount > 0" class="bv-compat-summary">
79
+ <span class="bv-compat-summary-ico">⚠</span>
80
+ <span>
81
+ <b>{{ overlapCount }}</b> Plan{{ overlapCount === 1 ? '' : 'e' }} mit Überschneidung
82
+ — Features/Quotas dieses Bundles sind im Plan bereits enthalten. Vor Publish
83
+ entweder das Bundle oder den Plan bereinigen.
84
+ </span>
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import { computed } from 'vue';
91
+ import type { DiscoveredQuota, PlanRow, PlanVersionRow } from '@saasicat/types';
92
+
93
+ import { findBundlePlanOverlap, type BundlePlanOverlap } from './bundle-version-status';
94
+
95
+ import type { FeatureMeta } from './BundleFeaturesEditor.vue';
96
+ import type { QuotaMeta } from './catalog-i18n.js';
97
+
98
+ // BundlePlanCompatPicker — multi-select of the plans this bundle may be
99
+ // booked with as an add-on (plan-bundle visibility, after plan
100
+ // simulation). For each selected plan the feature/quota overlap is
101
+ // computed live and shown as a warning.
102
+ //
103
+ // Note P11.7.4: the selection is currently persisted to
104
+ // `BundleVersionRow.compatibility` (the field exists in the type)
105
+ // — a dedicated `bundle_plans` junction will come in its own
106
+ // phase.
107
+
108
+ interface CompatEntry {
109
+ plan: PlanRow;
110
+ overlap: BundlePlanOverlap;
111
+ }
112
+
113
+ const props = defineProps<{
114
+ /** Plan master records from the consumer wrapper. */
115
+ plans: PlanRow[];
116
+ /** Live (or latest) PlanVersion per planKey — overlap source. */
117
+ livePlanVersions?: Record<string, PlanVersionRow | null>;
118
+ /** Features/quotas of the current BundleVersion (for overlap computation). */
119
+ bundleFeatures: string[];
120
+ bundleQuotas: Record<string, number>;
121
+ /** Currently selected plan keys. */
122
+ selectedKeys: string[];
123
+ locked?: boolean;
124
+ /** For overlap chip labels. */
125
+ featureRegistry?: Record<string, FeatureMeta>;
126
+ availableQuotas?: DiscoveredQuota[];
127
+ /** Quota labels resolved in the display locale (from the quota catalog). */
128
+ quotaRegistry?: Record<string, QuotaMeta>;
129
+ }>();
130
+
131
+ const emit = defineEmits<{
132
+ (e: 'toggle', planKey: string): void;
133
+ }>();
134
+
135
+ const entries = computed<CompatEntry[]>(() =>
136
+ props.plans.map((plan) => ({
137
+ plan,
138
+ overlap: findBundlePlanOverlap(
139
+ { features: props.bundleFeatures, quotas: props.bundleQuotas },
140
+ props.livePlanVersions?.[plan.planKey] ?? null,
141
+ ),
142
+ })),
143
+ );
144
+
145
+ const overlapCount = computed(
146
+ () =>
147
+ entries.value.filter((e) => props.selectedKeys.includes(e.plan.planKey) && e.overlap.hasAny)
148
+ .length,
149
+ );
150
+
151
+ function showOverlap(entry: CompatEntry): boolean {
152
+ return props.selectedKeys.includes(entry.plan.planKey) && entry.overlap.hasAny;
153
+ }
154
+
155
+ function featureLabel(key: string): string {
156
+ return props.featureRegistry?.[key]?.label ?? key;
157
+ }
158
+
159
+ function quotaLabel(key: string): string {
160
+ return (
161
+ props.quotaRegistry?.[key]?.label ??
162
+ props.availableQuotas?.find((q) => q.quotaKey === key)?.label ??
163
+ key
164
+ );
165
+ }
166
+
167
+ function onToggle(planKey: string): void {
168
+ if (props.locked) return;
169
+ emit('toggle', planKey);
170
+ }
171
+ </script>
172
+
173
+ <style scoped>
174
+ .bv-compat {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 10px;
178
+ }
179
+ .bv-compat-hint {
180
+ font-size: 11.5px;
181
+ color: #64748b;
182
+ line-height: 1.5;
183
+ }
184
+ .bv-compat-grid {
185
+ display: grid;
186
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
187
+ gap: 8px;
188
+ }
189
+ .bv-compat-card {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 6px;
193
+ padding: 10px 12px;
194
+ background: #fff;
195
+ border: 1px solid #e2e8f0;
196
+ border-radius: 8px;
197
+ cursor: pointer;
198
+ text-align: left;
199
+ font-family: inherit;
200
+ color: #0f172a;
201
+ transition:
202
+ background 0.12s,
203
+ border-color 0.12s;
204
+ }
205
+ .bv-compat-card:hover:not(:disabled) {
206
+ background: #f8fafc;
207
+ }
208
+ .bv-compat-card.on {
209
+ border-color: #2563eb;
210
+ background: #eff6ff;
211
+ }
212
+ .bv-compat-card.warn {
213
+ border-color: #fca5a5;
214
+ background: #fef2f2;
215
+ }
216
+ .bv-compat-card:disabled {
217
+ cursor: not-allowed;
218
+ opacity: 0.7;
219
+ }
220
+ .bv-compat-head {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 10px;
224
+ }
225
+ .bv-compat-mark {
226
+ width: 26px;
227
+ height: 26px;
228
+ display: grid;
229
+ place-items: center;
230
+ background: #e0e7ff;
231
+ color: #4338ca;
232
+ border: 1px solid #c7d2fe;
233
+ border-radius: 6px;
234
+ font:
235
+ 700 9px 'JetBrains Mono',
236
+ ui-monospace,
237
+ monospace;
238
+ letter-spacing: 0.04em;
239
+ flex: 0 0 auto;
240
+ }
241
+ .bv-compat-body {
242
+ flex: 1;
243
+ min-width: 0;
244
+ }
245
+ .bv-compat-name {
246
+ font-size: 13px;
247
+ font-weight: 600;
248
+ }
249
+ .bv-compat-key {
250
+ font:
251
+ 500 11px 'JetBrains Mono',
252
+ ui-monospace,
253
+ monospace;
254
+ color: #64748b;
255
+ }
256
+ .bv-compat-check {
257
+ color: #2563eb;
258
+ display: inline-flex;
259
+ }
260
+ .bv-compat-overlap {
261
+ padding: 6px 8px;
262
+ background: #fff;
263
+ border: 1px dashed #fecaca;
264
+ border-radius: 6px;
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 4px;
268
+ color: #b91c1c;
269
+ }
270
+ .bv-compat-overlap-head {
271
+ font-size: 11px;
272
+ font-weight: 700;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.06em;
275
+ }
276
+ .bv-compat-overlap-list {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 5px;
280
+ flex-wrap: wrap;
281
+ font-size: 11.5px;
282
+ }
283
+ .bv-compat-overlap-kind {
284
+ color: #7f1d1d;
285
+ font-weight: 600;
286
+ }
287
+ .bv-compat-overlap-chip {
288
+ padding: 1px 6px;
289
+ border: 1px solid #fecaca;
290
+ border-radius: 4px;
291
+ background: #fff;
292
+ color: #b91c1c;
293
+ }
294
+ .bv-compat-empty {
295
+ padding: 12px;
296
+ color: #94a3b8;
297
+ font-style: italic;
298
+ font-size: 12.5px;
299
+ text-align: center;
300
+ }
301
+ .bv-compat-summary {
302
+ display: flex;
303
+ align-items: flex-start;
304
+ gap: 8px;
305
+ padding: 10px 12px;
306
+ background: #fef2f2;
307
+ border: 1px solid #fecaca;
308
+ border-radius: 8px;
309
+ color: #b91c1c;
310
+ font-size: 12.5px;
311
+ line-height: 1.5;
312
+ }
313
+ .bv-compat-summary-ico {
314
+ font-weight: 700;
315
+ }
316
+ </style>