@saasicat/ui-vue 0.19.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{catalog-JXdKdIqG.d.cts → catalog-DJcfm5N2.d.cts} +21 -7
- package/dist/{catalog-JXdKdIqG.d.ts → catalog-DJcfm5N2.d.ts} +21 -7
- package/dist/{chunk-DPTIOQXX.js → chunk-F7NRM6KI.js} +56 -28
- package/dist/{chunk-RVG6LQHL.js → chunk-LY52T4N4.js} +21 -2
- package/dist/{chunk-ORX3ZG47.js → chunk-XMMFN2WJ.js} +74 -3
- package/dist/client/index.cjs +134 -29
- package/dist/client/index.d.cts +53 -3
- package/dist/client/index.d.ts +53 -3
- package/dist/client/index.js +14 -2
- package/dist/index.cjs +321 -162
- package/dist/index.d.cts +30 -8
- package/dist/index.d.ts +30 -8
- package/dist/index.js +176 -133
- package/dist/quasar/index.cjs +78 -29
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +6 -2
- package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
- package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
- package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
- package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
- package/package.json +18 -8
- package/src/client/i18n/messages/bundles.ts +0 -2
- package/src/client/i18n/messages/common.ts +24 -0
- package/src/client/i18n/messages/discovery.ts +2 -2
- package/src/client/i18n/messages/marketing.ts +2 -4
- package/src/client/i18n/messages/plan-detail.ts +0 -4
- package/src/client/i18n/messages/plans.ts +20 -8
- package/src/client/i18n/messages/tenants.ts +0 -2
- package/src/client/index.ts +1 -0
- package/src/client/manifest-loader.ts +1 -1
- package/src/client/resolve-plans.ts +144 -0
- package/src/components/BundleVersionPublishDialog.vue +6 -6
- package/src/components/KvBlock.vue +4 -4
- package/src/components/MarketingPromotionsTab.vue +34 -48
- package/src/components/MfaPromptDialog.vue +8 -2
- package/src/components/TenantActionConfirmDialog.vue +4 -4
- package/src/components/VersionDiffPreview.vue +7 -7
- package/src/components/admin-page/AdminBody.vue +40 -0
- package/src/components/admin-page/AdminFilters.vue +12 -0
- package/src/components/admin-page/AdminHero.vue +49 -0
- package/src/components/admin-page/AdminKpi.vue +84 -0
- package/src/components/admin-page/AdminPage.vue +16 -0
- package/src/components/admin-page/AdminPaginator.vue +195 -0
- package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
- package/src/components/admin-page/AdminSection.vue +53 -0
- package/src/components/admin-page/AdminStatistics.vue +40 -0
- package/src/components/admin-page/AdminTable.vue +175 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
- package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
- package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
- package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
- package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
- package/src/components/dialogs/PilotCreateDialog.vue +12 -12
- package/src/components/dialogs/PilotEditDialog.vue +7 -7
- package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
- package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
- package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
- package/src/components/plan/PlanGrid.vue +1 -1
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
- package/src/components/plan-detail/PlanAuditLog.vue +7 -8
- package/src/components/plan-detail/PlanDetail.vue +95 -292
- package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
- package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
- package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
- package/src/components/plan-list/PlanList.vue +206 -457
- package/src/components/plan-matrix/PlanMatrix.vue +66 -183
- package/src/components/plan-review/PlanReview.vue +191 -275
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
- package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
- package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
- package/src/index.ts +1 -0
- package/src/pages-standard/AdminLayout.vue +38 -12
- package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
- package/src/pages-standard/AuditPage.vue +65 -109
- package/src/pages-standard/BundlesPage.vue +146 -179
- package/src/pages-standard/DashboardPage.vue +59 -154
- package/src/pages-standard/DiscoveryPage.vue +172 -227
- package/src/pages-standard/EmailHistoryPage.vue +118 -159
- package/src/pages-standard/MarketingCatalogPage.vue +399 -440
- package/src/pages-standard/PilotsPage.vue +69 -117
- package/src/pages-standard/PlansPage.vue +240 -112
- package/src/pages-standard/PlatformEmailPage.vue +56 -91
- package/src/pages-standard/PromoCodeDetailPage.vue +49 -74
- package/src/pages-standard/PromoCodesPage.vue +95 -128
- package/src/pages-standard/SubscriptionsPage.vue +19 -52
- package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
- package/src/pages-standard/SuperAdminSetupWizard.vue +22 -18
- package/src/pages-standard/TenantDetailPage.vue +100 -191
- package/src/pages-standard/TenantsPage.vue +190 -295
- package/src/pages-standard/UsersPage.vue +89 -157
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
- package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
- package/src/pages-standard/bundles-page/BundlesToolbar.vue +63 -0
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
- package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
- package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
- package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +43 -62
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
- package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
- package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
- package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
- package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
- package/src/pages-standard/sa-theme.css +552 -86
- package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
- package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
- package/src/pages-standard/tenant-detail/TenantUsers.vue +15 -0
- package/src/pages-standard/tenant-detail/types.ts +38 -0
- package/src/pages-standard/tenants/StatusPill.vue +2 -2
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
- package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
- package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
- package/src/pages-tenant/TenantPlanSection.vue +12 -20
- package/src/quasar/create-super-admin-app.ts +4 -0
- package/src/vue/entitlement-provider.ts +0 -1
- package/src/vue/feature-router-guard.ts +0 -1
- package/src/vue/super-admin-context.ts +106 -1
- package/src/vue/use-api-list.ts +2 -1
- package/src/vue/use-mfa-prompt.ts +72 -0
- package/src/vue/use-pagination.ts +63 -0
- package/src/vue/use-plans.ts +1 -2
- package/src/vue/use-sign-out.ts +90 -0
- package/src/vue/use-tenant-billing.ts +1 -1
- package/src/vue/use-tenant-manifest.ts +0 -1
- package/src/components/KpiCard.vue +0 -53
- package/src/components/plan-cockpit/PlanCockpit.vue +0 -1012
- package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
- package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -100
- package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
- package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
- package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
- package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -251
- package/src/components/plan-cockpit/types.ts +0 -72
- package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
- package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
- package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
|
@@ -27,6 +27,9 @@ export const commonMessages = defineMessages(
|
|
|
27
27
|
loading: 'Lade …',
|
|
28
28
|
saving: 'Speichere …',
|
|
29
29
|
loadingData: 'Daten werden geladen…',
|
|
30
|
+
noData: 'Keine Daten vorhanden',
|
|
31
|
+
filters: 'Filter',
|
|
32
|
+
results: 'Ergebnisse',
|
|
30
33
|
error: 'Fehler',
|
|
31
34
|
errorLoadFailed: 'Laden fehlgeschlagen',
|
|
32
35
|
errorSaveFailed: 'Speichern fehlgeschlagen',
|
|
@@ -73,6 +76,15 @@ export const commonMessages = defineMessages(
|
|
|
73
76
|
both: 'Beide',
|
|
74
77
|
createFailed: 'Anlegen fehlgeschlagen',
|
|
75
78
|
noChanges: 'Keine Änderungen',
|
|
79
|
+
paginator: {
|
|
80
|
+
label: 'Seitennavigation',
|
|
81
|
+
rowsPerPage: 'Zeilen pro Seite',
|
|
82
|
+
all: 'Alle',
|
|
83
|
+
page: 'Seite {page}/{pages}',
|
|
84
|
+
pageInput: 'Seite, 1 bis {pages}',
|
|
85
|
+
previous: 'Vorherige Seite',
|
|
86
|
+
next: 'Nächste Seite',
|
|
87
|
+
},
|
|
76
88
|
},
|
|
77
89
|
{
|
|
78
90
|
save: 'Save',
|
|
@@ -97,6 +109,9 @@ export const commonMessages = defineMessages(
|
|
|
97
109
|
loading: 'Loading …',
|
|
98
110
|
saving: 'Saving …',
|
|
99
111
|
loadingData: 'Loading data…',
|
|
112
|
+
noData: 'No data available',
|
|
113
|
+
filters: 'Filters',
|
|
114
|
+
results: 'Results',
|
|
100
115
|
error: 'Error',
|
|
101
116
|
errorLoadFailed: 'Failed to load',
|
|
102
117
|
errorSaveFailed: 'Failed to save',
|
|
@@ -143,5 +158,14 @@ export const commonMessages = defineMessages(
|
|
|
143
158
|
both: 'Both',
|
|
144
159
|
createFailed: 'Creation failed',
|
|
145
160
|
noChanges: 'No changes',
|
|
161
|
+
paginator: {
|
|
162
|
+
label: 'Pagination',
|
|
163
|
+
rowsPerPage: 'Rows per page',
|
|
164
|
+
all: 'All',
|
|
165
|
+
page: 'Page {page}/{pages}',
|
|
166
|
+
pageInput: 'Page, 1 to {pages}',
|
|
167
|
+
previous: 'Previous page',
|
|
168
|
+
next: 'Next page',
|
|
169
|
+
},
|
|
146
170
|
},
|
|
147
171
|
);
|
|
@@ -91,7 +91,7 @@ export const discoveryMessages = defineMessages(
|
|
|
91
91
|
usageProvider: 'UsageProvider',
|
|
92
92
|
usageProviderMissing: 'fehlt',
|
|
93
93
|
noUsageProviderWarning:
|
|
94
|
-
'Eine harte Quota ohne UsageProvider ist nicht deploy-fähig (Preflight
|
|
94
|
+
'Eine harte Quota ohne UsageProvider ist nicht deploy-fähig (Preflight).',
|
|
95
95
|
outdatedBanner:
|
|
96
96
|
'Die code-abgeleiteten Quota-Fakten (Einheit/Enforcement/Provider) haben sich seit der letzten Freigabe geändert — bitte prüfen und',
|
|
97
97
|
},
|
|
@@ -202,7 +202,7 @@ export const discoveryMessages = defineMessages(
|
|
|
202
202
|
usageProvider: 'UsageProvider',
|
|
203
203
|
usageProviderMissing: 'missing',
|
|
204
204
|
noUsageProviderWarning:
|
|
205
|
-
'A hard quota without a UsageProvider is not deployable (preflight
|
|
205
|
+
'A hard quota without a UsageProvider is not deployable (preflight).',
|
|
206
206
|
outdatedBanner:
|
|
207
207
|
'The code-derived quota facts (unit/enforcement/provider) have changed since the last approval — please review and',
|
|
208
208
|
},
|
|
@@ -16,7 +16,6 @@ export const marketingMessages = defineMessages(
|
|
|
16
16
|
defaultLocale: 'Default-Sprache',
|
|
17
17
|
deactivateLocale: 'Sprache deaktivieren',
|
|
18
18
|
addLocale: '+ Sprache',
|
|
19
|
-
refresh: 'Aktualisieren',
|
|
20
19
|
},
|
|
21
20
|
page: {
|
|
22
21
|
loading: 'Lade Marketing-Catalog …',
|
|
@@ -89,7 +88,7 @@ export const marketingMessages = defineMessages(
|
|
|
89
88
|
promotionsTab: {
|
|
90
89
|
title: 'Aktionen & Rabatte',
|
|
91
90
|
subtitle:
|
|
92
|
-
'Zeitlich begrenzte Preis-Aktionen — überschreiben den regulären Preis auf der Pricing-Page
|
|
91
|
+
'Zeitlich begrenzte Preis-Aktionen — überschreiben den regulären Preis auf der Pricing-Page.',
|
|
93
92
|
add: '+ Aktion anlegen',
|
|
94
93
|
timelineHead: 'Aktions-Timeline · gestrichelt = heute',
|
|
95
94
|
today: 'heute',
|
|
@@ -150,7 +149,6 @@ export const marketingMessages = defineMessages(
|
|
|
150
149
|
defaultLocale: 'Default language',
|
|
151
150
|
deactivateLocale: 'Deactivate language',
|
|
152
151
|
addLocale: '+ Language',
|
|
153
|
-
refresh: 'Refresh',
|
|
154
152
|
},
|
|
155
153
|
page: {
|
|
156
154
|
loading: 'Loading marketing catalog …',
|
|
@@ -221,7 +219,7 @@ export const marketingMessages = defineMessages(
|
|
|
221
219
|
promotionsTab: {
|
|
222
220
|
title: 'Promotions & discounts',
|
|
223
221
|
subtitle:
|
|
224
|
-
'Time-limited price promotions — they override the regular price on the pricing page
|
|
222
|
+
'Time-limited price promotions — they override the regular price on the pricing page.',
|
|
225
223
|
add: '+ Create promotion',
|
|
226
224
|
timelineHead: 'Promotion timeline · dashed = today',
|
|
227
225
|
today: 'today',
|
|
@@ -26,10 +26,8 @@ export const planDetailMessages = defineMessages(
|
|
|
26
26
|
versionsSummary: '{published} live/superseded',
|
|
27
27
|
versionsSummaryWithDrafts: '{published} live/superseded · {drafts} Draft',
|
|
28
28
|
openDraft: 'Offener Draft',
|
|
29
|
-
noDraft: 'Kein Draft',
|
|
30
29
|
readyToEdit: 'Bereit zum Bearbeiten',
|
|
31
30
|
noOpenDraft: 'Keine Draft-Version offen',
|
|
32
|
-
createNewDraft: 'Neue Draft anlegen',
|
|
33
31
|
},
|
|
34
32
|
versions: {
|
|
35
33
|
title: 'Versionen',
|
|
@@ -157,10 +155,8 @@ export const planDetailMessages = defineMessages(
|
|
|
157
155
|
versionsSummary: '{published} live/superseded',
|
|
158
156
|
versionsSummaryWithDrafts: '{published} live/superseded · {drafts} draft',
|
|
159
157
|
openDraft: 'Open draft',
|
|
160
|
-
noDraft: 'No draft',
|
|
161
158
|
readyToEdit: 'Ready to edit',
|
|
162
159
|
noOpenDraft: 'No draft version open',
|
|
163
|
-
createNewDraft: 'Create a new draft',
|
|
164
160
|
},
|
|
165
161
|
versions: {
|
|
166
162
|
title: 'Versions',
|
|
@@ -5,6 +5,8 @@ export const plansMessages = defineMessages(
|
|
|
5
5
|
// PlansPage — shell of the plan area: loading state, toasts and the
|
|
6
6
|
// error texts of the draft/publish/archive/discard actions.
|
|
7
7
|
page: {
|
|
8
|
+
subtitle:
|
|
9
|
+
'Pläne bündeln Features & Quotas zu einem Angebot. Jede Änderung wird als neue Version veröffentlicht — Live-Versionen bleiben unangetastet.',
|
|
8
10
|
loading: 'Pläne werden geladen…',
|
|
9
11
|
toastPlanCreated: 'Plan {planKey} angelegt — jetzt die erste Version zusammenstellen.',
|
|
10
12
|
toastDraftSaved: 'Draft v{version} von {planKey} gespeichert.',
|
|
@@ -68,8 +70,10 @@ export const plansMessages = defineMessages(
|
|
|
68
70
|
// PlanList — default plan overview with one row per plan.
|
|
69
71
|
list: {
|
|
70
72
|
title: 'Alle Pläne',
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
statPlans: 'Pläne',
|
|
74
|
+
statLive: 'Live',
|
|
75
|
+
statDrafts: 'Offene Drafts',
|
|
76
|
+
statTenants: 'Mandanten',
|
|
73
77
|
matrixView: 'Matrix-Ansicht',
|
|
74
78
|
newPlan: 'Neuer Plan',
|
|
75
79
|
searchPlaceholder: 'Plan suchen…',
|
|
@@ -108,8 +112,10 @@ export const plansMessages = defineMessages(
|
|
|
108
112
|
// PlanMatrix — all plans side by side, components as rows.
|
|
109
113
|
matrix: {
|
|
110
114
|
title: 'Pläne · Matrix-Vergleich',
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
statPlans: 'Pläne',
|
|
116
|
+
statFeatures: 'Features',
|
|
117
|
+
statQuotas: 'Quotas',
|
|
118
|
+
statBundles: 'Bundles',
|
|
113
119
|
catalogPreview: 'Catalog-Vorschau',
|
|
114
120
|
createPlan: 'Plan anlegen',
|
|
115
121
|
createPlanSub: 'oder Klon erstellen',
|
|
@@ -260,6 +266,8 @@ export const plansMessages = defineMessages(
|
|
|
260
266
|
},
|
|
261
267
|
{
|
|
262
268
|
page: {
|
|
269
|
+
subtitle:
|
|
270
|
+
'Plans bundle features & quotas into an offering. Every change ships as a new version — live versions stay untouched.',
|
|
263
271
|
loading: 'Loading plans…',
|
|
264
272
|
toastPlanCreated: 'Plan {planKey} created — now assemble the first version.',
|
|
265
273
|
toastDraftSaved: 'Draft v{version} of {planKey} saved.',
|
|
@@ -319,8 +327,10 @@ export const plansMessages = defineMessages(
|
|
|
319
327
|
},
|
|
320
328
|
list: {
|
|
321
329
|
title: 'All plans',
|
|
322
|
-
|
|
323
|
-
|
|
330
|
+
statPlans: 'Plans',
|
|
331
|
+
statLive: 'Live',
|
|
332
|
+
statDrafts: 'Open drafts',
|
|
333
|
+
statTenants: 'Tenants',
|
|
324
334
|
matrixView: 'Matrix view',
|
|
325
335
|
newPlan: 'New plan',
|
|
326
336
|
searchPlaceholder: 'Search plan…',
|
|
@@ -358,8 +368,10 @@ export const plansMessages = defineMessages(
|
|
|
358
368
|
},
|
|
359
369
|
matrix: {
|
|
360
370
|
title: 'Plans · matrix comparison',
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
statPlans: 'Plans',
|
|
372
|
+
statFeatures: 'Features',
|
|
373
|
+
statQuotas: 'Quotas',
|
|
374
|
+
statBundles: 'Bundles',
|
|
363
375
|
catalogPreview: 'Catalog preview',
|
|
364
376
|
createPlan: 'Create plan',
|
|
365
377
|
createPlanSub: 'or create a clone',
|
|
@@ -14,7 +14,6 @@ export const tenantsMessages = defineMessages(
|
|
|
14
14
|
columnCreatedAt: 'Angelegt',
|
|
15
15
|
suspended: 'Suspendiert',
|
|
16
16
|
empty: 'Keine Mandanten mit diesen Filtern.',
|
|
17
|
-
pagination: '{total} Mandanten · Seite {page} / {pages}',
|
|
18
17
|
},
|
|
19
18
|
detail: {
|
|
20
19
|
backToList: 'Mandanten-Liste',
|
|
@@ -63,7 +62,6 @@ export const tenantsMessages = defineMessages(
|
|
|
63
62
|
columnCreatedAt: 'Created',
|
|
64
63
|
suspended: 'Suspended',
|
|
65
64
|
empty: 'No tenants match these filters.',
|
|
66
|
-
pagination: '{total} tenants · Page {page} / {pages}',
|
|
67
65
|
},
|
|
68
66
|
detail: {
|
|
69
67
|
backToList: 'Tenant list',
|
package/src/client/index.ts
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Resolving plans into what a listing actually shows — which version is live,
|
|
2
|
+
// which are scheduled, which is the open draft — and the four counts derived
|
|
3
|
+
// from that.
|
|
4
|
+
//
|
|
5
|
+
// It lives outside the components because two of them need the same answer:
|
|
6
|
+
// `PlanList` renders the rows, and `PlansPage` shows the counts above it. The
|
|
7
|
+
// alternative was the same sixty lines in both, drifting apart the first time
|
|
8
|
+
// a validity rule changed.
|
|
9
|
+
//
|
|
10
|
+
// Framework-free on purpose: no Vue, so the rules can be tested as plain data
|
|
11
|
+
// in and data out.
|
|
12
|
+
|
|
13
|
+
export interface PlanRowLike {
|
|
14
|
+
id: string;
|
|
15
|
+
planKey: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description?: string | null;
|
|
18
|
+
sortOrder: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PlanVersionLike {
|
|
22
|
+
id: string;
|
|
23
|
+
publishedAt?: string | null;
|
|
24
|
+
validFrom?: string | null;
|
|
25
|
+
validUntil?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolvedPlan<P extends PlanRowLike, V extends PlanVersionLike> {
|
|
29
|
+
plan: P;
|
|
30
|
+
planKey: string;
|
|
31
|
+
label: string;
|
|
32
|
+
description: string | null;
|
|
33
|
+
/** Version currently active for new bookings. */
|
|
34
|
+
currentLive: V | null;
|
|
35
|
+
/** Version shown on the parent row — the live one where there is one. */
|
|
36
|
+
primary: V | null;
|
|
37
|
+
/** Future-published versions first, then drafts. */
|
|
38
|
+
subRows: V[];
|
|
39
|
+
hasAnyVersion: boolean;
|
|
40
|
+
/** Every version has run out: hidden from the admin listing. */
|
|
41
|
+
allExpired: boolean;
|
|
42
|
+
draft: V | null;
|
|
43
|
+
tenantCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function todayIsoDate(): string {
|
|
47
|
+
return new Date().toISOString().slice(0, 10);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isCurrentlyValid(v: PlanVersionLike, today: string): boolean {
|
|
51
|
+
if (!v.publishedAt) return false;
|
|
52
|
+
if (v.validFrom && v.validFrom.slice(0, 10) > today) return false;
|
|
53
|
+
if (v.validUntil && v.validUntil.slice(0, 10) < today) return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isFutureScheduled(v: PlanVersionLike, today: string): boolean {
|
|
58
|
+
if (!v.publishedAt) return false;
|
|
59
|
+
if (!v.validFrom) return false;
|
|
60
|
+
return v.validFrom.slice(0, 10) > today;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isExpired(v: PlanVersionLike, today: string): boolean {
|
|
64
|
+
if (!v.publishedAt) return false;
|
|
65
|
+
if (!v.validUntil) return false;
|
|
66
|
+
return v.validUntil.slice(0, 10) < today;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ResolvePlansInput<P extends PlanRowLike, V extends PlanVersionLike> {
|
|
70
|
+
plans: readonly P[];
|
|
71
|
+
versionsByPlanId: Record<string, V[]>;
|
|
72
|
+
tenantCountsByPlanKey: Record<string, number>;
|
|
73
|
+
/** Injectable for tests; defaults to today. */
|
|
74
|
+
today?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolvePlans<P extends PlanRowLike, V extends PlanVersionLike>({
|
|
78
|
+
plans,
|
|
79
|
+
versionsByPlanId,
|
|
80
|
+
tenantCountsByPlanKey,
|
|
81
|
+
today = todayIsoDate(),
|
|
82
|
+
}: ResolvePlansInput<P, V>): ResolvedPlan<P, V>[] {
|
|
83
|
+
return [...plans]
|
|
84
|
+
.sort((a, b) => a.sortOrder - b.sortOrder || a.planKey.localeCompare(b.planKey))
|
|
85
|
+
.map<ResolvedPlan<P, V>>((plan) => {
|
|
86
|
+
const versions = versionsByPlanId[plan.id] ?? [];
|
|
87
|
+
const drafts = versions.filter((v) => !v.publishedAt);
|
|
88
|
+
const published = versions.filter((v) => Boolean(v.publishedAt));
|
|
89
|
+
const currentLive = published.find((v) => isCurrentlyValid(v, today)) ?? null;
|
|
90
|
+
const futureScheduled = published
|
|
91
|
+
.filter((v) => isFutureScheduled(v, today))
|
|
92
|
+
.sort((a, b) => (a.validFrom ?? '').localeCompare(b.validFrom ?? ''));
|
|
93
|
+
|
|
94
|
+
// Parent row: the live version where there is one, otherwise the
|
|
95
|
+
// next scheduled one, otherwise nothing — a plan with only drafts
|
|
96
|
+
// still gets a row.
|
|
97
|
+
const primary = currentLive ?? futureScheduled[0] ?? null;
|
|
98
|
+
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
if (primary) seen.add(primary.id);
|
|
101
|
+
const subRows: V[] = [];
|
|
102
|
+
for (const v of [...futureScheduled, ...drafts]) {
|
|
103
|
+
if (seen.has(v.id)) continue;
|
|
104
|
+
subRows.push(v);
|
|
105
|
+
seen.add(v.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
plan,
|
|
110
|
+
planKey: plan.planKey,
|
|
111
|
+
label: plan.label,
|
|
112
|
+
description: plan.description ?? null,
|
|
113
|
+
currentLive,
|
|
114
|
+
primary,
|
|
115
|
+
subRows,
|
|
116
|
+
hasAnyVersion: versions.length > 0,
|
|
117
|
+
allExpired:
|
|
118
|
+
versions.length > 0 &&
|
|
119
|
+
drafts.length === 0 &&
|
|
120
|
+
futureScheduled.length === 0 &&
|
|
121
|
+
published.every((v) => isExpired(v, today)),
|
|
122
|
+
draft: drafts[0] ?? null,
|
|
123
|
+
tenantCount: tenantCountsByPlanKey[plan.planKey] ?? 0,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface PlanCounts {
|
|
129
|
+
plans: number;
|
|
130
|
+
live: number;
|
|
131
|
+
drafts: number;
|
|
132
|
+
tenants: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function countPlans<P extends PlanRowLike, V extends PlanVersionLike>(
|
|
136
|
+
resolved: readonly ResolvedPlan<P, V>[],
|
|
137
|
+
): PlanCounts {
|
|
138
|
+
return {
|
|
139
|
+
plans: resolved.length,
|
|
140
|
+
live: resolved.filter((p) => p.currentLive !== null).length,
|
|
141
|
+
drafts: resolved.filter((p) => p.draft !== null).length,
|
|
142
|
+
tenants: resolved.reduce((sum, p) => sum + p.tenantCount, 0),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"
|
|
131
131
|
:loading="submitting"
|
|
132
132
|
:disable="!canSubmit"
|
|
133
|
-
@click="
|
|
133
|
+
@click="onSubmit"
|
|
134
134
|
/>
|
|
135
135
|
</q-card-actions>
|
|
136
136
|
</q-card>
|
|
@@ -246,7 +246,7 @@ function close(): void {
|
|
|
246
246
|
open.value = false;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
async function
|
|
249
|
+
async function onSubmit(): Promise<void> {
|
|
250
250
|
if (!canSubmit.value) return;
|
|
251
251
|
submitting.value = true;
|
|
252
252
|
try {
|
|
@@ -323,8 +323,8 @@ function formatValue(v: unknown): string {
|
|
|
323
323
|
}
|
|
324
324
|
.bvpd__validity {
|
|
325
325
|
padding: 12px;
|
|
326
|
-
background:
|
|
327
|
-
border: 1px solid
|
|
326
|
+
background: var(--sa-bg-surface-2);
|
|
327
|
+
border: 1px solid var(--sa-border);
|
|
328
328
|
border-radius: 8px;
|
|
329
329
|
}
|
|
330
330
|
.bvpd__validity-grid {
|
|
@@ -337,7 +337,7 @@ function formatValue(v: unknown): string {
|
|
|
337
337
|
gap: 6px;
|
|
338
338
|
font-size: 12px;
|
|
339
339
|
font-weight: 700;
|
|
340
|
-
color:
|
|
340
|
+
color: var(--sa-muted-dark);
|
|
341
341
|
}
|
|
342
342
|
.bvpd__input {
|
|
343
343
|
min-height: 36px;
|
|
@@ -345,7 +345,7 @@ function formatValue(v: unknown): string {
|
|
|
345
345
|
border-radius: 6px;
|
|
346
346
|
padding: 0 10px;
|
|
347
347
|
font: inherit;
|
|
348
|
-
color:
|
|
348
|
+
color: var(--sa-heading);
|
|
349
349
|
background: #fff;
|
|
350
350
|
}
|
|
351
351
|
.bvpd__error {
|
|
@@ -15,13 +15,13 @@ defineProps<{ label: string; value: string }>();
|
|
|
15
15
|
|
|
16
16
|
<style scoped>
|
|
17
17
|
.kv {
|
|
18
|
-
background:
|
|
19
|
-
border: 1px solid
|
|
18
|
+
background: var(--sa-bg-surface-2);
|
|
19
|
+
border: 1px solid var(--sa-border);
|
|
20
20
|
border-radius: 10px;
|
|
21
21
|
padding: 10px 12px;
|
|
22
22
|
}
|
|
23
23
|
.kv__label {
|
|
24
|
-
color:
|
|
24
|
+
color: var(--sa-muted);
|
|
25
25
|
font-size: 11px;
|
|
26
26
|
text-transform: uppercase;
|
|
27
27
|
letter-spacing: 0.05em;
|
|
@@ -30,7 +30,7 @@ defineProps<{ label: string; value: string }>();
|
|
|
30
30
|
.kv__value {
|
|
31
31
|
font-size: 14px;
|
|
32
32
|
font-weight: 600;
|
|
33
|
-
color:
|
|
33
|
+
color: var(--sa-heading);
|
|
34
34
|
margin-top: 4px;
|
|
35
35
|
}
|
|
36
36
|
</style>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{{ counts.expired }} {{ msg.promotionsTab.statusExpired }}
|
|
18
18
|
</span>
|
|
19
19
|
</div>
|
|
20
|
-
<button class="
|
|
20
|
+
<button class="sa-btn sa-btn--primary" type="button" :disabled="busy" @click="onAdd">
|
|
21
21
|
{{ msg.promotionsTab.add }}
|
|
22
22
|
</button>
|
|
23
23
|
</div>
|
|
@@ -325,7 +325,7 @@ import {
|
|
|
325
325
|
import { formatMessage } from '../client/i18n/format.js';
|
|
326
326
|
import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
327
327
|
|
|
328
|
-
// Promotions tab of the Marketing Catalog
|
|
328
|
+
// Promotions tab of the Marketing Catalog. Standalone child
|
|
329
329
|
// component — the MarketingCatalogPage wires `usePromotions` and passes the
|
|
330
330
|
// list + CRUD callbacks through.
|
|
331
331
|
|
|
@@ -586,7 +586,7 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
586
586
|
}
|
|
587
587
|
.mc-promo-subtitle {
|
|
588
588
|
font-size: 12px;
|
|
589
|
-
color:
|
|
589
|
+
color: var(--sa-muted);
|
|
590
590
|
}
|
|
591
591
|
.mc-promo-stats {
|
|
592
592
|
display: flex;
|
|
@@ -597,8 +597,8 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
597
597
|
font-weight: 600;
|
|
598
598
|
padding: 3px 9px;
|
|
599
599
|
border-radius: 999px;
|
|
600
|
-
background:
|
|
601
|
-
color:
|
|
600
|
+
background: var(--sa-border-soft);
|
|
601
|
+
color: var(--sa-muted-dark);
|
|
602
602
|
}
|
|
603
603
|
.mc-promo-stat.active {
|
|
604
604
|
background: #dcfce7;
|
|
@@ -606,54 +606,40 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
606
606
|
}
|
|
607
607
|
.mc-promo-stat.scheduled {
|
|
608
608
|
background: #dbeafe;
|
|
609
|
-
color:
|
|
609
|
+
color: var(--sa-primary-strong);
|
|
610
610
|
}
|
|
611
611
|
.mc-promo-stat.expired {
|
|
612
|
-
background:
|
|
613
|
-
color:
|
|
614
|
-
}
|
|
615
|
-
.mc-promo-add {
|
|
616
|
-
border: 0;
|
|
617
|
-
background: #3f6bff;
|
|
618
|
-
color: #fff;
|
|
619
|
-
font-weight: 600;
|
|
620
|
-
font-size: 13px;
|
|
621
|
-
padding: 8px 14px;
|
|
622
|
-
border-radius: 8px;
|
|
623
|
-
cursor: pointer;
|
|
624
|
-
}
|
|
625
|
-
.mc-promo-add:disabled {
|
|
626
|
-
opacity: 0.5;
|
|
627
|
-
cursor: default;
|
|
612
|
+
background: var(--sa-border-soft);
|
|
613
|
+
color: var(--sa-muted-light);
|
|
628
614
|
}
|
|
629
615
|
.mc-promo-timeline {
|
|
630
616
|
background: #fff;
|
|
631
|
-
border: 1px solid
|
|
617
|
+
border: 1px solid var(--sa-border);
|
|
632
618
|
border-radius: 10px;
|
|
633
619
|
padding: 12px 14px;
|
|
634
620
|
}
|
|
635
621
|
.mc-promo-timeline-head {
|
|
636
622
|
font-size: 11px;
|
|
637
|
-
color:
|
|
623
|
+
color: var(--sa-muted);
|
|
638
624
|
margin-bottom: 6px;
|
|
639
625
|
}
|
|
640
626
|
.mc-promo-timeline-chart {
|
|
641
627
|
position: relative;
|
|
642
|
-
border-left: 1px solid
|
|
628
|
+
border-left: 1px solid var(--sa-border);
|
|
643
629
|
}
|
|
644
630
|
.mc-promo-tick {
|
|
645
631
|
position: absolute;
|
|
646
632
|
top: 0;
|
|
647
633
|
bottom: 0;
|
|
648
634
|
width: 1px;
|
|
649
|
-
background:
|
|
635
|
+
background: var(--sa-border-soft);
|
|
650
636
|
}
|
|
651
637
|
.mc-promo-tick-label {
|
|
652
638
|
position: absolute;
|
|
653
639
|
bottom: 0;
|
|
654
640
|
left: 3px;
|
|
655
641
|
font-size: 9px;
|
|
656
|
-
color:
|
|
642
|
+
color: var(--sa-muted-light);
|
|
657
643
|
white-space: nowrap;
|
|
658
644
|
}
|
|
659
645
|
.mc-promo-today {
|
|
@@ -694,7 +680,7 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
694
680
|
.mc-promo-empty {
|
|
695
681
|
padding: 28px;
|
|
696
682
|
text-align: center;
|
|
697
|
-
color:
|
|
683
|
+
color: var(--sa-muted-light);
|
|
698
684
|
font-size: 13px;
|
|
699
685
|
border: 1px dashed #cbd5e1;
|
|
700
686
|
border-radius: 10px;
|
|
@@ -709,7 +695,7 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
709
695
|
align-items: center;
|
|
710
696
|
gap: 12px;
|
|
711
697
|
background: #fff;
|
|
712
|
-
border: 1px solid
|
|
698
|
+
border: 1px solid var(--sa-border);
|
|
713
699
|
border-radius: 8px;
|
|
714
700
|
padding: 10px 12px;
|
|
715
701
|
cursor: pointer;
|
|
@@ -743,30 +729,30 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
743
729
|
.mc-promo-typechip {
|
|
744
730
|
font-size: 10px;
|
|
745
731
|
font-weight: 700;
|
|
746
|
-
background:
|
|
747
|
-
color:
|
|
732
|
+
background: var(--sa-primary-50);
|
|
733
|
+
color: var(--sa-primary-strong);
|
|
748
734
|
padding: 1px 6px;
|
|
749
735
|
border-radius: 4px;
|
|
750
736
|
}
|
|
751
737
|
.mc-promo-planchip {
|
|
752
738
|
font-size: 10px;
|
|
753
|
-
background:
|
|
754
|
-
color:
|
|
739
|
+
background: var(--sa-border-soft);
|
|
740
|
+
color: var(--sa-muted-dark);
|
|
755
741
|
padding: 1px 6px;
|
|
756
742
|
border-radius: 4px;
|
|
757
743
|
}
|
|
758
744
|
.mc-promo-muted {
|
|
759
745
|
font-size: 11px;
|
|
760
|
-
color:
|
|
746
|
+
color: var(--sa-muted-light);
|
|
761
747
|
}
|
|
762
748
|
.mc-promo-row-when {
|
|
763
749
|
font-size: 11px;
|
|
764
|
-
color:
|
|
750
|
+
color: var(--sa-muted);
|
|
765
751
|
text-align: right;
|
|
766
752
|
}
|
|
767
753
|
.mc-promo-cycle {
|
|
768
754
|
display: block;
|
|
769
|
-
color:
|
|
755
|
+
color: var(--sa-muted-light);
|
|
770
756
|
}
|
|
771
757
|
.mc-promo-status {
|
|
772
758
|
font-size: 10px;
|
|
@@ -781,17 +767,17 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
781
767
|
}
|
|
782
768
|
.mc-promo-status.scheduled {
|
|
783
769
|
background: #dbeafe;
|
|
784
|
-
color:
|
|
770
|
+
color: var(--sa-primary-strong);
|
|
785
771
|
}
|
|
786
772
|
.mc-promo-status.expired {
|
|
787
|
-
background:
|
|
788
|
-
color:
|
|
773
|
+
background: var(--sa-border-soft);
|
|
774
|
+
color: var(--sa-muted-light);
|
|
789
775
|
}
|
|
790
776
|
.mc-promo-editor {
|
|
791
777
|
border: 1px solid #3f6bff;
|
|
792
778
|
border-top: 0;
|
|
793
779
|
border-radius: 0 0 8px 8px;
|
|
794
|
-
background:
|
|
780
|
+
background: var(--sa-bg-surface-2);
|
|
795
781
|
padding: 14px;
|
|
796
782
|
}
|
|
797
783
|
.mc-promo-editor-grid {
|
|
@@ -809,7 +795,7 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
809
795
|
font-weight: 700;
|
|
810
796
|
text-transform: uppercase;
|
|
811
797
|
letter-spacing: 0.5px;
|
|
812
|
-
color:
|
|
798
|
+
color: var(--sa-muted-dark);
|
|
813
799
|
margin-top: 6px;
|
|
814
800
|
}
|
|
815
801
|
.mc-promo-input {
|
|
@@ -841,8 +827,8 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
841
827
|
}
|
|
842
828
|
.mc-promo-typeopt.active {
|
|
843
829
|
border-color: #3f6bff;
|
|
844
|
-
background:
|
|
845
|
-
color:
|
|
830
|
+
background: var(--sa-primary-50);
|
|
831
|
+
color: var(--sa-primary-strong);
|
|
846
832
|
font-weight: 600;
|
|
847
833
|
}
|
|
848
834
|
.mc-promo-planlist {
|
|
@@ -862,10 +848,10 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
862
848
|
}
|
|
863
849
|
.mc-promo-planopt.active {
|
|
864
850
|
border-color: #3f6bff;
|
|
865
|
-
background:
|
|
851
|
+
background: var(--sa-primary-50);
|
|
866
852
|
}
|
|
867
853
|
.mc-promo-planopt code {
|
|
868
|
-
color:
|
|
854
|
+
color: var(--sa-muted-light);
|
|
869
855
|
font-size: 10px;
|
|
870
856
|
}
|
|
871
857
|
.mc-promo-i18n-block {
|
|
@@ -873,14 +859,14 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
873
859
|
flex-direction: column;
|
|
874
860
|
gap: 4px;
|
|
875
861
|
padding: 8px;
|
|
876
|
-
border: 1px solid
|
|
862
|
+
border: 1px solid var(--sa-border);
|
|
877
863
|
border-radius: 8px;
|
|
878
864
|
background: #fff;
|
|
879
865
|
}
|
|
880
866
|
.mc-promo-i18n-code {
|
|
881
867
|
font-size: 10px;
|
|
882
868
|
font-weight: 700;
|
|
883
|
-
background:
|
|
869
|
+
background: var(--sa-border-soft);
|
|
884
870
|
padding: 2px 7px;
|
|
885
871
|
border-radius: 5px;
|
|
886
872
|
align-self: flex-start;
|
|
@@ -897,7 +883,7 @@ async function onRemove(p: PromotionRow): Promise<void> {
|
|
|
897
883
|
cursor: pointer;
|
|
898
884
|
}
|
|
899
885
|
.mc-promo-colorbtn.active {
|
|
900
|
-
border-color:
|
|
886
|
+
border-color: var(--sa-heading);
|
|
901
887
|
}
|
|
902
888
|
.mc-promo-delete {
|
|
903
889
|
margin-top: 12px;
|