@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,65 @@
1
+ <template>
2
+ <section v-if="auditRows.length > 0 || loadingAudit" class="pc-card pc-audit-card">
3
+ <div class="pc-card-head pc-card-head--audit">
4
+ <div class="pc-card-head-text">
5
+ <div class="pc-card-title">Audit-Log</div>
6
+ <div class="pc-card-sub">Letzte Änderungen an diesem Plan</div>
7
+ </div>
8
+ </div>
9
+ <div class="pc-audit">
10
+ <div v-if="loadingAudit" class="pc-empty pc-empty--inline">Lade Audit-Log…</div>
11
+ <div v-for="a in auditRows" :key="a.id" class="pc-audit-row">
12
+ <span :class="['pc-audit-dot', `pc-audit-${auditKind(a.action)}`]" />
13
+ <span class="pc-audit-when">{{ formatAuditDate(a.createdAt) }}</span>
14
+ <span class="pc-audit-avatar">{{ auditInitial(a) }}</span>
15
+ <span class="pc-audit-who">{{ auditWho(a) }}</span>
16
+ <span class="pc-audit-what">{{ auditLabel(a) }}</span>
17
+ </div>
18
+ </div>
19
+ </section>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import type { AuditRow } from './types';
24
+
25
+ defineProps<{
26
+ auditRows: AuditRow[];
27
+ loadingAudit: boolean;
28
+ }>();
29
+
30
+ function auditKind(action: string): string {
31
+ const a = action.toLowerCase();
32
+ if (a.includes('publish')) return 'publish';
33
+ if (a.includes('create') && a.includes('draft')) return 'draft';
34
+ if (a.includes('create')) return 'add';
35
+ if (a.includes('update')) return 'change';
36
+ if (a.includes('delete')) return 'remove';
37
+ return 'change';
38
+ }
39
+
40
+ function auditWho(a: AuditRow): string {
41
+ if (a.user?.firstName || a.user?.lastName) {
42
+ return `${a.user.firstName ?? ''} ${a.user.lastName ?? ''}`.trim();
43
+ }
44
+ return a.user?.email ?? a.userEmail ?? 'system';
45
+ }
46
+
47
+ function auditInitial(a: AuditRow): string {
48
+ const who = auditWho(a);
49
+ return who[0]?.toUpperCase() ?? '?';
50
+ }
51
+
52
+ function auditLabel(a: AuditRow): string {
53
+ return a.action;
54
+ }
55
+
56
+ function formatAuditDate(iso: string): string {
57
+ const d = new Date(iso);
58
+ if (Number.isNaN(d.getTime())) return iso;
59
+ const today = new Date();
60
+ const isToday = d.toDateString() === today.toDateString();
61
+ const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
62
+ if (isToday) return `heute · ${time}`;
63
+ return `${d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' })} · ${time}`;
64
+ }
65
+ </script>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <section class="pc-card">
3
+ <div class="pc-card-head">
4
+ <div class="pc-card-head-text">
5
+ <div class="pc-card-title">
6
+ <template v-if="diffPair">
7
+ Diff v{{ diffPair.from.version }} → v{{ diffPair.to.version }}
8
+ </template>
9
+ <template v-else>Diff</template>
10
+ </div>
11
+ <div class="pc-card-sub">
12
+ {{ diffPair ? 'Was sich beim Publish ändert' : 'Kein Vergleich verfügbar' }}
13
+ </div>
14
+ </div>
15
+ <div v-if="diffPair" class="pc-pillrow">
16
+ <span class="pc-pill" style="background: #ecfdf5; color: #047857">
17
+ +{{ diff.addedFeatures.length }} F
18
+ </span>
19
+ <span class="pc-pill" style="background: #fef2f2; color: #b91c1c">
20
+ −{{ diff.removedFeatures.length }} F
21
+ </span>
22
+ <span class="pc-pill" style="background: #fffbeb; color: #b45309">
23
+ {{
24
+ diff.changedQuotas.length +
25
+ diff.addedQuotas.length +
26
+ diff.removedQuotas.length
27
+ }}
28
+ Q
29
+ </span>
30
+ </div>
31
+ </div>
32
+ <div class="pc-diff-body">
33
+ <div
34
+ v-for="d in diffRows"
35
+ :key="d.id"
36
+ class="pc-diff-row"
37
+ :style="{ background: d.bg, borderColor: d.border }"
38
+ >
39
+ <span class="pc-diff-sign" :style="{ background: d.color }">{{ d.sign }}</span>
40
+ <div class="pc-diff-main">
41
+ <div class="pc-diff-headline">
42
+ <span class="pc-diff-section" :style="{ color: d.color }">{{
43
+ d.section
44
+ }}</span>
45
+ <span class="pc-diff-label">{{ d.label }}</span>
46
+ <code v-if="d.sub" class="pc-mono pc-mono--xs">{{ d.sub }}</code>
47
+ </div>
48
+ <div v-if="d.from !== undefined" class="pc-diff-change">
49
+ <span class="pc-diff-from">{{ d.from }}</span>
50
+ <span class="pc-diff-arrow" :style="{ color: d.color }">
51
+ <svg
52
+ width="12"
53
+ height="12"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ >
59
+ <path d="M5 12h14M13 5l7 7-7 7" />
60
+ </svg>
61
+ </span>
62
+ <span class="pc-diff-to">{{ d.to }}</span>
63
+ </div>
64
+ </div>
65
+ <span
66
+ class="pc-chip pc-diff-tag"
67
+ :style="{
68
+ background: '#fff',
69
+ color: d.color,
70
+ borderColor: d.border,
71
+ }"
72
+ >
73
+ {{ d.tag }}
74
+ </span>
75
+ </div>
76
+ <div v-if="diffRows.length === 0" class="pc-empty pc-empty--inline">
77
+ Keine Änderungen erkannt.
78
+ </div>
79
+ </div>
80
+ </section>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import type { DiffRow, DiffSummary, PlanVersionPair } from './types';
85
+
86
+ defineProps<{
87
+ diffPair: PlanVersionPair | null;
88
+ diff: DiffSummary;
89
+ diffRows: DiffRow[];
90
+ }>();
91
+ </script>
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <div class="pc-backbar">
3
+ <button class="pc-back" type="button" @click="emit('back')">
4
+ <span class="pc-back-ico" aria-hidden="true">
5
+ <svg
6
+ width="14"
7
+ height="14"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ stroke-width="2"
12
+ >
13
+ <path d="M5 12h14M13 5l7 7-7 7" />
14
+ </svg>
15
+ </span>
16
+ <span>Zurück zur Liste</span>
17
+ </button>
18
+ </div>
19
+
20
+ <div class="pc-header">
21
+ <div class="pc-header-left">
22
+ <div class="pc-bigchip" :style="{ background: accentBg, color: accentFg }">
23
+ {{ plan.planKey }}
24
+ </div>
25
+ <div class="pc-header-titles">
26
+ <h2 class="pc-h-title">{{ plan.label }}</h2>
27
+ <p class="pc-h-sub">{{ plan.description || 'Keine Beschreibung.' }}</p>
28
+ </div>
29
+ </div>
30
+ <div class="pc-header-right">
31
+ <button class="pc-btn" type="button" @click="emit('clonePlan')">
32
+ <svg
33
+ width="14"
34
+ height="14"
35
+ viewBox="0 0 24 24"
36
+ fill="none"
37
+ stroke="currentColor"
38
+ stroke-width="2"
39
+ >
40
+ <rect x="9" y="9" width="11" height="11" rx="2" />
41
+ <path d="M5 15V5a2 2 0 012-2h10" />
42
+ </svg>
43
+ <span>Plan klonen</span>
44
+ </button>
45
+ <button class="pc-btn" type="button" @click="emit('viewCatalog')">
46
+ <svg
47
+ width="14"
48
+ height="14"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ stroke-width="2"
53
+ >
54
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
55
+ <circle cx="12" cy="12" r="3" />
56
+ </svg>
57
+ <span>Im Katalog ansehen</span>
58
+ </button>
59
+ <button
60
+ class="pc-btn pc-btn--primary"
61
+ type="button"
62
+ :disabled="hasOpenDraft"
63
+ :title="hasOpenDraft ? 'Es gibt bereits eine offene Draft' : undefined"
64
+ @click="emit('createDraft')"
65
+ >
66
+ <svg
67
+ width="14"
68
+ height="14"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ stroke-width="2.5"
73
+ >
74
+ <path d="M12 5v14M5 12h14" />
75
+ </svg>
76
+ <span>Neue Draft-Version</span>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ import type { PlanRow } from '@saasicat/types';
84
+
85
+ defineProps<{
86
+ plan: PlanRow;
87
+ accentBg: string;
88
+ accentFg: string;
89
+ hasOpenDraft: boolean;
90
+ }>();
91
+
92
+ const emit = defineEmits<{
93
+ (e: 'back'): void;
94
+ (e: 'clonePlan'): void;
95
+ (e: 'viewCatalog'): void;
96
+ (e: 'createDraft'): void;
97
+ }>();
98
+ </script>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <section v-if="tenantImpact" class="pc-card">
3
+ <div class="pc-card-head">
4
+ <div class="pc-card-head-text">
5
+ <div class="pc-card-title">Tenant-Impact</div>
6
+ <div class="pc-card-sub">
7
+ Wer ist betroffen, wenn die nächste Version published wird
8
+ </div>
9
+ </div>
10
+ </div>
11
+ <div class="pc-impact-strip">
12
+ <div class="pc-impact-num">{{ tenantImpactTotal }}</div>
13
+ <div class="pc-impact-bars">
14
+ <div class="pc-impact-bar">
15
+ <span
16
+ class="pc-impact-bar-fill"
17
+ :style="{
18
+ width: pct(tenantImpact.auto, tenantImpactTotal),
19
+ background: '#10b981',
20
+ }"
21
+ />
22
+ <span class="pc-impact-bar-label">{{ tenantImpact.auto }} auto-migrieren</span>
23
+ </div>
24
+ <div class="pc-impact-bar">
25
+ <span
26
+ class="pc-impact-bar-fill"
27
+ :style="{
28
+ width: pct(tenantImpact.review, tenantImpactTotal),
29
+ background: '#f59e0b',
30
+ }"
31
+ />
32
+ <span class="pc-impact-bar-label"
33
+ >{{ tenantImpact.review }} brauchen Review</span
34
+ >
35
+ </div>
36
+ <div class="pc-impact-bar">
37
+ <span
38
+ class="pc-impact-bar-fill"
39
+ :style="{
40
+ width: pct(tenantImpact.conflict, tenantImpactTotal),
41
+ background: '#ef4444',
42
+ }"
43
+ />
44
+ <span class="pc-impact-bar-label">{{ tenantImpact.conflict }} Konflikt</span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ <div
49
+ v-if="tenantImpact.examples && tenantImpact.examples.length > 0"
50
+ class="pc-impact-tenants"
51
+ >
52
+ <div v-for="t in tenantImpact.examples" :key="t.name" class="pc-impact-tenant">
53
+ <div class="pc-impact-avatar">{{ initialsOf(t.name) }}</div>
54
+ <div class="pc-impact-body">
55
+ <div class="pc-impact-name">{{ t.name }}</div>
56
+ <div class="pc-impact-sub">{{ t.plan }}</div>
57
+ </div>
58
+ <span :class="['pc-chip', impactStateChip(t.state)]">{{
59
+ impactStateLabel(t.state)
60
+ }}</span>
61
+ </div>
62
+ </div>
63
+ </section>
64
+ </template>
65
+
66
+ <script setup lang="ts">
67
+ import type { ImpactTenant, TenantImpact } from './types';
68
+
69
+ defineProps<{
70
+ tenantImpact: TenantImpact | null;
71
+ tenantImpactTotal: number;
72
+ }>();
73
+
74
+ function pct(part: number, total: number): string {
75
+ if (total <= 0) return '0%';
76
+ return `${Math.max(2, Math.round((part / total) * 100))}%`;
77
+ }
78
+
79
+ function initialsOf(name: string): string {
80
+ return name
81
+ .split(/\s+/)
82
+ .filter(Boolean)
83
+ .slice(0, 2)
84
+ .map((s) => s[0]?.toUpperCase() ?? '')
85
+ .join('');
86
+ }
87
+
88
+ function impactStateLabel(state: ImpactTenant['state']): string {
89
+ return state === 'auto' ? 'auto' : state === 'review' ? 'review' : 'Konflikt';
90
+ }
91
+
92
+ function impactStateChip(state: ImpactTenant['state']): string {
93
+ return state === 'auto'
94
+ ? 'pc-chip--live'
95
+ : state === 'review'
96
+ ? 'pc-chip--draft'
97
+ : 'pc-chip--conflict';
98
+ }
99
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="pc-kpis">
3
+ <div class="pc-kpi">
4
+ <div class="pc-kpi-label">Aktive Version</div>
5
+ <div class="pc-kpi-val">
6
+ <template v-if="liveVersion">
7
+ v{{ liveVersion.version }}
8
+ <span class="pc-chip pc-chip--live pc-chip--dot pc-chip--mid">live</span>
9
+ </template>
10
+ <template v-else>—</template>
11
+ </div>
12
+ <div class="pc-kpi-sub">
13
+ {{
14
+ liveVersion?.validFrom
15
+ ? `seit ${liveVersion.validFrom.slice(0, 10)}`
16
+ : 'keine veröffentlichte Version'
17
+ }}
18
+ </div>
19
+ </div>
20
+ <div class="pc-kpi">
21
+ <div class="pc-kpi-label">Tenant-Impact</div>
22
+ <div class="pc-kpi-val">{{ tenantImpactTotal }}</div>
23
+ <div class="pc-kpi-sub">Mandanten auf diesem Plan</div>
24
+ </div>
25
+ <div class="pc-kpi">
26
+ <div class="pc-kpi-label">Versionen</div>
27
+ <div class="pc-kpi-val">{{ versionsCount }}</div>
28
+ <div class="pc-kpi-sub">
29
+ {{ publishedCount }} live/superseded · {{ draftCount }} Draft
30
+ </div>
31
+ </div>
32
+ <div :class="['pc-kpi', draftVersion ? 'pc-kpi--draft' : '']">
33
+ <div class="pc-kpi-label" :style="draftVersion ? 'color:#b45309' : ''">
34
+ {{ draftVersion ? 'Offener Draft' : 'Kein Draft' }}
35
+ </div>
36
+ <div class="pc-kpi-val">
37
+ {{ draftVersion ? `v${draftVersion.version}` : '—' }}
38
+ </div>
39
+ <div class="pc-kpi-sub" :style="draftVersion ? 'color:#b45309' : ''">
40
+ {{ draftVersion ? 'Bereit zum Bearbeiten' : 'Neue Draft anlegen' }}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ import type { PlanVersionRow } from '@saasicat/types';
48
+
49
+ defineProps<{
50
+ liveVersion: PlanVersionRow | null;
51
+ tenantImpactTotal: number;
52
+ versionsCount: number;
53
+ publishedCount: number;
54
+ draftCount: number;
55
+ draftVersion: PlanVersionRow | null;
56
+ }>();
57
+ </script>
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <section class="pc-card pc-card--left">
3
+ <div class="pc-card-head">
4
+ <div class="pc-card-head-text">
5
+ <div class="pc-card-title">Versionen</div>
6
+ <div class="pc-card-sub">
7
+ Pro Plan max. 1 offene Draft-Version · Publish setzt Vorgänger auf
8
+ <code class="pc-mono">supersededAt</code>
9
+ </div>
10
+ </div>
11
+ <button
12
+ class="pc-btn pc-btn--sm pc-btn--primary"
13
+ type="button"
14
+ :disabled="hasOpenDraft"
15
+ @click="emit('createDraft')"
16
+ >
17
+ <svg
18
+ width="12"
19
+ height="12"
20
+ viewBox="0 0 24 24"
21
+ fill="none"
22
+ stroke="currentColor"
23
+ stroke-width="2.5"
24
+ >
25
+ <path d="M12 5v14M5 12h14" />
26
+ </svg>
27
+ <span>Draft v{{ nextDraftVersion }}</span>
28
+ </button>
29
+ </div>
30
+
31
+ <div v-if="versions.length > 0" class="pc-vtimeline">
32
+ <div class="pc-vtl-track">
33
+ <div
34
+ v-for="seg in timelineSegments"
35
+ :key="seg.key"
36
+ :class="['pc-vtl-seg', `pc-vtl-${seg.status}`]"
37
+ :style="{ flex: seg.flex }"
38
+ >
39
+ v{{ seg.version }}<template v-if="seg.status === 'draft'"> · draft</template>
40
+ </div>
41
+ </div>
42
+ <div class="pc-vtl-axis">
43
+ <span v-for="(label, i) in timelineAxis" :key="i">{{ label }}</span>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="pc-vrow pc-vrow--head">
48
+ <div class="pc-vrow-version pc-vrow-headlabel">Version</div>
49
+ <div class="pc-vrow-cell pc-vrow-headlabel">Gültig</div>
50
+ <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">Pricing</div>
51
+ <div class="pc-vrow-cell pc-vrow-headlabel pc-vrow-cell--right">Impact</div>
52
+ <div class="pc-vrow-note pc-vrow-headlabel">Change-Note</div>
53
+ <div class="pc-vrow-actions" />
54
+ </div>
55
+ <div v-if="versions.length === 0" class="pc-empty">
56
+ Noch keine Versionen — „Neue Draft-Version" oben rechts.
57
+ </div>
58
+ <div
59
+ v-for="row in sortedVersions"
60
+ :key="row.id"
61
+ :class="['pc-vrow', row.publishedAt === null ? 'pc-vrow--active' : '']"
62
+ >
63
+ <div class="pc-vrow-version">
64
+ <span class="pc-vrow-vlabel">v{{ row.version }}</span>
65
+ <span :class="['pc-chip pc-chip--dot', statusChipClass(row)]">{{
66
+ statusLabel(row)
67
+ }}</span>
68
+ </div>
69
+ <div class="pc-vrow-cell">
70
+ <div class="pc-vrow-validity">
71
+ {{ row.validFrom ? row.validFrom.slice(0, 10) : '—' }}
72
+ <span class="pc-vrow-arrow">→</span>
73
+ <template v-if="row.validUntil">{{ row.validUntil.slice(0, 10) }}</template>
74
+ <span v-else class="pc-vrow-inf">∞</span>
75
+ </div>
76
+ <div class="pc-vrow-sub">{{ updatedLabel(row) }}</div>
77
+ </div>
78
+ <div class="pc-vrow-cell pc-vrow-cell--right">
79
+ <div class="pc-vrow-price">
80
+ {{ formatMoney(row.monthlyNet) }}<span class="pc-vrow-price-unit">/ Mo</span>
81
+ </div>
82
+ <div class="pc-vrow-sub">{{ formatMoney(row.yearlyNet) }} / Jahr</div>
83
+ </div>
84
+ <div class="pc-vrow-cell pc-vrow-cell--right">
85
+ <div
86
+ class="pc-vrow-impact"
87
+ :class="(impactByVersion[row.version] ?? 0) > 0 ? '' : 'pc-vrow-impact--zero'"
88
+ >
89
+ {{ impactByVersion[row.version] ?? 0 }}
90
+ </div>
91
+ <div class="pc-vrow-sub">Mandanten</div>
92
+ </div>
93
+ <div class="pc-vrow-note" :title="row.changeNote ?? ''">
94
+ {{ row.changeNote || '—' }}
95
+ </div>
96
+ <div class="pc-vrow-actions">
97
+ <button
98
+ v-if="row.publishedAt === null"
99
+ class="pc-btn pc-btn--sm pc-btn--primary"
100
+ type="button"
101
+ @click="emit('publish', row)"
102
+ >
103
+ <svg
104
+ width="12"
105
+ height="12"
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ stroke-width="2"
110
+ >
111
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
112
+ </svg>
113
+ <span>Publish</span>
114
+ </button>
115
+ <button
116
+ v-if="editabilityOf(row).editable"
117
+ class="pc-btn pc-btn--sm"
118
+ type="button"
119
+ :title="
120
+ editabilityOf(row).reason === 'pre-active'
121
+ ? 'Published, aber noch nicht aktiv — Features, Quotas und Preis können bis zum Aktivierungsdatum noch korrigiert werden'
122
+ : 'Draft bearbeiten'
123
+ "
124
+ @click="emit('editDraft', row)"
125
+ >
126
+ <svg
127
+ width="12"
128
+ height="12"
129
+ viewBox="0 0 24 24"
130
+ fill="none"
131
+ stroke="currentColor"
132
+ stroke-width="2"
133
+ >
134
+ <path d="M12 20h9M16.5 3.5a2.12 2.12 0 113 3L7 19l-4 1 1-4 12.5-12.5z" />
135
+ </svg>
136
+ <span>Bearbeiten</span>
137
+ </button>
138
+ <button
139
+ v-if="row.publishedAt !== null && row.supersededAt === null && draftVersion"
140
+ class="pc-btn pc-btn--sm"
141
+ type="button"
142
+ @click="emit('viewDiff', draftVersion, row)"
143
+ >
144
+ <svg
145
+ width="12"
146
+ height="12"
147
+ viewBox="0 0 24 24"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-width="2"
151
+ >
152
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
153
+ <circle cx="12" cy="12" r="3" />
154
+ </svg>
155
+ <span>Diff</span>
156
+ </button>
157
+ </div>
158
+ </div>
159
+ </section>
160
+ </template>
161
+
162
+ <script setup lang="ts">
163
+ import { isVersionEditable, type PlanVersionRow } from '@saasicat/types';
164
+ import type { TimelineSegment } from './types';
165
+
166
+ defineProps<{
167
+ versions: PlanVersionRow[];
168
+ sortedVersions: PlanVersionRow[];
169
+ timelineSegments: TimelineSegment[];
170
+ timelineAxis: string[];
171
+ hasOpenDraft: boolean;
172
+ nextDraftVersion: number;
173
+ impactByVersion: Record<number, number>;
174
+ draftVersion: PlanVersionRow | null;
175
+ }>();
176
+
177
+ const emit = defineEmits<{
178
+ (e: 'createDraft'): void;
179
+ (e: 'publish', version: PlanVersionRow): void;
180
+ (e: 'editDraft', version: PlanVersionRow): void;
181
+ (e: 'viewDiff', from: PlanVersionRow, to: PlanVersionRow): void;
182
+ }>();
183
+
184
+ function editabilityOf(version: PlanVersionRow): {
185
+ editable: boolean;
186
+ reason: 'draft' | 'pre-active' | null;
187
+ } {
188
+ return isVersionEditable(version);
189
+ }
190
+
191
+ function statusChipClass(row: PlanVersionRow): string {
192
+ if (row.publishedAt === null) return 'pc-chip--draft';
193
+ if (row.supersededAt !== null) return 'pc-chip--supersed';
194
+ return 'pc-chip--live';
195
+ }
196
+
197
+ function statusLabel(row: PlanVersionRow): string {
198
+ if (row.publishedAt === null) return 'draft';
199
+ if (row.supersededAt !== null) return 'superseded';
200
+ return 'live';
201
+ }
202
+
203
+ function formatMoney(raw: string | number): string {
204
+ const num = typeof raw === 'string' ? Number(raw) : raw;
205
+ if (!Number.isFinite(num)) return String(raw);
206
+ if (Number.isInteger(num)) return `${num} €`;
207
+ return `${num.toFixed(2).replace('.', ',')} €`;
208
+ }
209
+
210
+ function updatedLabel(row: PlanVersionRow): string {
211
+ const iso =
212
+ row.publishedAt ??
213
+ (row as unknown as { updatedAt?: string }).updatedAt ??
214
+ (row as unknown as { createdAt?: string }).createdAt ??
215
+ '';
216
+ if (!iso) return '';
217
+ const d = new Date(iso);
218
+ if (Number.isNaN(d.getTime())) return '';
219
+ const diffMs = Date.now() - d.getTime();
220
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
221
+ if (days <= 0) return 'heute';
222
+ if (days < 30) return `vor ${days} Tagen`;
223
+ if (days < 365) return `vor ${Math.floor(days / 30)} Monaten`;
224
+ return `vor ${Math.floor(days / 365)} Jahren`;
225
+ }
226
+ </script>