@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,126 @@
1
+ // Shared types for platform dialogs. Extracted from the .vue files
2
+ // because vue-tsc does not support `type X from "...vue"`.
3
+
4
+ export interface PilotCreatePayload {
5
+ tenant: { name: string; slug?: string; legalForm?: string; vatId?: string };
6
+ admin: {
7
+ email: string;
8
+ firstName: string;
9
+ lastName: string;
10
+ initialPassword?: string;
11
+ };
12
+ pilot: { plan: string; note?: string; endsAt?: string };
13
+ }
14
+
15
+ export interface PilotCreateResult {
16
+ slug: string;
17
+ /** If the server generated an initial password, include it here. */
18
+ initialPassword?: string;
19
+ }
20
+
21
+ /** Edit an existing pilot subscription (plan, end date, note). */
22
+ export interface PilotEditPayload {
23
+ /** Optional — only the fields that are set get changed. */
24
+ plan?: string;
25
+ /** `null` clears the end date (open-ended pilot phase). */
26
+ endsAt?: string | null;
27
+ /** `null` or empty clears the note. */
28
+ note?: string | null;
29
+ }
30
+
31
+ export interface PilotEditResult {
32
+ slug: string;
33
+ changed?: string[];
34
+ }
35
+
36
+ /**
37
+ * Tenant-specific vocabulary for the pilot dialogs. The platform
38
+ * provides neutral defaults ("Mandant"); consumers override with
39
+ * their domain language (e.g. "Verein" or "Händler").
40
+ */
41
+ export interface PilotCopy {
42
+ /** Subtitle of the tenant section in the create dialog. */
43
+ tenantSubtitle?: string;
44
+ /** Label of the name field, e.g. "Vereinsname" / "Firmenname". */
45
+ tenantNameLabel?: string;
46
+ /** Placeholder for the tenant name. */
47
+ tenantNamePlaceholder?: string;
48
+ /** Placeholder for the slug. */
49
+ slugPlaceholder?: string;
50
+ /** Placeholder for the initial admin email. */
51
+ adminEmailPlaceholder?: string;
52
+ /** Placeholder for the internal note (create + edit). */
53
+ notePlaceholder?: string;
54
+ }
55
+
56
+ /** Neutral, tenant-agnostic defaults for {@link PilotCopy}. */
57
+ export const DEFAULT_PILOT_COPY: Required<PilotCopy> = {
58
+ tenantSubtitle: 'Stammdaten des Mandanten',
59
+ tenantNameLabel: 'Name des Mandanten',
60
+ tenantNamePlaceholder: 'z. B. Muster GmbH',
61
+ slugPlaceholder: 'muster-mandant',
62
+ adminEmailPlaceholder: 'admin@example.com',
63
+ notePlaceholder: 'z. B. Sales-Pilot · Demo-Zugang',
64
+ };
65
+
66
+ export type PromoCodeValueType = 'PERCENT' | 'ABSOLUTE';
67
+ export type PromoCodeDurationType = 'ONCE' | 'MONTHS' | 'BILLING_CYCLES';
68
+
69
+ export interface PromoCodeCreatePayload {
70
+ code: string;
71
+ valueType: PromoCodeValueType;
72
+ value: number;
73
+ durationType: PromoCodeDurationType;
74
+ durationValue: number | null;
75
+ maxRedemptions: number | null;
76
+ validFrom: string | null;
77
+ validUntil: string | null;
78
+ /** Plan keys the code applies to. Empty = all plans. */
79
+ appliesToPlans?: string[];
80
+ /** Optional: filter on MONTHLY/YEARLY subscriptions. */
81
+ appliesToBilling?: 'MONTHLY' | 'YEARLY';
82
+ /** Only redeemable by new customers. */
83
+ firstTimeCustomersOnly?: boolean;
84
+ /** Minimum gross plan amount from which the code takes effect. */
85
+ minimumPlanAmountGross?: number;
86
+ /** Allows €0 invoices (otherwise the discount is capped at 100% of the amount). */
87
+ allowZeroInvoice?: boolean;
88
+ /** Ledger account for discount revenue reduction (app-specific). */
89
+ revenueDeductionAccount?: string;
90
+ campaignTag?: string;
91
+ description?: string;
92
+ }
93
+
94
+ /** Plan option for the plan picker in PromoCodeCreateDialog. */
95
+ export interface PromoCodePlanOption {
96
+ /** Plan key as sent to the backend (e.g. 'BASIC'). */
97
+ key: string;
98
+ /** Display label (e.g. 'Basic'). */
99
+ label: string;
100
+ /** Optional accent for the plan chip; fallback neutral gray. */
101
+ color?: string;
102
+ }
103
+
104
+ /**
105
+ * PATCH payload — all fields optional, only the explicitly set ones
106
+ * are sent to the backend (whitelist on the server side). `code` is
107
+ * not in the list because it stays stable after creation.
108
+ */
109
+ export interface PromoCodeUpdatePayload {
110
+ status?: 'ACTIVE' | 'PAUSED';
111
+ valueType?: PromoCodeValueType;
112
+ value?: number;
113
+ durationType?: PromoCodeDurationType;
114
+ durationValue?: number | null;
115
+ maxRedemptions?: number | null;
116
+ validFrom?: string | null;
117
+ validUntil?: string | null;
118
+ appliesToPlans?: string[];
119
+ appliesToBilling?: 'MONTHLY' | 'YEARLY' | null;
120
+ firstTimeCustomersOnly?: boolean;
121
+ minimumPlanAmountGross?: number | null;
122
+ allowZeroInvoice?: boolean;
123
+ description?: string | null;
124
+ campaignTag?: string | null;
125
+ revenueDeductionAccount?: string | null;
126
+ }
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div class="sp-cycle" role="radiogroup" :aria-label="i18n.ariaLabel">
3
+ <button
4
+ type="button"
5
+ class="sp-cycle__btn"
6
+ :class="{ 'sp-cycle__btn--active': modelValue === 'MONTHLY' }"
7
+ role="radio"
8
+ :aria-checked="modelValue === 'MONTHLY'"
9
+ @click="emit('update:modelValue', 'MONTHLY')"
10
+ >
11
+ {{ i18n.monthly }}
12
+ </button>
13
+ <button
14
+ type="button"
15
+ class="sp-cycle__btn"
16
+ :class="{ 'sp-cycle__btn--active': modelValue === 'YEARLY' }"
17
+ role="radio"
18
+ :aria-checked="modelValue === 'YEARLY'"
19
+ @click="emit('update:modelValue', 'YEARLY')"
20
+ >
21
+ {{ i18n.yearly }}
22
+ <span v-if="i18n.savePill" class="sp-cycle__pill">{{ i18n.savePill }}</span>
23
+ </button>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import type { BillingCycleStr } from '../../use-tenant-billing.js';
29
+
30
+ interface I18n {
31
+ ariaLabel: string;
32
+ monthly: string;
33
+ yearly: string;
34
+ /** Optional: Spar-Pill neben "Jährlich" (z. B. "−2 Mt"). */
35
+ savePill?: string;
36
+ }
37
+
38
+ const props = defineProps<{
39
+ modelValue: BillingCycleStr;
40
+ i18n: I18n;
41
+ }>();
42
+
43
+ const emit = defineEmits<{
44
+ 'update:modelValue': [BillingCycleStr];
45
+ }>();
46
+
47
+ void props;
48
+ </script>
49
+
50
+ <style scoped>
51
+ .sp-cycle {
52
+ display: inline-flex;
53
+ /* CSS-Vars: dark-mode-overrides unten via :global(.body--dark) */
54
+ --sp-cycle-bg: rgba(15, 118, 110, 0.06);
55
+ --sp-cycle-btn-fg: rgba(0, 0, 0, 0.55);
56
+ --sp-cycle-btn-active-bg: #fff;
57
+ --sp-cycle-btn-active-fg: var(--q-primary, #0f766e);
58
+ background: var(--sp-cycle-bg);
59
+ border-radius: 12px;
60
+ padding: 3px;
61
+ gap: 2px;
62
+ }
63
+ :global(.body--dark) .sp-cycle {
64
+ --sp-cycle-bg: rgba(255, 255, 255, 0.08);
65
+ --sp-cycle-btn-fg: rgba(255, 255, 255, 0.7);
66
+ --sp-cycle-btn-active-bg: rgba(255, 255, 255, 0.92);
67
+ --sp-cycle-btn-active-fg: var(--q-primary, #0f766e);
68
+ }
69
+ .sp-cycle__btn {
70
+ padding: 8px 14px;
71
+ border: none;
72
+ background: none;
73
+ font-family: inherit;
74
+ font-size: 12px;
75
+ font-weight: 700;
76
+ color: var(--sp-cycle-btn-fg);
77
+ cursor: pointer;
78
+ border-radius: 9px;
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ letter-spacing: 0.02em;
83
+ transition: all 180ms;
84
+ }
85
+ .sp-cycle__btn--active {
86
+ background: var(--sp-cycle-btn-active-bg);
87
+ color: var(--sp-cycle-btn-active-fg);
88
+ box-shadow: 0 2px 8px rgba(15, 118, 110, 0.12);
89
+ }
90
+ .sp-cycle__pill {
91
+ background: rgba(22, 163, 74, 0.16);
92
+ color: #166534;
93
+ font-size: 10px;
94
+ padding: 2px 7px;
95
+ border-radius: 999px;
96
+ font-family: 'SF Mono', Consolas, monospace;
97
+ letter-spacing: 0.04em;
98
+ }
99
+ </style>
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <div class="sp-models">
3
+ <button
4
+ v-for="plan in plans"
5
+ :key="plan.id"
6
+ type="button"
7
+ class="sp-model"
8
+ :class="{
9
+ 'sp-model--selected': modelValue === plan.id,
10
+ 'sp-model--current': plan.id === currentPlanId,
11
+ }"
12
+ :aria-pressed="modelValue === plan.id"
13
+ @click="emit('update:modelValue', plan.id)"
14
+ >
15
+ <span v-if="plan.popular && plan.id !== currentPlanId" class="sp-model__flag">
16
+ {{ i18n.popular }}
17
+ </span>
18
+ <span
19
+ v-else-if="plan.id === currentPlanId"
20
+ class="sp-model__flag sp-model__flag--current"
21
+ >
22
+ {{ i18n.current }}
23
+ </span>
24
+
25
+ <div class="sp-model__name">{{ plan.name }}</div>
26
+ <div class="sp-model__tagline">{{ plan.tagline }}</div>
27
+
28
+ <div class="sp-model__price">
29
+ <template v-if="planPrice(plan) !== null">
30
+ <strong>{{ formatCurrency(planPrice(plan)!) }}</strong>
31
+ <span class="sp-model__price-cycle">
32
+ /{{ cycle === 'YEARLY' ? i18n.perYear : i18n.perMonth }}
33
+ </span>
34
+ </template>
35
+ <template v-else>
36
+ {{ i18n.priceOnRequest }}
37
+ </template>
38
+ </div>
39
+
40
+ <div class="sp-model__quotas">
41
+ <div v-for="key in catalogQuotaKeys" :key="key" class="sp-model__quota">
42
+ <strong>{{ formatQuotaValue(key, plan.quotas[key]) }}</strong>
43
+ <span>{{ quotaLabel(key) }}</span>
44
+ </div>
45
+ </div>
46
+ </button>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import type { CatalogPlan } from '../../use-tenant-billing-catalog.js';
52
+ import type { BillingCycleStr } from '../../use-tenant-billing.js';
53
+
54
+ interface I18n {
55
+ popular: string;
56
+ current: string;
57
+ perMonth: string;
58
+ perYear: string;
59
+ priceOnRequest: string;
60
+ }
61
+
62
+ const props = defineProps<{
63
+ modelValue: string | null;
64
+ plans: CatalogPlan[];
65
+ cycle: BillingCycleStr;
66
+ /** quotaKeys to display, in order. */
67
+ catalogQuotaKeys: string[];
68
+ /** Current subscription, optional — when set, the plan card is marked with "Aktuell". */
69
+ currentPlanId?: string | null;
70
+ formatCurrency: (n: number) => string;
71
+ /** Formats a single Quota value (`-1` → ∞, otherwise number + unit). */
72
+ formatQuotaValue: (key: string, value: number) => string;
73
+ quotaLabel: (key: string) => string;
74
+ i18n: I18n;
75
+ }>();
76
+
77
+ const emit = defineEmits<{
78
+ 'update:modelValue': [string];
79
+ }>();
80
+
81
+ function planPrice(plan: CatalogPlan): number | null {
82
+ if (props.cycle === 'YEARLY') {
83
+ return plan.yearlyNet ?? (plan.monthlyNet != null ? plan.monthlyNet * 10 : null);
84
+ }
85
+ return plan.monthlyNet;
86
+ }
87
+ </script>
88
+
89
+ <style scoped>
90
+ .sp-models {
91
+ display: grid;
92
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
93
+ gap: 12px;
94
+ }
95
+ /* CSS vars for light + dark mode. Quasar's `body--dark` triggers the dark set. */
96
+ .sp-models {
97
+ --sp-card-bg: #fff;
98
+ --sp-card-border: rgba(0, 0, 0, 0.1);
99
+ --sp-card-border-soft: rgba(0, 0, 0, 0.08);
100
+ --sp-card-current-bg: rgba(0, 0, 0, 0.02);
101
+ --sp-card-selected-grad: rgba(15, 118, 110, 0.04);
102
+ --sp-text-strong: rgba(0, 0, 0, 0.85);
103
+ --sp-text-muted: rgba(0, 0, 0, 0.55);
104
+ --sp-flag-current-bg: rgba(0, 0, 0, 0.6);
105
+ --sp-flag-current-fg: #fff;
106
+ }
107
+ :global(.body--dark) .sp-models {
108
+ --sp-card-bg: rgba(255, 255, 255, 0.06);
109
+ --sp-card-border: rgba(255, 255, 255, 0.2);
110
+ --sp-card-border-soft: rgba(255, 255, 255, 0.12);
111
+ --sp-card-current-bg: rgba(255, 255, 255, 0.1);
112
+ --sp-card-selected-grad: rgba(15, 118, 110, 0.25);
113
+ --sp-text-strong: rgba(255, 255, 255, 0.92);
114
+ --sp-text-muted: rgba(255, 255, 255, 0.65);
115
+ --sp-flag-current-bg: rgba(255, 255, 255, 0.85);
116
+ --sp-flag-current-fg: #0f172a;
117
+ }
118
+ .sp-model {
119
+ position: relative;
120
+ text-align: left;
121
+ cursor: pointer;
122
+ font-family: inherit;
123
+ background: var(--sp-card-bg);
124
+ border: 1.5px solid var(--sp-card-border);
125
+ border-radius: 18px;
126
+ padding: 16px;
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 8px;
130
+ transition: all 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
131
+ color: var(--sp-text-strong);
132
+ }
133
+ .sp-model:hover {
134
+ transform: translateY(-3px);
135
+ border-color: rgba(15, 118, 110, 0.55);
136
+ }
137
+ .sp-model--selected {
138
+ border-color: var(--q-primary, #0f766e);
139
+ background: linear-gradient(180deg, var(--sp-card-selected-grad), var(--sp-card-bg) 70%);
140
+ box-shadow: 0 14px 30px rgba(15, 118, 110, 0.16);
141
+ }
142
+ .sp-model--current {
143
+ background: var(--sp-card-current-bg);
144
+ }
145
+ .sp-model__flag {
146
+ position: absolute;
147
+ top: -10px;
148
+ right: 14px;
149
+ background: var(--q-primary, #0f766e);
150
+ color: #fff;
151
+ font-size: 10px;
152
+ font-weight: 700;
153
+ padding: 4px 10px;
154
+ border-radius: 999px;
155
+ letter-spacing: 0.04em;
156
+ text-transform: uppercase;
157
+ }
158
+ .sp-model__flag--current {
159
+ background: var(--sp-flag-current-bg);
160
+ color: var(--sp-flag-current-fg);
161
+ }
162
+ .sp-model__name {
163
+ font-size: 17px;
164
+ font-weight: 700;
165
+ color: var(--sp-text-strong);
166
+ }
167
+ .sp-model__tagline {
168
+ font-size: 12px;
169
+ color: var(--sp-text-muted);
170
+ line-height: 1.4;
171
+ }
172
+ .sp-model__price {
173
+ display: flex;
174
+ align-items: baseline;
175
+ gap: 4px;
176
+ margin-top: 4px;
177
+ }
178
+ .sp-model__price strong {
179
+ font-size: 22px;
180
+ font-weight: 800;
181
+ color: var(--sp-text-strong);
182
+ }
183
+ .sp-model__price-cycle {
184
+ font-size: 11px;
185
+ color: var(--sp-text-muted);
186
+ }
187
+ .sp-model__quotas {
188
+ display: grid;
189
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
190
+ gap: 6px;
191
+ margin-top: 4px;
192
+ padding-top: 10px;
193
+ border-top: 1px dashed var(--sp-card-border-soft);
194
+ }
195
+ .sp-model__quota {
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 1px;
199
+ font-size: 11px;
200
+ }
201
+ .sp-model__quota strong {
202
+ font-size: 14px;
203
+ color: var(--sp-text-strong);
204
+ }
205
+ .sp-model__quota span {
206
+ color: var(--sp-text-muted);
207
+ font-size: 10px;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.06em;
210
+ }
211
+ </style>