@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,107 @@
1
+ <template>
2
+ <div class="sp-usage-bar" :class="`sp-usage-bar--${tone}`">
3
+ <div class="sp-usage-bar__head">
4
+ <span class="sp-usage-bar__label">{{ label }}</span>
5
+ <span class="sp-usage-bar__values"> {{ formattedUsed }} / {{ formattedMax }} </span>
6
+ </div>
7
+ <div class="sp-usage-bar__track">
8
+ <div class="sp-usage-bar__fill" :style="{ width: `${percent}%` }" />
9
+ </div>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed } from 'vue';
15
+
16
+ // UsageBar — generic usage display for a single limit dimension. The consumer
17
+ // passes label, usage and maximum through. `-1` as the maximum = unlimited
18
+ // (Catalog convention), rendered as ∞.
19
+
20
+ interface Props {
21
+ label: string;
22
+ used: number;
23
+ max: number;
24
+ /** Format hook: integer by default, storage is shown with 1 decimal place. */
25
+ fractional?: boolean;
26
+ /**
27
+ * Custom value formatter. Applied to `used` and (unless ∞) `max`.
28
+ * Allows, for example, adaptive units ("5.75 MB" vs. "5 GB") for small
29
+ * fractional values without UsageBar needing to know the unit.
30
+ */
31
+ formatValue?: (value: number) => string;
32
+ }
33
+
34
+ const props = defineProps<Props>();
35
+
36
+ const isUnlimited = computed(() => props.max === -1);
37
+
38
+ const percent = computed(() => {
39
+ if (isUnlimited.value) return 0;
40
+ if (props.max <= 0) return 0;
41
+ return Math.min(100, Math.round((props.used / props.max) * 100));
42
+ });
43
+
44
+ const tone = computed(() => {
45
+ if (isUnlimited.value) return 'ok';
46
+ if (props.max <= 0) return 'ok';
47
+ const ratio = props.used / props.max;
48
+ if (ratio >= 1) return 'full';
49
+ if (ratio >= 0.8) return 'warn';
50
+ return 'ok';
51
+ });
52
+
53
+ function defaultFormat(value: number): string {
54
+ return props.fractional ? value.toFixed(1) : Math.round(value).toString();
55
+ }
56
+
57
+ const formattedUsed = computed(() =>
58
+ props.formatValue ? props.formatValue(props.used) : defaultFormat(props.used),
59
+ );
60
+ const formattedMax = computed(() => {
61
+ if (isUnlimited.value) return '∞';
62
+ return props.formatValue ? props.formatValue(props.max) : defaultFormat(props.max);
63
+ });
64
+ </script>
65
+
66
+ <style scoped>
67
+ .sp-usage-bar {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 4px;
71
+ }
72
+ .sp-usage-bar__head {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ font-size: 13px;
77
+ }
78
+ .sp-usage-bar__label {
79
+ color: var(--q-secondary, #555);
80
+ font-weight: 500;
81
+ }
82
+ .sp-usage-bar__values {
83
+ font-variant-numeric: tabular-nums;
84
+ color: var(--q-secondary, #555);
85
+ }
86
+ .sp-usage-bar__track {
87
+ width: 100%;
88
+ height: 8px;
89
+ background: rgba(0, 0, 0, 0.08);
90
+ border-radius: 4px;
91
+ overflow: hidden;
92
+ }
93
+ .sp-usage-bar__fill {
94
+ height: 100%;
95
+ transition: width 200ms ease;
96
+ border-radius: 4px;
97
+ }
98
+ .sp-usage-bar--ok .sp-usage-bar__fill {
99
+ background: var(--q-positive, #21ba45);
100
+ }
101
+ .sp-usage-bar--warn .sp-usage-bar__fill {
102
+ background: var(--q-warning, #f2c037);
103
+ }
104
+ .sp-usage-bar--full .sp-usage-bar__fill {
105
+ background: var(--q-negative, #c10015);
106
+ }
107
+ </style>
@@ -0,0 +1,280 @@
1
+ // Default i18n map for the tenant plan section. Consumers override individual
2
+ // keys via the `i18n` prop or pass their own map through (build a vue-i18n
3
+ // resolver on top of this shape).
4
+
5
+ export interface TenantPlanSectionI18n {
6
+ sectionTitle: string;
7
+ sectionSubtitle: string;
8
+ loading: string;
9
+ noSubscription: string;
10
+ activePlan: string;
11
+ cycleMonthly: string;
12
+ cycleYearly: string;
13
+ statusActive: string;
14
+ statusTrial: string;
15
+ statusPastDue: string;
16
+ statusCanceled: string;
17
+ statusPendingSales: string;
18
+ trialEndsAt: string;
19
+ pilotEndsAt: string;
20
+ nextBillingDate: string;
21
+ pendingChange: string;
22
+ changeFromTo: string;
23
+ changeEffectiveAt: string;
24
+ changePlanButton: string;
25
+ cancelSubscriptionButton: string;
26
+ usageTitle: string;
27
+ /** #18 — feature / scope-of-services matrix (included + locked). */
28
+ featuresOverviewTitle: string;
29
+ featuresActive: string;
30
+ featuresLocked: string;
31
+ /** #15 — bundle store (booked + available bundles). */
32
+ bundlesStoreTitle: string;
33
+ bundlesBookedTitle: string;
34
+ bundlesAvailableTitle: string;
35
+ bundlesAvailableEmpty: string;
36
+ bundlesPerMonth: string;
37
+ bundleBookAction: string;
38
+ bundleBookInProgress: string;
39
+ bundleCancelAction: string;
40
+ bundleReactivateAction: string;
41
+ bundleReactivateConfirmTitle: string;
42
+ bundleReactivateConfirmBody: string;
43
+ bundleCanceledAt: string;
44
+ bundleMinimumTermUntil: string;
45
+ bundleIncludesLabel: string;
46
+ bundleAlreadyBooked: string;
47
+ bundleIncompatible: string;
48
+ /** #37/#61 — requires graying-out + booking/cancellation preview. */
49
+ bundleMissingRequires: string;
50
+ bundlePreviewAddTitle: string;
51
+ bundlePreviewCancelTitle: string;
52
+ bundlePreviewLoading: string;
53
+ bundlePreviewProrationTitle: string;
54
+ bundlePreviewProratedNow: string;
55
+ bundlePreviewProrationDays: string;
56
+ bundlePreviewNextPeriod: string;
57
+ bundlePreviewTrialNote: string;
58
+ bundlePreviewNoPrice: string;
59
+ bundlePreviewMinimumTermLabel: string;
60
+ bundlePreviewMinimumTermMonths: string;
61
+ bundlePreviewMinimumTermNone: string;
62
+ bundlePreviewRedundantTitle: string;
63
+ bundlePreviewRedundantCoveredByPlan: string;
64
+ bundlePreviewRedundantCoveredByBundle: string;
65
+ bundlePreviewMissingRequiresTitle: string;
66
+ bundlePreviewBlockersTitle: string;
67
+ bundlePreviewWarningsTitle: string;
68
+ bundlePreviewEffectiveAt: string;
69
+ bundlePreviewSavings: string;
70
+ bundlePreviewConfirmAdd: string;
71
+ bundlePreviewConfirmCancel: string;
72
+ bundlePreviewInProgress: string;
73
+ bundlePreviewClose: string;
74
+ pendingVersionTitle: string;
75
+ pendingVersionChipNonRegressive: string;
76
+ pendingVersionChipRegressive: string;
77
+ pendingVersionEffectiveAt: string;
78
+ pendingVersionAcceptAction: string;
79
+ pendingVersionAcceptInProgress: string;
80
+ pendingVersionAcceptedAt: string;
81
+ wizardTitle: string;
82
+ wizardClose: string;
83
+ wizardCurrent: string;
84
+ wizardBadgeCurrent: string;
85
+ wizardBadgePopular: string;
86
+ wizardPriceUnitMonthly: string;
87
+ wizardPriceUnitYearly: string;
88
+ wizardPriceOnRequest: string;
89
+ wizardStepChoose: string;
90
+ wizardStepChooseIntro: string;
91
+ wizardStepPreview: string;
92
+ wizardStepConfirm: string;
93
+ wizardNext: string;
94
+ wizardBack: string;
95
+ wizardPreviewLoading: string;
96
+ wizardEffectiveAtLabel: string;
97
+ wizardEffectiveImmediate: string;
98
+ wizardProrationTitle: string;
99
+ wizardProrationLine: string;
100
+ wizardProrationDays: string;
101
+ wizardLimitsTitle: string;
102
+ wizardLimitsUsed: string;
103
+ wizardLimitsCurrent: string;
104
+ wizardLimitsTarget: string;
105
+ wizardFeaturesGained: string;
106
+ wizardFeaturesLost: string;
107
+ wizardBlockersTitle: string;
108
+ wizardConfirmImmediate: string;
109
+ wizardConfirmScheduled: string;
110
+ wizardConfirmAction: string;
111
+ wizardConfirmInProgress: string;
112
+ /** #17 — price overview in the confirm step. */
113
+ wizardConfirmPriceTitle: string;
114
+ wizardConfirmProratedNow: string;
115
+ wizardConfirmRecurringNext: string;
116
+ wizardConfirmRecurringFrom: string;
117
+ wizardConfirmPerCycleMonthly: string;
118
+ wizardConfirmPerCycleYearly: string;
119
+ /** #17 — trial case in the confirm step (nothing due during the trial). */
120
+ wizardConfirmTrialNote: string;
121
+ wizardConfirmRecurringTrialEnd: string;
122
+ wizardChangeTypeUpgrade: string;
123
+ wizardChangeTypeDowngrade: string;
124
+ wizardChangeTypeCycle: string;
125
+ wizardChangeTypeNoop: string;
126
+ /** P11.4: read-only display of the package snapshot. */
127
+ packageSnapshotTitle: string;
128
+ packageSnapshotSubtitle: string;
129
+ packageSnapshotCapturedAt: string;
130
+ packageSnapshotOfferRef: string;
131
+ packageSnapshotPlanLabel: string;
132
+ packageSnapshotPlanVersionLabel: string;
133
+ packageSnapshotCycleLabel: string;
134
+ packageSnapshotBundlesLabel: string;
135
+ packageSnapshotBundlesEmpty: string;
136
+ packageSnapshotPriceMonthly: string;
137
+ packageSnapshotPriceYearly: string;
138
+ packageSnapshotPriceTotal: string;
139
+ packageSnapshotNone: string;
140
+ packageSnapshotShowRaw: string;
141
+ packageSnapshotHideRaw: string;
142
+ }
143
+
144
+ /** German default strings — apps can override them selectively. */
145
+ export const DEFAULT_I18N_DE: TenantPlanSectionI18n = {
146
+ sectionTitle: 'Paket & Verbrauch',
147
+ sectionSubtitle: 'Aktuelles Paket, Verbrauch und Bundles.',
148
+ loading: 'Lade …',
149
+ noSubscription: 'Keine Subscription für diesen Mandanten gefunden.',
150
+ activePlan: 'Aktuelles Paket',
151
+ cycleMonthly: 'Monatlich',
152
+ cycleYearly: 'Jährlich',
153
+ statusActive: 'Aktiv',
154
+ statusTrial: 'Testphase',
155
+ statusPastDue: 'Zahlung überfällig',
156
+ statusCanceled: 'Gekündigt',
157
+ statusPendingSales: 'Vertrieb-Klärung',
158
+ trialEndsAt: 'Testphase endet am',
159
+ pilotEndsAt: 'Pilot endet am',
160
+ nextBillingDate: 'Nächste Abrechnung',
161
+ pendingChange: 'Ausstehender Plan-Wechsel',
162
+ changeFromTo: 'Wechsel zu',
163
+ changeEffectiveAt: 'wirksam ab',
164
+ changePlanButton: 'Paket wechseln',
165
+ cancelSubscriptionButton: 'Abonnement kündigen',
166
+ usageTitle: 'Verbrauch',
167
+ featuresOverviewTitle: 'Leistungsumfang',
168
+ featuresActive: 'Enthalten',
169
+ featuresLocked: 'Nicht enthalten',
170
+ bundlesStoreTitle: 'Bundles',
171
+ bundlesBookedTitle: 'Gebuchte Bundles',
172
+ bundlesAvailableTitle: 'Verfügbare Bundles',
173
+ bundlesAvailableEmpty: 'Aktuell sind keine zusätzlichen Bundles verfügbar.',
174
+ bundlesPerMonth: 'netto/Monat',
175
+ bundleBookAction: 'Bundle buchen',
176
+ bundleBookInProgress: 'Buchung läuft …',
177
+ bundleCancelAction: 'Kündigen',
178
+ bundleReactivateAction: 'Reaktivieren',
179
+ bundleReactivateConfirmTitle: 'Bundle reaktivieren',
180
+ bundleReactivateConfirmBody:
181
+ 'Die Kündigung wird rückgängig gemacht. Das Bundle bleibt gebucht, läuft regulär weiter und wird wieder abgerechnet.',
182
+ bundleCanceledAt: 'Gekündigt zum',
183
+ bundleMinimumTermUntil: 'Mindestlaufzeit bis',
184
+ bundleIncludesLabel: 'Enthält',
185
+ bundleAlreadyBooked: 'Bereits gebucht',
186
+ bundleIncompatible: 'Nicht kompatibel mit aktuellem Paket',
187
+ bundleMissingRequires: 'Benötigt',
188
+ bundlePreviewAddTitle: 'Bundle buchen',
189
+ bundlePreviewCancelTitle: 'Bundle kündigen',
190
+ bundlePreviewLoading: 'Vorschau wird berechnet …',
191
+ bundlePreviewProrationTitle: 'Anteilige Abrechnung',
192
+ bundlePreviewProratedNow: 'Heute anteilig fällig',
193
+ bundlePreviewProrationDays: 'Tage',
194
+ bundlePreviewNextPeriod: 'Regulär ab nächster Periode',
195
+ bundlePreviewTrialNote:
196
+ 'Während der Testphase fällt nichts an — die Abrechnung beginnt mit der ersten bezahlten Periode.',
197
+ bundlePreviewNoPrice: 'Für den aktuellen Abrechnungszyklus ist kein Listenpreis gepflegt.',
198
+ bundlePreviewMinimumTermLabel: 'Mindestlaufzeit',
199
+ bundlePreviewMinimumTermMonths: 'Monate, bis',
200
+ bundlePreviewMinimumTermNone: 'Keine Mindestlaufzeit',
201
+ bundlePreviewRedundantTitle: 'Bereits enthaltene Features (würden doppelt bezahlt)',
202
+ bundlePreviewRedundantCoveredByPlan: 'bereits im Plan',
203
+ bundlePreviewRedundantCoveredByBundle: 'bereits im Bundle',
204
+ bundlePreviewMissingRequiresTitle: 'Fehlende Voraussetzungen',
205
+ bundlePreviewBlockersTitle: 'Buchung nicht möglich',
206
+ bundlePreviewWarningsTitle: 'Hinweise',
207
+ bundlePreviewEffectiveAt: 'Kündigung wirksam zum',
208
+ bundlePreviewSavings: 'Ersparnis pro Periode ab Wirksamkeit',
209
+ bundlePreviewConfirmAdd: 'Kostenpflichtig buchen',
210
+ bundlePreviewConfirmCancel: 'Kündigung bestätigen',
211
+ bundlePreviewInProgress: 'Wird ausgeführt …',
212
+ bundlePreviewClose: 'Abbrechen',
213
+ pendingVersionTitle: 'Anstehende Plan-Änderung',
214
+ pendingVersionChipNonRegressive: 'Verbessernd',
215
+ pendingVersionChipRegressive: 'Bestätigung erforderlich',
216
+ pendingVersionEffectiveAt: 'Wirksam ab',
217
+ pendingVersionAcceptAction: 'Änderungen akzeptieren',
218
+ pendingVersionAcceptInProgress: 'Akzeptiere …',
219
+ pendingVersionAcceptedAt: 'Akzeptiert am',
220
+ wizardTitle: 'Paket wechseln',
221
+ wizardClose: 'Schließen',
222
+ wizardCurrent: 'Aktuell',
223
+ wizardBadgeCurrent: 'aktiv',
224
+ wizardBadgePopular: 'beliebt',
225
+ wizardPriceUnitMonthly: 'netto/Monat',
226
+ wizardPriceUnitYearly: 'netto/Jahr',
227
+ wizardPriceOnRequest: 'auf Anfrage',
228
+ wizardStepChoose: 'Paket wählen',
229
+ wizardStepChooseIntro:
230
+ 'Wähle dein Ziel-Paket und den Abrechnungszyklus. Im nächsten Schritt zeigen wir Verbrauchs-Check und Feature-Diff.',
231
+ wizardStepPreview: 'Vorschau',
232
+ wizardStepConfirm: 'Bestätigen',
233
+ wizardNext: 'Weiter',
234
+ wizardBack: 'Zurück',
235
+ wizardPreviewLoading: 'Vorschau wird berechnet …',
236
+ wizardEffectiveAtLabel: 'Wirksam ab',
237
+ wizardEffectiveImmediate: 'Wirksam sofort',
238
+ wizardProrationTitle: 'Anteilige Abrechnung',
239
+ wizardProrationLine: 'Mehrbetrag bis Periodenende:',
240
+ wizardProrationDays: 'Tage',
241
+ wizardLimitsTitle: 'Limit-Vergleich',
242
+ wizardLimitsUsed: 'Verbrauch',
243
+ wizardLimitsCurrent: 'Aktuell',
244
+ wizardLimitsTarget: 'Ziel',
245
+ wizardFeaturesGained: 'Neu freigeschaltet',
246
+ wizardFeaturesLost: 'Wegfallende Features',
247
+ wizardBlockersTitle: 'Verhinderungs-Gründe',
248
+ wizardConfirmImmediate: 'Der Wechsel wird sofort wirksam.',
249
+ wizardConfirmScheduled: 'Der Wechsel wird wirksam zum',
250
+ wizardConfirmAction: 'Wechsel bestätigen',
251
+ wizardConfirmInProgress: 'Wechsel läuft …',
252
+ wizardConfirmPriceTitle: 'Preisübersicht',
253
+ wizardConfirmProratedNow: 'Heute anteilig fällig',
254
+ wizardConfirmRecurringNext: 'Regulär ab nächster Periode',
255
+ wizardConfirmRecurringFrom: 'Regulär fällig ab',
256
+ wizardConfirmPerCycleMonthly: 'pro Monat',
257
+ wizardConfirmPerCycleYearly: 'pro Jahr',
258
+ wizardConfirmTrialNote: 'Während der Testphase fällt nichts an.',
259
+ wizardConfirmRecurringTrialEnd: 'Regulär ab Ende der Testphase',
260
+ wizardChangeTypeUpgrade: 'Upgrade',
261
+ wizardChangeTypeDowngrade: 'Downgrade',
262
+ wizardChangeTypeCycle: 'Zyklus-Wechsel',
263
+ wizardChangeTypeNoop: 'Keine Änderung',
264
+ packageSnapshotTitle: 'Gebuchtes Paket (Snapshot)',
265
+ packageSnapshotSubtitle:
266
+ 'Schreibgeschützte Kopie des Pakets, wie es beim Abschluss vermarktet wurde.',
267
+ packageSnapshotCapturedAt: 'Erfasst am',
268
+ packageSnapshotOfferRef: 'Angebots-Referenz',
269
+ packageSnapshotPlanLabel: 'Plan',
270
+ packageSnapshotPlanVersionLabel: 'Plan-Version',
271
+ packageSnapshotCycleLabel: 'Abrechnungszyklus',
272
+ packageSnapshotBundlesLabel: 'Enthaltene Bundles',
273
+ packageSnapshotBundlesEmpty: 'Keine Bundles im Paket.',
274
+ packageSnapshotPriceMonthly: 'Preis monatlich',
275
+ packageSnapshotPriceYearly: 'Preis jährlich',
276
+ packageSnapshotPriceTotal: 'Gesamtpreis',
277
+ packageSnapshotNone: 'Diese Subscription wurde nicht über ein Webseiten-Angebot abgeschlossen.',
278
+ packageSnapshotShowRaw: 'Rohdaten anzeigen',
279
+ packageSnapshotHideRaw: 'Rohdaten ausblenden',
280
+ };
@@ -0,0 +1,304 @@
1
+ <template>
2
+ <q-dialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)">
3
+ <q-card class="sp-bundle-preview">
4
+ <q-card-section class="sp-bundle-preview__head">
5
+ <div class="sp-bundle-preview__title">
6
+ {{ isCancel ? i18n.bundlePreviewCancelTitle : i18n.bundlePreviewAddTitle }}
7
+ </div>
8
+ <div v-if="preview" class="sp-bundle-preview__bundle">
9
+ {{ preview.bundle.label }}
10
+ </div>
11
+ </q-card-section>
12
+
13
+ <q-separator />
14
+
15
+ <q-card-section v-if="loading" class="sp-bundle-preview__loading">
16
+ <q-spinner size="24px" />
17
+ <span>{{ i18n.bundlePreviewLoading }}</span>
18
+ </q-card-section>
19
+
20
+ <q-card-section v-else-if="error" class="sp-bundle-preview__error">
21
+ {{ error }}
22
+ </q-card-section>
23
+
24
+ <template v-else-if="preview">
25
+ <!-- Blocker: booking/cancellation not possible -->
26
+ <q-card-section v-if="preview.blockers.length > 0" class="sp-bundle-preview__blockers">
27
+ <div class="sp-bundle-preview__block-title">
28
+ {{ i18n.bundlePreviewBlockersTitle }}
29
+ </div>
30
+ <ul>
31
+ <li v-for="blocker in preview.blockers" :key="blocker.code">
32
+ {{ blocker.message }}
33
+ </li>
34
+ </ul>
35
+ <!-- requires blocker with resolved feature labels (#35) -->
36
+ <div
37
+ v-if="addPreview && addPreview.missingRequires.length > 0"
38
+ class="sp-bundle-preview__requires"
39
+ >
40
+ <span class="sp-bundle-preview__block-subtitle">
41
+ {{ i18n.bundlePreviewMissingRequiresTitle }}:
42
+ </span>
43
+ {{ addPreview.missingRequires.map(featureLabel).join(', ') }}
44
+ </div>
45
+ </q-card-section>
46
+
47
+ <!-- Add preview: price + proration -->
48
+ <q-card-section v-if="addPreview" class="sp-bundle-preview__price">
49
+ <div class="sp-bundle-preview__block-title">
50
+ {{ i18n.bundlePreviewProrationTitle }}
51
+ </div>
52
+ <div v-if="addPreview.proration" class="sp-bundle-preview__price-row">
53
+ <span>
54
+ {{ i18n.bundlePreviewProratedNow }}
55
+ ({{ addPreview.proration.daysRemainingInPeriod }}/{{
56
+ addPreview.proration.daysInPeriod
57
+ }}
58
+ {{ i18n.bundlePreviewProrationDays }})
59
+ </span>
60
+ <strong>{{ formatCurrency(addPreview.proration.prorataDeltaNet) }}</strong>
61
+ </div>
62
+ <div v-else-if="isTrial" class="sp-bundle-preview__note">
63
+ {{ i18n.bundlePreviewTrialNote }}
64
+ </div>
65
+ <div v-else class="sp-bundle-preview__note">
66
+ {{ i18n.bundlePreviewNoPrice }}
67
+ </div>
68
+ <div
69
+ v-if="addPreview.nextPeriodPriceNet !== null"
70
+ class="sp-bundle-preview__price-row"
71
+ >
72
+ <span>{{ i18n.bundlePreviewNextPeriod }}</span>
73
+ <strong>
74
+ {{ formatCurrency(addPreview.nextPeriodPriceNet) }}
75
+ <small>{{ cycleUnit }}</small>
76
+ </strong>
77
+ </div>
78
+ <div class="sp-bundle-preview__price-row">
79
+ <span>{{ i18n.bundlePreviewMinimumTermLabel }}</span>
80
+ <span v-if="addPreview.minimumTermMonths > 0 && addPreview.minimumTermEndsAt">
81
+ {{ addPreview.minimumTermMonths }}
82
+ {{ i18n.bundlePreviewMinimumTermMonths }}
83
+ {{ formatDate(addPreview.minimumTermEndsAt) }}
84
+ </span>
85
+ <span v-else>{{ i18n.bundlePreviewMinimumTermNone }}</span>
86
+ </div>
87
+ </q-card-section>
88
+
89
+ <!-- Add preview: redundancy hint (AK-13) -->
90
+ <q-card-section
91
+ v-if="addPreview && addPreview.redundantFeatures.length > 0"
92
+ class="sp-bundle-preview__warnings"
93
+ >
94
+ <div class="sp-bundle-preview__block-title">
95
+ {{ i18n.bundlePreviewRedundantTitle }}
96
+ </div>
97
+ <ul>
98
+ <li v-for="hint in addPreview.redundantFeatures" :key="hint.featureKey">
99
+ {{ featureLabel(hint.featureKey) }} —
100
+ {{
101
+ hint.coveredBy === 'PLAN'
102
+ ? i18n.bundlePreviewRedundantCoveredByPlan
103
+ : i18n.bundlePreviewRedundantCoveredByBundle
104
+ }}
105
+ „{{ hint.coveredByKey }}"
106
+ </li>
107
+ </ul>
108
+ </q-card-section>
109
+
110
+ <!-- Cancel preview: effective date + savings -->
111
+ <q-card-section v-if="cancelPreview" class="sp-bundle-preview__price">
112
+ <div class="sp-bundle-preview__price-row">
113
+ <span>{{ i18n.bundlePreviewEffectiveAt }}</span>
114
+ <strong>{{ formatDate(cancelPreview.effectiveAt) }}</strong>
115
+ </div>
116
+ <div
117
+ v-if="cancelPreview.nextPeriodSavingsNet !== null"
118
+ class="sp-bundle-preview__price-row"
119
+ >
120
+ <span>{{ i18n.bundlePreviewSavings }}</span>
121
+ <strong>{{ formatCurrency(cancelPreview.nextPeriodSavingsNet) }}</strong>
122
+ </div>
123
+ </q-card-section>
124
+
125
+ <!-- Warnings (e.g. MINIMUM_TERM_BINDS) -->
126
+ <q-card-section
127
+ v-if="otherWarnings.length > 0"
128
+ class="sp-bundle-preview__warnings"
129
+ >
130
+ <div class="sp-bundle-preview__block-title">
131
+ {{ i18n.bundlePreviewWarningsTitle }}
132
+ </div>
133
+ <ul>
134
+ <li v-for="warning in otherWarnings" :key="warning.code">
135
+ {{ warning.message }}
136
+ </li>
137
+ </ul>
138
+ </q-card-section>
139
+ </template>
140
+
141
+ <q-separator />
142
+
143
+ <q-card-actions align="right">
144
+ <q-btn flat :label="i18n.bundlePreviewClose" :disable="submitting" @click="close" />
145
+ <q-btn
146
+ :color="isCancel ? 'negative' : 'primary'"
147
+ unelevated
148
+ :label="
149
+ submitting
150
+ ? i18n.bundlePreviewInProgress
151
+ : isCancel
152
+ ? i18n.bundlePreviewConfirmCancel
153
+ : i18n.bundlePreviewConfirmAdd
154
+ "
155
+ :loading="submitting"
156
+ :disable="!canConfirm"
157
+ @click="emit('confirm')"
158
+ />
159
+ </q-card-actions>
160
+ </q-card>
161
+ </q-dialog>
162
+ </template>
163
+
164
+ <script setup lang="ts">
165
+ import { computed } from 'vue';
166
+ import type { TenantPlanSectionI18n } from '../default-i18n.js';
167
+ import type {
168
+ BundleAddPreviewShape,
169
+ BundleCancelPreviewShape,
170
+ BundlePreviewShape,
171
+ } from '../../use-tenant-billing.js';
172
+
173
+ // BundlePreviewDialog (#37/#61) — preview BEFORE bundle add/cancel in the
174
+ // tenant self-service: proration until period end, next-period price,
175
+ // redundancy hint (AK-13), requires blocker and minimum-term warning.
176
+ // Booking is only possible when the `blockers` list is empty.
177
+
178
+ const REDUNDANT_WARNING_CODE = 'REDUNDANT_FEATURES';
179
+
180
+ const props = defineProps<{
181
+ modelValue: boolean;
182
+ /** null while the preview is loading. */
183
+ preview: BundlePreviewShape | null;
184
+ loading: boolean;
185
+ /** Error while loading the preview (preview endpoint). */
186
+ error: string | null;
187
+ /** Confirm mutation in progress (add/cancel). */
188
+ submitting: boolean;
189
+ /** Subscription status — during TRIAL there is deliberately no proration. */
190
+ subscriptionStatus: string;
191
+ i18n: TenantPlanSectionI18n;
192
+ formatCurrency: (n: number) => string;
193
+ formatDate: (iso: string) => string;
194
+ featureLabel: (key: string) => string;
195
+ }>();
196
+
197
+ const emit = defineEmits<{
198
+ 'update:modelValue': [value: boolean];
199
+ confirm: [];
200
+ }>();
201
+
202
+ const isCancel = computed(() => props.preview?.action === 'cancel');
203
+ const isTrial = computed(() => props.subscriptionStatus === 'TRIAL');
204
+
205
+ const addPreview = computed<BundleAddPreviewShape | null>(() =>
206
+ props.preview?.action === 'add' ? props.preview : null,
207
+ );
208
+ const cancelPreview = computed<BundleCancelPreviewShape | null>(() =>
209
+ props.preview?.action === 'cancel' ? props.preview : null,
210
+ );
211
+
212
+ // REDUNDANT_FEATURES has its own feature-resolved section — the generic
213
+ // warning for it would be a duplicate.
214
+ const otherWarnings = computed(() =>
215
+ (props.preview?.warnings ?? []).filter((w) => w.code !== REDUNDANT_WARNING_CODE),
216
+ );
217
+
218
+ const canConfirm = computed(
219
+ () =>
220
+ !props.loading &&
221
+ !props.error &&
222
+ props.preview !== null &&
223
+ props.preview.blockers.length === 0 &&
224
+ !props.submitting,
225
+ );
226
+
227
+ const cycleUnit = computed(() =>
228
+ props.preview?.billingCycle === 'YEARLY'
229
+ ? props.i18n.wizardConfirmPerCycleYearly
230
+ : props.i18n.wizardConfirmPerCycleMonthly,
231
+ );
232
+
233
+ function close() {
234
+ emit('update:modelValue', false);
235
+ }
236
+ </script>
237
+
238
+ <style scoped>
239
+ .sp-bundle-preview {
240
+ min-width: 420px;
241
+ max-width: 560px;
242
+ }
243
+ .sp-bundle-preview__head {
244
+ padding-bottom: 8px;
245
+ }
246
+ .sp-bundle-preview__title {
247
+ font-size: 16px;
248
+ font-weight: 600;
249
+ }
250
+ .sp-bundle-preview__bundle {
251
+ margin-top: 2px;
252
+ color: var(--sp-text-secondary, rgba(0, 0, 0, 0.6));
253
+ }
254
+ .sp-bundle-preview__loading {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 12px;
258
+ color: var(--sp-text-secondary, rgba(0, 0, 0, 0.6));
259
+ }
260
+ .sp-bundle-preview__error {
261
+ color: var(--q-negative, #c10015);
262
+ }
263
+ .sp-bundle-preview__blockers {
264
+ background: rgba(193, 0, 21, 0.08);
265
+ color: var(--q-negative, #c10015);
266
+ }
267
+ .sp-bundle-preview__warnings {
268
+ background: rgba(242, 192, 55, 0.12);
269
+ }
270
+ .sp-bundle-preview__block-title {
271
+ font-weight: 600;
272
+ font-size: 13px;
273
+ margin-bottom: 6px;
274
+ }
275
+ .sp-bundle-preview__block-subtitle {
276
+ font-weight: 500;
277
+ }
278
+ .sp-bundle-preview__blockers ul,
279
+ .sp-bundle-preview__warnings ul {
280
+ margin: 0;
281
+ padding-left: 18px;
282
+ font-size: 13px;
283
+ }
284
+ .sp-bundle-preview__requires {
285
+ margin-top: 6px;
286
+ font-size: 13px;
287
+ }
288
+ .sp-bundle-preview__price-row {
289
+ display: flex;
290
+ justify-content: space-between;
291
+ gap: 16px;
292
+ padding: 4px 0;
293
+ font-size: 14px;
294
+ }
295
+ .sp-bundle-preview__price-row small {
296
+ font-weight: 400;
297
+ color: var(--sp-text-muted, rgba(0, 0, 0, 0.55));
298
+ }
299
+ .sp-bundle-preview__note {
300
+ font-size: 13px;
301
+ color: var(--sp-text-muted, rgba(0, 0, 0, 0.55));
302
+ padding: 4px 0;
303
+ }
304
+ </style>