@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.
- package/LICENSE +202 -0
- package/README.md +47 -0
- package/dist/index.cjs +3776 -0
- package/dist/index.d.cts +2395 -0
- package/dist/index.d.ts +2395 -0
- package/dist/index.js +3659 -0
- package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
- package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
- package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
- package/dist/testing-e2e/admin-pages-suite.js +109 -0
- package/package.json +87 -0
- package/src/action-registry.ts +110 -0
- package/src/batch-column-fetcher.ts +152 -0
- package/src/boot-loader.ts +60 -0
- package/src/components/BundleVersionPublishDialog.vue +359 -0
- package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
- package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
- package/src/components/FeatureGate.vue +56 -0
- package/src/components/KpiCard.vue +53 -0
- package/src/components/KvBlock.vue +36 -0
- package/src/components/MarketingPromotionsTab.vue +861 -0
- package/src/components/MfaPromptDialog.vue +104 -0
- package/src/components/TenantActionConfirmDialog.vue +252 -0
- package/src/components/VersionDiffPreview.vue +150 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
- package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
- package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
- package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
- package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
- package/src/components/bundle-editor/bundle-version-status.ts +164 -0
- package/src/components/bundle-editor/catalog-i18n.ts +85 -0
- package/src/components/dialogs/PilotCreateDialog.vue +780 -0
- package/src/components/dialogs/PilotEditDialog.vue +548 -0
- package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
- package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
- package/src/components/dialogs/types.ts +126 -0
- package/src/components/plan/PlanCycleToggle.vue +99 -0
- package/src/components/plan/PlanGrid.vue +211 -0
- package/src/components/plan/PriceSummary.vue +293 -0
- package/src/components/plan/PromoCodeInput.vue +192 -0
- package/src/components/plan/PublicBundleGrid.vue +287 -0
- package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
- package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
- package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
- package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
- package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
- package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
- package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
- package/src/components/plan-cockpit/types.ts +72 -0
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
- package/src/components/plan-detail/PlanAuditLog.vue +59 -0
- package/src/components/plan-detail/PlanDetail.vue +1195 -0
- package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
- package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
- package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
- package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
- package/src/components/plan-detail/types.ts +59 -0
- package/src/components/plan-list/PlanList.vue +1067 -0
- package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
- package/src/components/plan-review/PlanReview.vue +742 -0
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
- package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
- package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
- package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
- package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
- package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
- package/src/components/plan-version-editor/types.ts +96 -0
- package/src/components/wizard-stepper/WizardStepper.vue +107 -0
- package/src/create-admin-routes.ts +68 -0
- package/src/create-super-admin-app.ts +345 -0
- package/src/entitlement-provider.ts +36 -0
- package/src/feature-router-guard.ts +77 -0
- package/src/http-json.ts +62 -0
- package/src/index.ts +98 -0
- package/src/manifest-loader.ts +139 -0
- package/src/manifest-store-factory.ts +114 -0
- package/src/nav-builder.ts +274 -0
- package/src/pages-standard/AdminLayout.vue +411 -0
- package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
- package/src/pages-standard/AuditPage.vue +223 -0
- package/src/pages-standard/BundlesPage.vue +910 -0
- package/src/pages-standard/BusinessTypesPage.vue +662 -0
- package/src/pages-standard/DashboardPage.vue +595 -0
- package/src/pages-standard/DiscoveryPage.vue +663 -0
- package/src/pages-standard/EmailHistoryPage.vue +576 -0
- package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
- package/src/pages-standard/PilotsPage.vue +577 -0
- package/src/pages-standard/PlanVersionsPage.vue +216 -0
- package/src/pages-standard/PlansPage.vue +1222 -0
- package/src/pages-standard/PlatformEmailPage.vue +434 -0
- package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
- package/src/pages-standard/PromoCodesPage.vue +589 -0
- package/src/pages-standard/SubscriptionsPage.vue +159 -0
- package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
- package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
- package/src/pages-standard/TenantDetailPage.vue +432 -0
- package/src/pages-standard/TenantsPage.vue +732 -0
- package/src/pages-standard/UsersPage.vue +504 -0
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
- package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
- package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
- package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
- package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
- package/src/pages-standard/bundles-page/types.ts +15 -0
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
- package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
- package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
- package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
- package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
- package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
- package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
- package/src/pages-standard/email-history.types.ts +48 -0
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
- package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
- package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
- package/src/pages-standard/marketing-catalog/types.ts +43 -0
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
- package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
- package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
- package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
- package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
- package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
- package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
- package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
- package/src/pages-standard/plan-versions/format.ts +53 -0
- package/src/pages-standard/plan-versions/types.ts +33 -0
- package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
- package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
- package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
- package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
- package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
- package/src/pages-standard/plans-page/types.ts +18 -0
- package/src/pages-standard/platform-email.types.ts +41 -0
- package/src/pages-standard/sa-theme.css +223 -0
- package/src/pages-standard/tenants/StatusPill.vue +62 -0
- package/src/pages-standard/tenants/format.ts +38 -0
- package/src/pages-tenant/LimitsRow.vue +67 -0
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
- package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
- package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
- package/src/pages-tenant/PendingVersionBanner.vue +126 -0
- package/src/pages-tenant/PlanChangeWizard.vue +630 -0
- package/src/pages-tenant/TenantPlanSection.vue +729 -0
- package/src/pages-tenant/UsageBar.vue +107 -0
- package/src/pages-tenant/default-i18n.ts +280 -0
- package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
- package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
- package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
- package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
- package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
- package/src/plan-versions-catalog.ts +368 -0
- package/src/platform-loaders.ts +68 -0
- package/src/project-page-host.ts +108 -0
- package/src/testing-e2e/admin-pages-suite.ts +187 -0
- package/src/types.ts +58 -0
- package/src/use-actions.ts +25 -0
- package/src/use-api-list.ts +151 -0
- package/src/use-audit-entries.ts +48 -0
- package/src/use-batch-columns.ts +59 -0
- package/src/use-bulk-publish.ts +150 -0
- package/src/use-bundle-versions-map.ts +112 -0
- package/src/use-bundles.ts +297 -0
- package/src/use-business-types.ts +269 -0
- package/src/use-catalog-entries.ts +243 -0
- package/src/use-discovery.ts +158 -0
- package/src/use-entitlement.ts +87 -0
- package/src/use-live-plan-versions.ts +126 -0
- package/src/use-manifest.ts +57 -0
- package/src/use-marketing-projections.ts +160 -0
- package/src/use-nav.ts +33 -0
- package/src/use-plan-editor.ts +162 -0
- package/src/use-plan-versions.ts +83 -0
- package/src/use-plans.ts +375 -0
- package/src/use-platform-tenant-actions.ts +286 -0
- package/src/use-promotions.ts +126 -0
- package/src/use-public-boot.ts +40 -0
- package/src/use-subscription-draft.ts +355 -0
- package/src/use-super-admin-context.ts +93 -0
- package/src/use-tenant-action-flow.ts +238 -0
- package/src/use-tenant-billing-catalog.ts +160 -0
- package/src/use-tenant-billing.ts +453 -0
- package/src/use-tenant-manifest.ts +94 -0
- package/src/use-tenant-subscription-bundles.ts +148 -0
- package/src/use-tenants.ts +61 -0
- package/src/version.ts +10 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3659 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
var ADMIN_UI_VERSION = "1.2.0";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
function defaultKvStore() {
|
|
6
|
+
if (typeof globalThis.localStorage === "undefined") {
|
|
7
|
+
return {
|
|
8
|
+
get: () => null,
|
|
9
|
+
set: () => {
|
|
10
|
+
},
|
|
11
|
+
remove: () => {
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const ls = globalThis.localStorage;
|
|
16
|
+
return {
|
|
17
|
+
get: (k) => ls.getItem(k),
|
|
18
|
+
set: (k, v) => ls.setItem(k, v),
|
|
19
|
+
remove: (k) => ls.removeItem(k)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function defaultHttpClient() {
|
|
23
|
+
return (url, init) => fetch(url, init);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/http-json.ts
|
|
27
|
+
var HttpJsonError = class extends Error {
|
|
28
|
+
constructor(status, code) {
|
|
29
|
+
super(code ?? `HTTP ${status}`);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.name = "HttpJsonError";
|
|
33
|
+
}
|
|
34
|
+
status;
|
|
35
|
+
code;
|
|
36
|
+
};
|
|
37
|
+
async function extractCode(res) {
|
|
38
|
+
try {
|
|
39
|
+
const body = await res.json();
|
|
40
|
+
return typeof body?.code === "string" ? body.code : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function trimTrailingSlashes(url) {
|
|
46
|
+
let end = url.length;
|
|
47
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
48
|
+
return url.slice(0, end);
|
|
49
|
+
}
|
|
50
|
+
async function getJson(http, url) {
|
|
51
|
+
const res = await http(url);
|
|
52
|
+
if (res.status < 200 || res.status >= 300) {
|
|
53
|
+
throw new HttpJsonError(res.status, await extractCode(res));
|
|
54
|
+
}
|
|
55
|
+
return await res.json();
|
|
56
|
+
}
|
|
57
|
+
async function postJson(http, url, body) {
|
|
58
|
+
const res = await http(url, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify(body)
|
|
62
|
+
});
|
|
63
|
+
if (res.status < 200 || res.status >= 300) {
|
|
64
|
+
throw new HttpJsonError(res.status, await extractCode(res));
|
|
65
|
+
}
|
|
66
|
+
return await res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/project-page-host.ts
|
|
70
|
+
import { computed, defineAsyncComponent, defineComponent, h, inject } from "vue";
|
|
71
|
+
import { useRoute } from "vue-router";
|
|
72
|
+
|
|
73
|
+
// src/create-super-admin-app.ts
|
|
74
|
+
import { createApp } from "vue";
|
|
75
|
+
import { Quasar, Notify, Dialog, Loading } from "quasar";
|
|
76
|
+
import { createPinia } from "pinia";
|
|
77
|
+
import {
|
|
78
|
+
createRouter,
|
|
79
|
+
createWebHistory
|
|
80
|
+
} from "vue-router";
|
|
81
|
+
var SUPER_ADMIN_BRAND_KEY = /* @__PURE__ */ Symbol.for(
|
|
82
|
+
"@saasicat/ui-vue/SUPER_ADMIN_BRAND"
|
|
83
|
+
);
|
|
84
|
+
var SUPER_ADMIN_ENDPOINTS_KEY = /* @__PURE__ */ Symbol.for(
|
|
85
|
+
"@saasicat/ui-vue/SUPER_ADMIN_ENDPOINTS"
|
|
86
|
+
);
|
|
87
|
+
var SUPER_ADMIN_EXTENSIONS_KEY = /* @__PURE__ */ Symbol.for(
|
|
88
|
+
"@saasicat/ui-vue/SUPER_ADMIN_EXTENSIONS"
|
|
89
|
+
);
|
|
90
|
+
var SUPER_ADMIN_ACTIONS_KEY = /* @__PURE__ */ Symbol.for(
|
|
91
|
+
"@saasicat/ui-vue/SUPER_ADMIN_ACTIONS"
|
|
92
|
+
);
|
|
93
|
+
var SUPER_ADMIN_MANIFEST_KEY = /* @__PURE__ */ Symbol.for(
|
|
94
|
+
"@saasicat/ui-vue/SUPER_ADMIN_MANIFEST"
|
|
95
|
+
);
|
|
96
|
+
var SUPER_ADMIN_LOGIN_ADAPTER_KEY = /* @__PURE__ */ Symbol.for(
|
|
97
|
+
"@saasicat/ui-vue/SUPER_ADMIN_LOGIN_ADAPTER"
|
|
98
|
+
);
|
|
99
|
+
var SUPER_ADMIN_HTTP_KEY = /* @__PURE__ */ Symbol.for(
|
|
100
|
+
"@saasicat/ui-vue/SUPER_ADMIN_HTTP"
|
|
101
|
+
);
|
|
102
|
+
var DEFAULT_QUASAR_OPTIONS = {
|
|
103
|
+
plugins: { Notify, Dialog, Loading },
|
|
104
|
+
config: { notify: { position: "top-right", timeout: 3e3 } }
|
|
105
|
+
};
|
|
106
|
+
function createSuperAdminApp(options) {
|
|
107
|
+
const app = createApp(options.rootComponent);
|
|
108
|
+
app.use(Quasar, options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS);
|
|
109
|
+
const pinia = createPinia();
|
|
110
|
+
app.use(pinia);
|
|
111
|
+
const router = createRouter({
|
|
112
|
+
history: options.routerHistory ?? createWebHistory(),
|
|
113
|
+
routes: options.appRoutes
|
|
114
|
+
});
|
|
115
|
+
const navGuard = buildNavigationGuard(options);
|
|
116
|
+
if (navGuard) {
|
|
117
|
+
router.beforeEach(navGuard);
|
|
118
|
+
}
|
|
119
|
+
app.use(router);
|
|
120
|
+
const endpoints = {
|
|
121
|
+
apiBase: options.endpoints.apiBase,
|
|
122
|
+
publicBootEndpoint: options.endpoints.publicBootEndpoint ?? `${options.endpoints.apiBase}/boot`,
|
|
123
|
+
manifestEndpoint: options.endpoints.manifestEndpoint ?? `${options.endpoints.apiBase}/manifest`
|
|
124
|
+
};
|
|
125
|
+
app.provide(SUPER_ADMIN_BRAND_KEY, { tag: "SuperAdmin", ...options.brand });
|
|
126
|
+
app.provide(SUPER_ADMIN_ENDPOINTS_KEY, endpoints);
|
|
127
|
+
app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
|
|
128
|
+
app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
|
|
129
|
+
if (options.manifestGuard?.getManifest) {
|
|
130
|
+
app.provide(SUPER_ADMIN_MANIFEST_KEY, options.manifestGuard.getManifest);
|
|
131
|
+
}
|
|
132
|
+
if (options.loginAdapter) {
|
|
133
|
+
app.provide(SUPER_ADMIN_LOGIN_ADAPTER_KEY, options.loginAdapter);
|
|
134
|
+
}
|
|
135
|
+
app.provide(SUPER_ADMIN_HTTP_KEY, options.http ?? defaultHttpClient());
|
|
136
|
+
for (const plugin of options.installPlugins ?? []) {
|
|
137
|
+
plugin(app);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
app,
|
|
141
|
+
router,
|
|
142
|
+
pinia,
|
|
143
|
+
mount: (selector) => app.mount(selector)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function buildNavigationGuard(options) {
|
|
147
|
+
const { authGuard, manifestGuard } = options;
|
|
148
|
+
if (!authGuard && !manifestGuard) return null;
|
|
149
|
+
return async (to) => {
|
|
150
|
+
if (to.meta?.public === true) return true;
|
|
151
|
+
if (authGuard) {
|
|
152
|
+
if (!authGuard.isAuthenticated()) return authGuard.onUnauthenticated();
|
|
153
|
+
if (authGuard.isSuperAdmin && !authGuard.isSuperAdmin()) {
|
|
154
|
+
return authGuard.onUnauthenticated();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (manifestGuard) {
|
|
158
|
+
try {
|
|
159
|
+
await manifestGuard.ensureLoaded();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (manifestGuard.errorRoute && to.path !== manifestGuard.errorRoute) {
|
|
162
|
+
return manifestGuard.errorRoute;
|
|
163
|
+
}
|
|
164
|
+
console.error("[SuperAdmin] manifest load failed", err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/project-page-host.ts
|
|
172
|
+
var asyncComponentCache = /* @__PURE__ */ new WeakMap();
|
|
173
|
+
function resolveAsync(extensions, key) {
|
|
174
|
+
let bucket = asyncComponentCache.get(extensions);
|
|
175
|
+
if (!bucket) {
|
|
176
|
+
bucket = /* @__PURE__ */ new Map();
|
|
177
|
+
asyncComponentCache.set(extensions, bucket);
|
|
178
|
+
}
|
|
179
|
+
const cached = bucket.get(key);
|
|
180
|
+
if (cached) return cached;
|
|
181
|
+
const loader = extensions[key];
|
|
182
|
+
if (!loader) return null;
|
|
183
|
+
const cmp = defineAsyncComponent(loader);
|
|
184
|
+
bucket.set(key, cmp);
|
|
185
|
+
return cmp;
|
|
186
|
+
}
|
|
187
|
+
var ProjectPageHost = defineComponent({
|
|
188
|
+
name: "ProjectPageHost",
|
|
189
|
+
setup() {
|
|
190
|
+
const route = useRoute();
|
|
191
|
+
const extensions = inject(SUPER_ADMIN_EXTENSIONS_KEY, {});
|
|
192
|
+
const manifestAccessor = inject(SUPER_ADMIN_MANIFEST_KEY, null);
|
|
193
|
+
const matchingPage = computed(() => {
|
|
194
|
+
const manifest = manifestAccessor ? manifestAccessor() : null;
|
|
195
|
+
if (!manifest) return null;
|
|
196
|
+
const pages = manifest.navigation?.projectPages ?? [];
|
|
197
|
+
return pages.find((p) => p.route === route.path) ?? null;
|
|
198
|
+
});
|
|
199
|
+
const resolvedComponent = computed(() => {
|
|
200
|
+
const page = matchingPage.value;
|
|
201
|
+
if (!page) return null;
|
|
202
|
+
return resolveAsync(extensions, page.componentKey);
|
|
203
|
+
});
|
|
204
|
+
return () => {
|
|
205
|
+
const page = matchingPage.value;
|
|
206
|
+
const cmp = resolvedComponent.value;
|
|
207
|
+
if (page && cmp) return h(cmp);
|
|
208
|
+
return h("div", { class: "sa-project-page-host__missing", role: "alert" }, [
|
|
209
|
+
h("h2", "Seite nicht verf\xFCgbar"),
|
|
210
|
+
h(
|
|
211
|
+
"p",
|
|
212
|
+
page ? `componentKey "${page.componentKey}" ist nicht in der extensions:-Map registriert. Apps m\xFCssen die Komponente in createSuperAdminApp({ extensions }) eintragen.` : `Keine Manifest-Definition f\xFCr ${route.path}. Entweder fehlt die ProjectPage im Manifest, oder die Capability ist nicht aktiv.`
|
|
213
|
+
)
|
|
214
|
+
]);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
function createProjectPageHostRoute(options) {
|
|
219
|
+
return {
|
|
220
|
+
path: options?.path ?? ":projectPagePath(.+)",
|
|
221
|
+
component: ProjectPageHost,
|
|
222
|
+
meta: { isProjectPageHost: true }
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/create-admin-routes.ts
|
|
227
|
+
function createAdminRoutes(options) {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
path: "/login",
|
|
231
|
+
name: "login",
|
|
232
|
+
component: options.loginPage,
|
|
233
|
+
meta: { public: true }
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
// `meta.public: true` MUST be set, otherwise the route runs through
|
|
237
|
+
// the manifest guard again → redirect loop.
|
|
238
|
+
path: "/admin-error",
|
|
239
|
+
name: "admin-error",
|
|
240
|
+
component: options.adminErrorPage,
|
|
241
|
+
meta: { public: true }
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
path: "/admin",
|
|
245
|
+
component: options.adminLayout,
|
|
246
|
+
children: [
|
|
247
|
+
{ path: "", redirect: options.dashboardPath ?? "/admin/dashboard" },
|
|
248
|
+
...options.children,
|
|
249
|
+
// Catch-all for manifest ProjectPages — Vue Router 4 matches
|
|
250
|
+
// more specific children first; the host only steps in when
|
|
251
|
+
// no static page matches. MUST come last.
|
|
252
|
+
createProjectPageHostRoute()
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
{ path: "/", redirect: "/admin" },
|
|
256
|
+
{ path: "/:pathMatch(.*)*", redirect: "/admin" }
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/boot-loader.ts
|
|
261
|
+
var BootLoadError = class extends Error {
|
|
262
|
+
constructor(status, message) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.status = status;
|
|
265
|
+
this.name = "BootLoadError";
|
|
266
|
+
}
|
|
267
|
+
status;
|
|
268
|
+
};
|
|
269
|
+
var BootLoader = class {
|
|
270
|
+
endpoint;
|
|
271
|
+
http;
|
|
272
|
+
constructor(options) {
|
|
273
|
+
if (!options?.endpoint) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'BootLoader: `endpoint` ist Pflicht (z. B. "/api/admin/boot" oder "/api/v1/admin/boot"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
this.endpoint = options.endpoint;
|
|
279
|
+
this.http = options.http ?? defaultHttpClient();
|
|
280
|
+
}
|
|
281
|
+
async load() {
|
|
282
|
+
const res = await this.http(this.endpoint);
|
|
283
|
+
if (res.status !== 200) {
|
|
284
|
+
throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
|
|
285
|
+
}
|
|
286
|
+
return await res.json();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/manifest-loader.ts
|
|
291
|
+
var ManifestLoadError = class extends Error {
|
|
292
|
+
constructor(status, message) {
|
|
293
|
+
super(message);
|
|
294
|
+
this.status = status;
|
|
295
|
+
this.name = "ManifestLoadError";
|
|
296
|
+
}
|
|
297
|
+
status;
|
|
298
|
+
};
|
|
299
|
+
var ManifestLoader = class {
|
|
300
|
+
endpoint;
|
|
301
|
+
http;
|
|
302
|
+
storage;
|
|
303
|
+
bodyKey;
|
|
304
|
+
etagKey;
|
|
305
|
+
getAuthToken;
|
|
306
|
+
constructor(options) {
|
|
307
|
+
if (!options?.endpoint) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
'ManifestLoader: `endpoint` ist Pflicht (z. B. "/api/admin/manifest" oder "/api/v1/admin/manifest"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
this.endpoint = options.endpoint;
|
|
313
|
+
this.http = options.http ?? defaultHttpClient();
|
|
314
|
+
this.storage = options.storage ?? defaultKvStore();
|
|
315
|
+
const prefix = options.storageKeyPrefix ?? "";
|
|
316
|
+
this.bodyKey = `${prefix}manifest:body`;
|
|
317
|
+
this.etagKey = `${prefix}manifest:etag`;
|
|
318
|
+
this.getAuthToken = options.getAuthToken;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Loads the current manifest. On a cache hit (304) the cached
|
|
322
|
+
* body is returned — otherwise the fresh server body.
|
|
323
|
+
*/
|
|
324
|
+
async load() {
|
|
325
|
+
const cachedEtag = this.storage.get(this.etagKey);
|
|
326
|
+
const headers = {};
|
|
327
|
+
const token = this.getAuthToken?.();
|
|
328
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
329
|
+
if (cachedEtag) headers["If-None-Match"] = cachedEtag;
|
|
330
|
+
const res = await this.http(this.endpoint, { method: "GET", headers });
|
|
331
|
+
if (res.status === 304) {
|
|
332
|
+
const cachedBody = this.readCachedBody();
|
|
333
|
+
if (!cachedBody) {
|
|
334
|
+
throw new ManifestLoadError(
|
|
335
|
+
304,
|
|
336
|
+
"Server lieferte 304, aber Cache-Body fehlt \u2014 bitte clearCache() + reload"
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return cachedBody.body;
|
|
340
|
+
}
|
|
341
|
+
if (res.status !== 200) {
|
|
342
|
+
throw new ManifestLoadError(
|
|
343
|
+
res.status,
|
|
344
|
+
`Manifest-Endpunkt antwortete HTTP ${res.status}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const body = await res.json();
|
|
348
|
+
const etag = res.headers.get("ETag") ?? res.headers.get("etag");
|
|
349
|
+
if (etag) {
|
|
350
|
+
this.storage.set(this.etagKey, etag);
|
|
351
|
+
this.storage.set(this.bodyKey, JSON.stringify(body));
|
|
352
|
+
}
|
|
353
|
+
return body;
|
|
354
|
+
}
|
|
355
|
+
/** Reads the cached manifest body from storage; null if absent. */
|
|
356
|
+
readCachedBody() {
|
|
357
|
+
const etag = this.storage.get(this.etagKey);
|
|
358
|
+
const bodyStr = this.storage.get(this.bodyKey);
|
|
359
|
+
if (!etag || !bodyStr) return null;
|
|
360
|
+
try {
|
|
361
|
+
return { etag, body: JSON.parse(bodyStr) };
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** Clears the cache — e.g. on logout or after `manifest reload`. */
|
|
367
|
+
clearCache() {
|
|
368
|
+
this.storage.remove(this.etagKey);
|
|
369
|
+
this.storage.remove(this.bodyKey);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/nav-builder.ts
|
|
374
|
+
var DEFAULT_STANDARD_PAGE_ROUTES = {
|
|
375
|
+
dashboard: "/admin/dashboard",
|
|
376
|
+
tenants: "/admin/tenants",
|
|
377
|
+
subscriptions: "/admin/subscriptions",
|
|
378
|
+
promoCodes: "/admin/promo-codes",
|
|
379
|
+
plans: "/admin/plans",
|
|
380
|
+
planVersions: "/admin/plan-versions",
|
|
381
|
+
audit: "/admin/audit",
|
|
382
|
+
users: "/admin/users",
|
|
383
|
+
pilots: "/admin/pilots",
|
|
384
|
+
discovery: "/admin/discovery",
|
|
385
|
+
bundles: "/admin/bundles",
|
|
386
|
+
businessTypes: "/admin/business-types",
|
|
387
|
+
marketingCatalog: "/admin/marketing-catalog",
|
|
388
|
+
platformEmail: "/admin/platform-email",
|
|
389
|
+
platformEmailHistory: "/admin/platform-email-history"
|
|
390
|
+
};
|
|
391
|
+
var DEFAULT_LABELS = {
|
|
392
|
+
dashboard: "Dashboard",
|
|
393
|
+
tenants: "Mandanten",
|
|
394
|
+
subscriptions: "Abonnements",
|
|
395
|
+
promoCodes: "Promo-Codes",
|
|
396
|
+
plans: "Pl\xE4ne & Versionen",
|
|
397
|
+
planVersions: "Plan-Versionen",
|
|
398
|
+
audit: "Audit-Log",
|
|
399
|
+
users: "Benutzer",
|
|
400
|
+
pilots: "Piloten",
|
|
401
|
+
discovery: "Discovery",
|
|
402
|
+
bundles: "Bundles",
|
|
403
|
+
businessTypes: "Gesch\xE4ftstypen",
|
|
404
|
+
marketingCatalog: "Marketing-Catalog",
|
|
405
|
+
platformEmail: "Plattform-E-Mail",
|
|
406
|
+
platformEmailHistory: "E-Mail-Verlauf"
|
|
407
|
+
};
|
|
408
|
+
var DEFAULT_ICONS = {
|
|
409
|
+
dashboard: "dashboard",
|
|
410
|
+
tenants: "business",
|
|
411
|
+
subscriptions: "card_membership",
|
|
412
|
+
promoCodes: "local_activity",
|
|
413
|
+
plans: "workspace_premium",
|
|
414
|
+
planVersions: "history_edu",
|
|
415
|
+
audit: "history",
|
|
416
|
+
users: "people",
|
|
417
|
+
pilots: "flight_takeoff",
|
|
418
|
+
discovery: "travel_explore",
|
|
419
|
+
bundles: "inventory_2",
|
|
420
|
+
businessTypes: "category",
|
|
421
|
+
marketingCatalog: "campaign",
|
|
422
|
+
platformEmail: "mail",
|
|
423
|
+
platformEmailHistory: "mark_email_read"
|
|
424
|
+
};
|
|
425
|
+
var DEFAULT_NAV_SECTIONS = {
|
|
426
|
+
dashboard: "\xDCbersicht",
|
|
427
|
+
discovery: "Produktkatalog",
|
|
428
|
+
businessTypes: "Produktkatalog",
|
|
429
|
+
bundles: "Produktkatalog",
|
|
430
|
+
plans: "Produktkatalog",
|
|
431
|
+
planVersions: "Produktkatalog",
|
|
432
|
+
marketingCatalog: "Produktkatalog",
|
|
433
|
+
promoCodes: "Produktkatalog",
|
|
434
|
+
tenants: "Kunden",
|
|
435
|
+
subscriptions: "Kunden",
|
|
436
|
+
users: "Kunden",
|
|
437
|
+
pilots: "Kunden",
|
|
438
|
+
audit: "System",
|
|
439
|
+
platformEmail: "System",
|
|
440
|
+
platformEmailHistory: "System"
|
|
441
|
+
};
|
|
442
|
+
var DEFAULT_SECTION_ORDER = [
|
|
443
|
+
"\xDCbersicht",
|
|
444
|
+
"Produktkatalog",
|
|
445
|
+
"Kunden",
|
|
446
|
+
"System"
|
|
447
|
+
];
|
|
448
|
+
function buildRoutes(manifest, options = {}) {
|
|
449
|
+
const routes = [];
|
|
450
|
+
const capabilities = manifest.capabilities ?? {};
|
|
451
|
+
const standard = manifest.navigation?.standardPages ?? {};
|
|
452
|
+
const overrideRoutes = { ...DEFAULT_STANDARD_PAGE_ROUTES, ...options.standardPageRoutes };
|
|
453
|
+
const overrideLabels = { ...DEFAULT_LABELS, ...options.standardPageLabels };
|
|
454
|
+
const overrideIcons = { ...DEFAULT_ICONS, ...options.standardPageIcons };
|
|
455
|
+
const overrideNavSection = { ...DEFAULT_NAV_SECTIONS, ...options.standardPageNavSection };
|
|
456
|
+
for (const key of Object.keys(standard)) {
|
|
457
|
+
const def = standard[key];
|
|
458
|
+
if (!def?.enabled) continue;
|
|
459
|
+
const cap = def.requiredCapability ?? null;
|
|
460
|
+
if (cap && capabilities[cap] !== true) continue;
|
|
461
|
+
routes.push({
|
|
462
|
+
id: key,
|
|
463
|
+
path: overrideRoutes[key],
|
|
464
|
+
label: overrideLabels[key],
|
|
465
|
+
icon: overrideIcons[key],
|
|
466
|
+
navSection: overrideNavSection[key],
|
|
467
|
+
componentKey: `platform-${key}`,
|
|
468
|
+
requiredCapability: cap,
|
|
469
|
+
isStandard: true
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const projectPages = manifest.navigation?.projectPages ?? [];
|
|
473
|
+
const exts = options.availableExtensions;
|
|
474
|
+
for (const p of projectPages) {
|
|
475
|
+
const cap = p.requiredCapability ?? null;
|
|
476
|
+
if (cap && capabilities[cap] !== true) continue;
|
|
477
|
+
if (exts && !exts.has(p.componentKey)) continue;
|
|
478
|
+
routes.push({
|
|
479
|
+
id: p.id,
|
|
480
|
+
path: p.route,
|
|
481
|
+
label: p.label,
|
|
482
|
+
icon: p.icon,
|
|
483
|
+
navSection: p.navSection,
|
|
484
|
+
componentKey: p.componentKey,
|
|
485
|
+
requiredCapability: cap,
|
|
486
|
+
isStandard: false,
|
|
487
|
+
prefetchOnIdle: p.prefetchOnIdle
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return routes;
|
|
491
|
+
}
|
|
492
|
+
function buildSidebar(routes, sectionOrder = DEFAULT_SECTION_ORDER) {
|
|
493
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const r of routes) {
|
|
495
|
+
const section = r.navSection ?? null;
|
|
496
|
+
const list = bySection.get(section) ?? [];
|
|
497
|
+
list.push({ id: r.id, path: r.path, label: r.label, icon: r.icon });
|
|
498
|
+
bySection.set(section, list);
|
|
499
|
+
}
|
|
500
|
+
const sections = [];
|
|
501
|
+
if (bySection.has(null)) {
|
|
502
|
+
sections.push({ section: null, items: bySection.get(null) });
|
|
503
|
+
}
|
|
504
|
+
const ordered = /* @__PURE__ */ new Set();
|
|
505
|
+
for (const name of sectionOrder) {
|
|
506
|
+
if (bySection.has(name)) {
|
|
507
|
+
sections.push({ section: name, items: bySection.get(name) });
|
|
508
|
+
ordered.add(name);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const remaining = [...bySection.entries()].filter(([s]) => s !== null && !ordered.has(s)).sort((a, b) => a[0].localeCompare(b[0]));
|
|
512
|
+
for (const [section, items] of remaining) {
|
|
513
|
+
sections.push({ section, items });
|
|
514
|
+
}
|
|
515
|
+
return sections;
|
|
516
|
+
}
|
|
517
|
+
function resolveExtension(componentKey, extensions) {
|
|
518
|
+
return extensions[componentKey] ?? null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/action-registry.ts
|
|
522
|
+
var MissingHandlerError = class extends Error {
|
|
523
|
+
constructor(actionKey) {
|
|
524
|
+
super(
|
|
525
|
+
`Kein Handler f\xFCr actionKey "${actionKey}" registriert. Konsument-Shell muss \`actions["${actionKey}"]\` bereitstellen.`
|
|
526
|
+
);
|
|
527
|
+
this.name = "MissingHandlerError";
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
var ActionDefNotInManifestError = class extends Error {
|
|
531
|
+
constructor(actionKey) {
|
|
532
|
+
super(
|
|
533
|
+
`actionKey "${actionKey}" ist im Manifest nicht deklariert. Bitte als TenantAction in einer ManifestContribution registrieren.`
|
|
534
|
+
);
|
|
535
|
+
this.name = "ActionDefNotInManifestError";
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var ActionRegistry = class {
|
|
539
|
+
defs = /* @__PURE__ */ new Map();
|
|
540
|
+
handlers = /* @__PURE__ */ new Map();
|
|
541
|
+
constructor(manifest, handlers = {}) {
|
|
542
|
+
for (const def of manifest.tenants?.actions ?? []) {
|
|
543
|
+
this.defs.set(def.actionKey, def);
|
|
544
|
+
}
|
|
545
|
+
for (const [key, handler] of Object.entries(handlers)) {
|
|
546
|
+
this.handlers.set(key, handler);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Registers a handler after the fact (e.g. when consumer code loads
|
|
551
|
+
* lazily). Throws `ActionDefNotInManifestError` if the `actionKey` is not
|
|
552
|
+
* declared in the manifest — prevents dead registrations.
|
|
553
|
+
*/
|
|
554
|
+
register(actionKey, handler) {
|
|
555
|
+
if (!this.defs.has(actionKey)) {
|
|
556
|
+
throw new ActionDefNotInManifestError(actionKey);
|
|
557
|
+
}
|
|
558
|
+
this.handlers.set(actionKey, handler);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Returns the `{def, handler}` pair. Throws if the key is missing from
|
|
562
|
+
* the manifest or no handler is registered. The shell's UI layer calls
|
|
563
|
+
* the method, checks `def.requiresMfa` / `def.confirmType` for the
|
|
564
|
+
* pre-flow, and then calls `handler(input)`.
|
|
565
|
+
*/
|
|
566
|
+
get(actionKey) {
|
|
567
|
+
const def = this.defs.get(actionKey);
|
|
568
|
+
if (!def) throw new ActionDefNotInManifestError(actionKey);
|
|
569
|
+
const handler = this.handlers.get(actionKey);
|
|
570
|
+
if (!handler) throw new MissingHandlerError(actionKey);
|
|
571
|
+
return { def, handler };
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Convenience: `get(key).handler(input)`. UI convenience for actions
|
|
575
|
+
* that need neither MFA nor confirm.
|
|
576
|
+
*/
|
|
577
|
+
async dispatch(actionKey, input) {
|
|
578
|
+
return this.get(actionKey).handler(input);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* List of actionKeys that are declared in the manifest but have no
|
|
582
|
+
* handler. Consumers use this in a doctor check to detect drift between
|
|
583
|
+
* the manifest and the shell build.
|
|
584
|
+
*/
|
|
585
|
+
listOrphanedDefs() {
|
|
586
|
+
return [...this.defs.keys()].filter((k) => !this.handlers.has(k));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* List of registered handlers that are missing from the manifest. Drift
|
|
590
|
+
* in the other direction.
|
|
591
|
+
*/
|
|
592
|
+
listOrphanedHandlers() {
|
|
593
|
+
return [...this.handlers.keys()].filter((k) => !this.defs.has(k));
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/batch-column-fetcher.ts
|
|
598
|
+
var BatchColumnDriftError = class extends Error {
|
|
599
|
+
constructor(column, reason) {
|
|
600
|
+
super(`Spalte "${column.key}": ${reason}`);
|
|
601
|
+
this.column = column;
|
|
602
|
+
this.name = "BatchColumnDriftError";
|
|
603
|
+
}
|
|
604
|
+
column;
|
|
605
|
+
};
|
|
606
|
+
var BatchColumnFetcher = class {
|
|
607
|
+
http;
|
|
608
|
+
paramStyle;
|
|
609
|
+
getAuthToken;
|
|
610
|
+
constructor(options = {}) {
|
|
611
|
+
this.http = options.http ?? defaultHttpClient();
|
|
612
|
+
this.paramStyle = options.paramStyle ?? "comma";
|
|
613
|
+
this.getAuthToken = options.getAuthToken;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Loads the data for all columns declared in the manifest in parallel
|
|
617
|
+
* (one request per column, all `tenantIds` in the batch). Columns without
|
|
618
|
+
* a satisfied capability are ignored.
|
|
619
|
+
*/
|
|
620
|
+
async fetchAll(manifest, tenantIds) {
|
|
621
|
+
if (tenantIds.length === 0) return {};
|
|
622
|
+
const cols = this.eligibleColumns(manifest);
|
|
623
|
+
const results = await Promise.all(
|
|
624
|
+
cols.map(
|
|
625
|
+
(col) => this.fetchOne(col, tenantIds).then((data) => [col.key, data])
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
const out = {};
|
|
629
|
+
for (const [key, data] of results) out[key] = data;
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
/** Fetch a single column (consumers may also use this directly). */
|
|
633
|
+
async fetchOne(column, tenantIds) {
|
|
634
|
+
this.validateBatchEndpoint(column);
|
|
635
|
+
if (tenantIds.length === 0) return {};
|
|
636
|
+
const url = this.buildUrl(column.endpoint, tenantIds);
|
|
637
|
+
const headers = {};
|
|
638
|
+
const token = this.getAuthToken?.();
|
|
639
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
640
|
+
const res = await this.http(url, { method: "GET", headers });
|
|
641
|
+
if (res.status !== 200) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`Spalte "${column.key}" \u2014 Endpoint ${column.endpoint} antwortete HTTP ${res.status}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const body = await res.json();
|
|
647
|
+
return body;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Column drift against the manifest. Consumers use this for CI smoke
|
|
651
|
+
* tests of the manifest-vs-shell build.
|
|
652
|
+
*/
|
|
653
|
+
listDriftIssues(manifest) {
|
|
654
|
+
const issues = [];
|
|
655
|
+
for (const col of manifest.tenants?.columns ?? []) {
|
|
656
|
+
try {
|
|
657
|
+
this.validateBatchEndpoint(col);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (err instanceof BatchColumnDriftError) issues.push(err);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return issues;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Which columns have a satisfied `requiredCapability`? Manifest =
|
|
666
|
+
* discovery, so a local check is enough.
|
|
667
|
+
*/
|
|
668
|
+
eligibleColumns(manifest) {
|
|
669
|
+
const caps = manifest.capabilities ?? {};
|
|
670
|
+
return (manifest.tenants?.columns ?? []).filter((col) => {
|
|
671
|
+
if (!col.requiredCapability) return true;
|
|
672
|
+
return caps[col.requiredCapability] === true;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
validateBatchEndpoint(col) {
|
|
676
|
+
if (!col.endpoint || col.endpoint.trim().length === 0) {
|
|
677
|
+
throw new BatchColumnDriftError(col, "endpoint ist leer");
|
|
678
|
+
}
|
|
679
|
+
if (col.endpoint.includes("{slug}") || col.endpoint.includes("{tenantId}")) {
|
|
680
|
+
throw new BatchColumnDriftError(
|
|
681
|
+
col,
|
|
682
|
+
"endpoint enth\xE4lt per-Tenant-Placeholder ({slug}/{tenantId}). Spalten-Endpoints m\xFCssen batchf\xE4hig sein \u2014 bitte Backend auf `?tenantIds=...`-Parameter umstellen."
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
buildUrl(endpoint, tenantIds) {
|
|
687
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
688
|
+
if (this.paramStyle === "comma") {
|
|
689
|
+
const enc = tenantIds.map(encodeURIComponent).join(",");
|
|
690
|
+
return `${endpoint}${sep}tenantIds=${enc}`;
|
|
691
|
+
}
|
|
692
|
+
const params = tenantIds.map((id) => `tenantIds=${encodeURIComponent(id)}`).join("&");
|
|
693
|
+
return `${endpoint}${sep}${params}`;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// src/use-api-list.ts
|
|
698
|
+
import { ref, watch } from "vue";
|
|
699
|
+
function useApiList(options) {
|
|
700
|
+
const http = options.http ?? defaultHttpClient();
|
|
701
|
+
const items = ref([]);
|
|
702
|
+
const page = ref(1);
|
|
703
|
+
const pageSize = ref(50);
|
|
704
|
+
const total = ref(0);
|
|
705
|
+
const loading = ref(false);
|
|
706
|
+
const error = ref(null);
|
|
707
|
+
function buildUrl() {
|
|
708
|
+
const params = new URLSearchParams();
|
|
709
|
+
params.set("page", String(page.value));
|
|
710
|
+
params.set("pageSize", String(pageSize.value));
|
|
711
|
+
const f = options.filter?.value ?? {};
|
|
712
|
+
for (const [k, v] of Object.entries(f)) {
|
|
713
|
+
if (v === void 0 || v === null || v === "") continue;
|
|
714
|
+
params.set(k, String(v));
|
|
715
|
+
}
|
|
716
|
+
const sep = options.endpoint.includes("?") ? "&" : "?";
|
|
717
|
+
return `${options.endpoint}${sep}${params.toString()}`;
|
|
718
|
+
}
|
|
719
|
+
async function load() {
|
|
720
|
+
loading.value = true;
|
|
721
|
+
error.value = null;
|
|
722
|
+
try {
|
|
723
|
+
const headers = {};
|
|
724
|
+
const token = options.getAuthToken?.();
|
|
725
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
726
|
+
const res = await http(buildUrl(), { method: "GET", headers });
|
|
727
|
+
if (res.status !== 200) {
|
|
728
|
+
throw new Error(`Endpoint ${options.endpoint} \u2192 HTTP ${res.status}`);
|
|
729
|
+
}
|
|
730
|
+
const raw = await res.json();
|
|
731
|
+
if (Array.isArray(raw)) {
|
|
732
|
+
items.value = raw;
|
|
733
|
+
total.value = raw.length;
|
|
734
|
+
} else if (raw !== null && typeof raw === "object") {
|
|
735
|
+
const body = raw;
|
|
736
|
+
items.value = body.items ?? [];
|
|
737
|
+
if (typeof body.page === "number") page.value = body.page;
|
|
738
|
+
if (typeof body.pageSize === "number") pageSize.value = body.pageSize;
|
|
739
|
+
if (typeof body.total === "number") total.value = body.total;
|
|
740
|
+
} else {
|
|
741
|
+
items.value = [];
|
|
742
|
+
total.value = 0;
|
|
743
|
+
}
|
|
744
|
+
} catch (err) {
|
|
745
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
746
|
+
items.value = [];
|
|
747
|
+
total.value = 0;
|
|
748
|
+
} finally {
|
|
749
|
+
loading.value = false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function goToPage(p) {
|
|
753
|
+
page.value = Math.max(1, Math.floor(p));
|
|
754
|
+
await load();
|
|
755
|
+
}
|
|
756
|
+
async function setPageSize(size) {
|
|
757
|
+
pageSize.value = Math.max(1, Math.floor(size));
|
|
758
|
+
page.value = 1;
|
|
759
|
+
await load();
|
|
760
|
+
}
|
|
761
|
+
if (options.filter) {
|
|
762
|
+
watch(
|
|
763
|
+
options.filter,
|
|
764
|
+
() => {
|
|
765
|
+
page.value = 1;
|
|
766
|
+
void load();
|
|
767
|
+
},
|
|
768
|
+
{ deep: true }
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (options.autoLoad !== false) {
|
|
772
|
+
Promise.resolve().then(() => void load());
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
items,
|
|
776
|
+
page,
|
|
777
|
+
pageSize,
|
|
778
|
+
total,
|
|
779
|
+
loading,
|
|
780
|
+
error,
|
|
781
|
+
reload: load,
|
|
782
|
+
goToPage,
|
|
783
|
+
setPageSize
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/use-tenants.ts
|
|
788
|
+
import { ref as ref2 } from "vue";
|
|
789
|
+
function useTenants(options) {
|
|
790
|
+
if (!options?.endpoint) {
|
|
791
|
+
throw new Error(
|
|
792
|
+
'useTenants: `endpoint` ist Pflicht (z. B. "/api/v1/admin/tenants" oder "/api/admin/tenants"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const filter = options.filter ?? ref2({});
|
|
796
|
+
const list = useApiList({
|
|
797
|
+
endpoint: options.endpoint,
|
|
798
|
+
filter,
|
|
799
|
+
http: options.http,
|
|
800
|
+
getAuthToken: options.getAuthToken,
|
|
801
|
+
autoLoad: options.autoLoad
|
|
802
|
+
});
|
|
803
|
+
return { ...list, filter };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/use-audit-entries.ts
|
|
807
|
+
import { ref as ref3 } from "vue";
|
|
808
|
+
function useAuditEntries(options) {
|
|
809
|
+
if (!options?.endpoint) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
'useAuditEntries: `endpoint` ist Pflicht (z. B. "/api/admin/audit" oder "/api/v1/admin/audit"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const filter = options.filter ?? ref3({});
|
|
815
|
+
const list = useApiList({
|
|
816
|
+
endpoint: options.endpoint,
|
|
817
|
+
filter,
|
|
818
|
+
http: options.http,
|
|
819
|
+
getAuthToken: options.getAuthToken,
|
|
820
|
+
autoLoad: options.autoLoad
|
|
821
|
+
});
|
|
822
|
+
return { ...list, filter };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/use-entitlement.ts
|
|
826
|
+
import { ref as ref4 } from "vue";
|
|
827
|
+
function useEntitlement(options) {
|
|
828
|
+
if (!options?.endpoint) {
|
|
829
|
+
throw new Error(
|
|
830
|
+
'useEntitlement: `endpoint` ist Pflicht (z. B. "/api/billing/entitlement" oder "/api/v1/billing/entitlement"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
const endpoint = options.endpoint;
|
|
834
|
+
const http = options.http ?? defaultHttpClient();
|
|
835
|
+
const entitlement = ref4(null);
|
|
836
|
+
const loading = ref4(false);
|
|
837
|
+
const error = ref4(null);
|
|
838
|
+
async function load() {
|
|
839
|
+
loading.value = true;
|
|
840
|
+
error.value = null;
|
|
841
|
+
try {
|
|
842
|
+
const headers = {};
|
|
843
|
+
const token = options.getAuthToken?.();
|
|
844
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
845
|
+
const res = await http(endpoint, { method: "GET", headers });
|
|
846
|
+
if (res.status !== 200) {
|
|
847
|
+
throw new Error(`Entitlement-Endpoint \u2192 HTTP ${res.status}`);
|
|
848
|
+
}
|
|
849
|
+
entitlement.value = await res.json();
|
|
850
|
+
} catch (err) {
|
|
851
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
852
|
+
entitlement.value = null;
|
|
853
|
+
} finally {
|
|
854
|
+
loading.value = false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function hasFeature(key) {
|
|
858
|
+
return entitlement.value?.features.includes(key) ?? false;
|
|
859
|
+
}
|
|
860
|
+
if (options.autoLoad !== false) {
|
|
861
|
+
Promise.resolve().then(() => void load());
|
|
862
|
+
}
|
|
863
|
+
return { entitlement, loading, error, load, hasFeature };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/use-tenant-manifest.ts
|
|
867
|
+
import { ref as ref5 } from "vue";
|
|
868
|
+
function useTenantManifest(options) {
|
|
869
|
+
if (!options?.endpoint) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
'useTenantManifest: `endpoint` ist Pflicht (z. B. "/api/tenant/manifest").'
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const http = options.http ?? defaultHttpClient();
|
|
875
|
+
const manifest = ref5(null);
|
|
876
|
+
const loading = ref5(false);
|
|
877
|
+
const error = ref5(null);
|
|
878
|
+
async function load() {
|
|
879
|
+
loading.value = true;
|
|
880
|
+
error.value = null;
|
|
881
|
+
try {
|
|
882
|
+
const headers = {};
|
|
883
|
+
const token = options.getAuthToken?.();
|
|
884
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
885
|
+
const res = await http(options.endpoint, { method: "GET", headers });
|
|
886
|
+
if (res.status !== 200) {
|
|
887
|
+
throw new Error(`Tenant-Manifest-Endpoint \u2192 HTTP ${res.status}`);
|
|
888
|
+
}
|
|
889
|
+
manifest.value = await res.json();
|
|
890
|
+
} catch (err) {
|
|
891
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
892
|
+
manifest.value = null;
|
|
893
|
+
} finally {
|
|
894
|
+
loading.value = false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function hasFeature(key) {
|
|
898
|
+
return manifest.value?.features.includes(key) ?? false;
|
|
899
|
+
}
|
|
900
|
+
function quotaLimit(key) {
|
|
901
|
+
const v = manifest.value?.quotas[key];
|
|
902
|
+
return v === void 0 ? null : v;
|
|
903
|
+
}
|
|
904
|
+
if (options.autoLoad !== false) {
|
|
905
|
+
Promise.resolve().then(() => void load());
|
|
906
|
+
}
|
|
907
|
+
return { manifest, loading, error, load, hasFeature, quotaLimit };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/entitlement-provider.ts
|
|
911
|
+
import { inject as inject2 } from "vue";
|
|
912
|
+
var ENTITLEMENT_INJECTION_KEY = /* @__PURE__ */ Symbol.for(
|
|
913
|
+
"@saasicat/ui-vue/ENTITLEMENT"
|
|
914
|
+
);
|
|
915
|
+
function provideEntitlement(app, entitlement) {
|
|
916
|
+
app.provide(ENTITLEMENT_INJECTION_KEY, entitlement);
|
|
917
|
+
}
|
|
918
|
+
function useInjectedEntitlement() {
|
|
919
|
+
return inject2(ENTITLEMENT_INJECTION_KEY, null);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/feature-router-guard.ts
|
|
923
|
+
function extractRequired(to) {
|
|
924
|
+
const raw = to.meta?.requiresFeature;
|
|
925
|
+
if (!raw) return [];
|
|
926
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
927
|
+
}
|
|
928
|
+
function buildFeatureRouterGuard(options) {
|
|
929
|
+
return function featureGuard(to, _from, next) {
|
|
930
|
+
const required = extractRequired(to);
|
|
931
|
+
if (required.length === 0) return next();
|
|
932
|
+
const ent = options.getEntitlement();
|
|
933
|
+
if (!ent) {
|
|
934
|
+
return next();
|
|
935
|
+
}
|
|
936
|
+
const allowLoading = options.allowWhileLoading !== false;
|
|
937
|
+
if (allowLoading && ent.loading.value && ent.entitlement.value === null) {
|
|
938
|
+
return next();
|
|
939
|
+
}
|
|
940
|
+
const allowed = required.some((f) => ent.hasFeature(f));
|
|
941
|
+
if (allowed) return next();
|
|
942
|
+
if (options.redirectTo) return next(options.redirectTo);
|
|
943
|
+
return next(false);
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/use-tenant-billing-catalog.ts
|
|
948
|
+
import { ref as ref6 } from "vue";
|
|
949
|
+
function useTenantBillingCatalog(options = {}) {
|
|
950
|
+
const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
|
|
951
|
+
const http = options.http ?? defaultHttpClient();
|
|
952
|
+
const plans = ref6(null);
|
|
953
|
+
const featureRegistry = ref6(null);
|
|
954
|
+
const bundles = ref6(null);
|
|
955
|
+
const loading = ref6(false);
|
|
956
|
+
const error = ref6(null);
|
|
957
|
+
async function fetchJson(path) {
|
|
958
|
+
const res = await http(`${apiPrefix}${path}`, { method: "GET" });
|
|
959
|
+
if (res.status !== 200) {
|
|
960
|
+
throw new Error(`${apiPrefix}${path} \u2192 HTTP ${res.status}`);
|
|
961
|
+
}
|
|
962
|
+
return await res.json();
|
|
963
|
+
}
|
|
964
|
+
async function load() {
|
|
965
|
+
loading.value = true;
|
|
966
|
+
error.value = null;
|
|
967
|
+
try {
|
|
968
|
+
const [plansResult, registryResult] = await Promise.all([
|
|
969
|
+
fetchJson("/plans"),
|
|
970
|
+
fetchJson("/feature-registry")
|
|
971
|
+
]);
|
|
972
|
+
plans.value = plansResult;
|
|
973
|
+
featureRegistry.value = registryResult;
|
|
974
|
+
} catch (err) {
|
|
975
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
976
|
+
plans.value = null;
|
|
977
|
+
featureRegistry.value = null;
|
|
978
|
+
} finally {
|
|
979
|
+
loading.value = false;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const raw = await fetchJson("/bundles");
|
|
983
|
+
bundles.value = (raw ?? []).map((b) => ({
|
|
984
|
+
bundleVersionId: b.bundleVersionId,
|
|
985
|
+
bundleKey: b.bundleKey,
|
|
986
|
+
label: b.label,
|
|
987
|
+
description: b.description,
|
|
988
|
+
features: b.features ?? [],
|
|
989
|
+
quotas: b.quotas ?? {},
|
|
990
|
+
monthlyNet: b.monthlyNet !== null ? parseFloat(b.monthlyNet) : null,
|
|
991
|
+
yearlyNet: b.yearlyNet !== null ? parseFloat(b.yearlyNet) : null,
|
|
992
|
+
requiresFeatures: b.requiresFeatures ?? [],
|
|
993
|
+
priceTag: b.marketing?.priceTag ?? null
|
|
994
|
+
}));
|
|
995
|
+
} catch {
|
|
996
|
+
bundles.value = [];
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (options.autoLoad !== false) {
|
|
1000
|
+
Promise.resolve().then(() => void load());
|
|
1001
|
+
}
|
|
1002
|
+
return { plans, featureRegistry, bundles, loading, error, load };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/use-tenant-billing.ts
|
|
1006
|
+
import { ref as ref7 } from "vue";
|
|
1007
|
+
function useTenantBilling(options = {}) {
|
|
1008
|
+
const apiPrefix = trimTrailingSlashes(options.apiPrefix ?? "/billing");
|
|
1009
|
+
const http = options.http ?? defaultHttpClient();
|
|
1010
|
+
const usage = ref7(null);
|
|
1011
|
+
const loading = ref7(false);
|
|
1012
|
+
const error = ref7(null);
|
|
1013
|
+
const subscriptionBundles = ref7([]);
|
|
1014
|
+
function authHeaders() {
|
|
1015
|
+
const headers = {};
|
|
1016
|
+
const token = options.getAuthToken?.();
|
|
1017
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1018
|
+
return headers;
|
|
1019
|
+
}
|
|
1020
|
+
async function fetchOrThrow(path, init) {
|
|
1021
|
+
const res = await http(`${apiPrefix}${path}`, {
|
|
1022
|
+
method: init?.method ?? "GET",
|
|
1023
|
+
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
1024
|
+
body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
1025
|
+
});
|
|
1026
|
+
if (res.status >= 400) {
|
|
1027
|
+
let body;
|
|
1028
|
+
try {
|
|
1029
|
+
body = await res.json();
|
|
1030
|
+
} catch {
|
|
1031
|
+
body = await res.text();
|
|
1032
|
+
}
|
|
1033
|
+
const err = new Error(
|
|
1034
|
+
`${apiPrefix}${path} \u2192 HTTP ${res.status}: ${typeof body === "string" ? body : JSON.stringify(body)}`
|
|
1035
|
+
);
|
|
1036
|
+
err.status = res.status;
|
|
1037
|
+
err.body = body;
|
|
1038
|
+
throw err;
|
|
1039
|
+
}
|
|
1040
|
+
if (res.status === 204) return void 0;
|
|
1041
|
+
return await res.json();
|
|
1042
|
+
}
|
|
1043
|
+
async function reload() {
|
|
1044
|
+
loading.value = true;
|
|
1045
|
+
error.value = null;
|
|
1046
|
+
try {
|
|
1047
|
+
usage.value = await fetchOrThrow("/usage");
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1050
|
+
usage.value = null;
|
|
1051
|
+
} finally {
|
|
1052
|
+
loading.value = false;
|
|
1053
|
+
}
|
|
1054
|
+
await loadBundles();
|
|
1055
|
+
}
|
|
1056
|
+
async function loadBundles() {
|
|
1057
|
+
try {
|
|
1058
|
+
subscriptionBundles.value = await fetchOrThrow("/subscription-bundles") ?? [];
|
|
1059
|
+
} catch {
|
|
1060
|
+
subscriptionBundles.value = [];
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async function addBundle(bundleVersionId, minimumTermMonths) {
|
|
1064
|
+
await fetchOrThrow("/subscription-bundles", {
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
body: minimumTermMonths !== void 0 ? { bundleVersionId, minimumTermMonths } : { bundleVersionId }
|
|
1067
|
+
});
|
|
1068
|
+
await loadBundles();
|
|
1069
|
+
}
|
|
1070
|
+
async function cancelBundle(subscriptionBundleId) {
|
|
1071
|
+
await fetchOrThrow(`/subscription-bundles/${subscriptionBundleId}`, {
|
|
1072
|
+
method: "DELETE",
|
|
1073
|
+
body: {}
|
|
1074
|
+
});
|
|
1075
|
+
await loadBundles();
|
|
1076
|
+
}
|
|
1077
|
+
async function reactivateBundle(subscriptionBundleId) {
|
|
1078
|
+
await fetchOrThrow(`/subscription-bundles/${subscriptionBundleId}/reactivate`, {
|
|
1079
|
+
method: "POST",
|
|
1080
|
+
body: {}
|
|
1081
|
+
});
|
|
1082
|
+
await loadBundles();
|
|
1083
|
+
}
|
|
1084
|
+
async function previewAddBundle(bundleVersionId, minimumTermMonths) {
|
|
1085
|
+
return fetchOrThrow("/subscription-bundles/preview", {
|
|
1086
|
+
method: "POST",
|
|
1087
|
+
body: minimumTermMonths !== void 0 ? { bundleVersionId, minimumTermMonths } : { bundleVersionId }
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
async function previewCancelBundle(subscriptionBundleId) {
|
|
1091
|
+
return fetchOrThrow("/subscription-bundles/preview", {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
body: { subscriptionBundleId }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async function previewPlanChange(plan, billingCycle) {
|
|
1097
|
+
return fetchOrThrow("/plan/preview", {
|
|
1098
|
+
method: "POST",
|
|
1099
|
+
body: { plan, billingCycle }
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
async function changePlan(plan, billingCycle, effectiveImmediately) {
|
|
1103
|
+
await fetchOrThrow("/plan", {
|
|
1104
|
+
method: "POST",
|
|
1105
|
+
body: { plan, billingCycle, effectiveImmediately }
|
|
1106
|
+
});
|
|
1107
|
+
await reload();
|
|
1108
|
+
}
|
|
1109
|
+
async function acceptPendingPlanVersion() {
|
|
1110
|
+
await fetchOrThrow("/subscription/accept-pending-version", { method: "POST" });
|
|
1111
|
+
await reload();
|
|
1112
|
+
}
|
|
1113
|
+
async function cancelSubscription(immediately) {
|
|
1114
|
+
await fetchOrThrow("/cancel", {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
body: { immediately }
|
|
1117
|
+
});
|
|
1118
|
+
await reload();
|
|
1119
|
+
}
|
|
1120
|
+
function hasFeature(key) {
|
|
1121
|
+
return usage.value?.limits.features.includes(key) ?? false;
|
|
1122
|
+
}
|
|
1123
|
+
if (options.autoLoad !== false) {
|
|
1124
|
+
Promise.resolve().then(() => void reload());
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
usage,
|
|
1128
|
+
loading,
|
|
1129
|
+
error,
|
|
1130
|
+
reload,
|
|
1131
|
+
previewPlanChange,
|
|
1132
|
+
changePlan,
|
|
1133
|
+
acceptPendingPlanVersion,
|
|
1134
|
+
cancelSubscription,
|
|
1135
|
+
hasFeature,
|
|
1136
|
+
subscriptionBundles,
|
|
1137
|
+
loadBundles,
|
|
1138
|
+
addBundle,
|
|
1139
|
+
cancelBundle,
|
|
1140
|
+
reactivateBundle,
|
|
1141
|
+
previewAddBundle,
|
|
1142
|
+
previewCancelBundle
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/use-subscription-draft.ts
|
|
1147
|
+
import { computed as computed2, ref as ref8 } from "vue";
|
|
1148
|
+
import {
|
|
1149
|
+
selectChargeableBundles
|
|
1150
|
+
} from "@saasicat/types";
|
|
1151
|
+
var DEFAULT_YEARLY_FACTOR = 10;
|
|
1152
|
+
function unwrap(source) {
|
|
1153
|
+
if (source && typeof source === "object" && "value" in source) {
|
|
1154
|
+
return source.value;
|
|
1155
|
+
}
|
|
1156
|
+
return source;
|
|
1157
|
+
}
|
|
1158
|
+
function priceForCycle(monthlyNet, yearlyNet, cycle, yearlyFactor) {
|
|
1159
|
+
if (cycle === "YEARLY") {
|
|
1160
|
+
return yearlyNet != null ? yearlyNet : monthlyNet * yearlyFactor;
|
|
1161
|
+
}
|
|
1162
|
+
return monthlyNet;
|
|
1163
|
+
}
|
|
1164
|
+
function useSubscriptionDraft(options) {
|
|
1165
|
+
const yearlyFactor = options.yearlyFactor ?? DEFAULT_YEARLY_FACTOR;
|
|
1166
|
+
const plansRef = computed2(() => unwrap(options.plans) ?? []);
|
|
1167
|
+
const subscriptionBundlesRef = computed2(
|
|
1168
|
+
() => unwrap(options.subscriptionBundles ?? null) ?? []
|
|
1169
|
+
);
|
|
1170
|
+
const plan = ref8(options.initialPlan ?? null);
|
|
1171
|
+
const cycle = ref8(options.initialCycle ?? "MONTHLY");
|
|
1172
|
+
const selectedBundleVersionIds = ref8(
|
|
1173
|
+
new Set(options.initialBundleVersionIds ?? [])
|
|
1174
|
+
);
|
|
1175
|
+
const promoCode = ref8("");
|
|
1176
|
+
const promoState = ref8({ status: "idle", preview: null, message: "" });
|
|
1177
|
+
const selectedPlan = computed2(() => {
|
|
1178
|
+
if (!plan.value) return null;
|
|
1179
|
+
return plansRef.value.find((p) => p.id === plan.value) ?? null;
|
|
1180
|
+
});
|
|
1181
|
+
const selectedBundles = computed2(
|
|
1182
|
+
() => subscriptionBundlesRef.value.filter(
|
|
1183
|
+
(b) => selectedBundleVersionIds.value.has(b.bundleVersionId)
|
|
1184
|
+
)
|
|
1185
|
+
);
|
|
1186
|
+
const chargeableBundles = computed2(() => {
|
|
1187
|
+
const planFeatures = selectedPlan.value?.features ?? [];
|
|
1188
|
+
const selected = selectedBundles.value;
|
|
1189
|
+
const keptIds = new Set(
|
|
1190
|
+
selectChargeableBundles(planFeatures, selected).map((b) => b.bundleVersionId)
|
|
1191
|
+
);
|
|
1192
|
+
return selected.filter((b) => keptIds.has(b.bundleVersionId));
|
|
1193
|
+
});
|
|
1194
|
+
const activeFeatures = computed2(() => {
|
|
1195
|
+
const result = /* @__PURE__ */ new Set();
|
|
1196
|
+
const planObj = selectedPlan.value;
|
|
1197
|
+
if (planObj) {
|
|
1198
|
+
for (const f of planObj.features) result.add(f);
|
|
1199
|
+
}
|
|
1200
|
+
for (const bundle of selectedBundles.value) {
|
|
1201
|
+
for (const f of bundle.features) result.add(f);
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
});
|
|
1205
|
+
const pricing = computed2(() => {
|
|
1206
|
+
const cyc = cycle.value;
|
|
1207
|
+
const planObj = selectedPlan.value;
|
|
1208
|
+
const planNet = planObj ? priceForCycle(planObj.monthlyNet ?? 0, planObj.yearlyNet, cyc, yearlyFactor) : 0;
|
|
1209
|
+
const bundleItems = [];
|
|
1210
|
+
let bundlesNet = 0;
|
|
1211
|
+
for (const b of chargeableBundles.value) {
|
|
1212
|
+
const net = priceForCycle(b.monthlyNet ?? 0, b.yearlyNet, cyc, yearlyFactor);
|
|
1213
|
+
bundlesNet += net;
|
|
1214
|
+
bundleItems.push({
|
|
1215
|
+
key: `subscription-bundle:${b.bundleVersionId}`,
|
|
1216
|
+
label: b.label,
|
|
1217
|
+
net
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
const subtotalNet = planNet + bundlesNet;
|
|
1221
|
+
let discountNet = 0;
|
|
1222
|
+
const preview = promoState.value.preview;
|
|
1223
|
+
if (preview && preview.valid && promoState.value.status === "valid") {
|
|
1224
|
+
const valid = preview;
|
|
1225
|
+
const value = Number(valid.discount.value);
|
|
1226
|
+
if (Number.isFinite(value) && value > 0) {
|
|
1227
|
+
if (valid.discount.valueType === "PERCENT") {
|
|
1228
|
+
discountNet = subtotalNet * value / 100;
|
|
1229
|
+
} else {
|
|
1230
|
+
discountNet = Math.min(value, subtotalNet);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const totalNet = Math.max(0, subtotalNet - discountNet);
|
|
1235
|
+
let yearSavings = 0;
|
|
1236
|
+
if (planObj) {
|
|
1237
|
+
const monthlyTotal = priceForCycle(planObj.monthlyNet ?? 0, planObj.yearlyNet, "MONTHLY", yearlyFactor) * 12;
|
|
1238
|
+
const yearlyTotal = priceForCycle(
|
|
1239
|
+
planObj.monthlyNet ?? 0,
|
|
1240
|
+
planObj.yearlyNet,
|
|
1241
|
+
"YEARLY",
|
|
1242
|
+
yearlyFactor
|
|
1243
|
+
);
|
|
1244
|
+
yearSavings = Math.max(0, monthlyTotal - yearlyTotal);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
cycle: cyc,
|
|
1248
|
+
planNet,
|
|
1249
|
+
bundlesNet,
|
|
1250
|
+
subtotalNet,
|
|
1251
|
+
discountNet,
|
|
1252
|
+
totalNet,
|
|
1253
|
+
yearSavings,
|
|
1254
|
+
breakdown: {
|
|
1255
|
+
plan: planObj ? {
|
|
1256
|
+
key: `plan:${planObj.id}`,
|
|
1257
|
+
label: planObj.name,
|
|
1258
|
+
net: planNet,
|
|
1259
|
+
sublabel: `${planObj.features.length} Basis-Module`
|
|
1260
|
+
} : null,
|
|
1261
|
+
bundles: bundleItems
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
const isDirty = computed2(() => {
|
|
1266
|
+
return selectedBundleVersionIds.value.size > 0 || promoState.value.status === "valid";
|
|
1267
|
+
});
|
|
1268
|
+
function setPlan(planId) {
|
|
1269
|
+
if (plan.value === planId) return;
|
|
1270
|
+
plan.value = planId;
|
|
1271
|
+
const compatibleBundleVersions = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (const bundle of subscriptionBundlesRef.value) {
|
|
1273
|
+
if (bundle.compatiblePlanKeys.length === 0 || bundle.compatiblePlanKeys.includes(planId)) {
|
|
1274
|
+
compatibleBundleVersions.add(bundle.bundleVersionId);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
selectedBundleVersionIds.value = new Set(
|
|
1278
|
+
[...selectedBundleVersionIds.value].filter((id) => compatibleBundleVersions.has(id))
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
function setCycle(c) {
|
|
1282
|
+
cycle.value = c;
|
|
1283
|
+
}
|
|
1284
|
+
function toggleSubscriptionBundle(bundleVersionId) {
|
|
1285
|
+
const next = new Set(selectedBundleVersionIds.value);
|
|
1286
|
+
if (next.has(bundleVersionId)) next.delete(bundleVersionId);
|
|
1287
|
+
else next.add(bundleVersionId);
|
|
1288
|
+
selectedBundleVersionIds.value = next;
|
|
1289
|
+
}
|
|
1290
|
+
function setPromoCode(code) {
|
|
1291
|
+
promoCode.value = code.toUpperCase();
|
|
1292
|
+
if (promoState.value.status !== "idle") {
|
|
1293
|
+
promoState.value = { status: "idle", preview: null, message: "" };
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function setPromoState(state) {
|
|
1297
|
+
promoState.value = state;
|
|
1298
|
+
}
|
|
1299
|
+
function clearPromo() {
|
|
1300
|
+
promoCode.value = "";
|
|
1301
|
+
promoState.value = { status: "idle", preview: null, message: "" };
|
|
1302
|
+
}
|
|
1303
|
+
function toApiPayload() {
|
|
1304
|
+
if (!plan.value) {
|
|
1305
|
+
throw new Error("useSubscriptionDraft: plan ist nicht gesetzt");
|
|
1306
|
+
}
|
|
1307
|
+
const payload = {
|
|
1308
|
+
plan: plan.value,
|
|
1309
|
+
billingCycle: cycle.value
|
|
1310
|
+
};
|
|
1311
|
+
if (promoState.value.status === "valid" && promoCode.value) {
|
|
1312
|
+
payload.promoCode = promoCode.value;
|
|
1313
|
+
}
|
|
1314
|
+
const chargeableIds = chargeableBundles.value.map((b) => b.bundleVersionId);
|
|
1315
|
+
if (chargeableIds.length > 0) {
|
|
1316
|
+
payload.bundleVersionIds = chargeableIds;
|
|
1317
|
+
}
|
|
1318
|
+
return payload;
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
plan,
|
|
1322
|
+
cycle,
|
|
1323
|
+
selectedBundleVersionIds,
|
|
1324
|
+
promoCode,
|
|
1325
|
+
promoState,
|
|
1326
|
+
selectedPlan,
|
|
1327
|
+
activeFeatures,
|
|
1328
|
+
pricing,
|
|
1329
|
+
isDirty,
|
|
1330
|
+
setPlan,
|
|
1331
|
+
setCycle,
|
|
1332
|
+
toggleSubscriptionBundle,
|
|
1333
|
+
setPromoCode,
|
|
1334
|
+
setPromoState,
|
|
1335
|
+
clearPromo,
|
|
1336
|
+
toApiPayload
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/pages-tenant/default-i18n.ts
|
|
1341
|
+
var DEFAULT_I18N_DE = {
|
|
1342
|
+
sectionTitle: "Paket & Verbrauch",
|
|
1343
|
+
sectionSubtitle: "Aktuelles Paket, Verbrauch und Bundles.",
|
|
1344
|
+
loading: "Lade \u2026",
|
|
1345
|
+
noSubscription: "Keine Subscription f\xFCr diesen Mandanten gefunden.",
|
|
1346
|
+
activePlan: "Aktuelles Paket",
|
|
1347
|
+
cycleMonthly: "Monatlich",
|
|
1348
|
+
cycleYearly: "J\xE4hrlich",
|
|
1349
|
+
statusActive: "Aktiv",
|
|
1350
|
+
statusTrial: "Testphase",
|
|
1351
|
+
statusPastDue: "Zahlung \xFCberf\xE4llig",
|
|
1352
|
+
statusCanceled: "Gek\xFCndigt",
|
|
1353
|
+
statusPendingSales: "Vertrieb-Kl\xE4rung",
|
|
1354
|
+
trialEndsAt: "Testphase endet am",
|
|
1355
|
+
pilotEndsAt: "Pilot endet am",
|
|
1356
|
+
nextBillingDate: "N\xE4chste Abrechnung",
|
|
1357
|
+
pendingChange: "Ausstehender Plan-Wechsel",
|
|
1358
|
+
changeFromTo: "Wechsel zu",
|
|
1359
|
+
changeEffectiveAt: "wirksam ab",
|
|
1360
|
+
changePlanButton: "Paket wechseln",
|
|
1361
|
+
cancelSubscriptionButton: "Abonnement k\xFCndigen",
|
|
1362
|
+
usageTitle: "Verbrauch",
|
|
1363
|
+
featuresOverviewTitle: "Leistungsumfang",
|
|
1364
|
+
featuresActive: "Enthalten",
|
|
1365
|
+
featuresLocked: "Nicht enthalten",
|
|
1366
|
+
bundlesStoreTitle: "Bundles",
|
|
1367
|
+
bundlesBookedTitle: "Gebuchte Bundles",
|
|
1368
|
+
bundlesAvailableTitle: "Verf\xFCgbare Bundles",
|
|
1369
|
+
bundlesAvailableEmpty: "Aktuell sind keine zus\xE4tzlichen Bundles verf\xFCgbar.",
|
|
1370
|
+
bundlesPerMonth: "netto/Monat",
|
|
1371
|
+
bundleBookAction: "Bundle buchen",
|
|
1372
|
+
bundleBookInProgress: "Buchung l\xE4uft \u2026",
|
|
1373
|
+
bundleCancelAction: "K\xFCndigen",
|
|
1374
|
+
bundleReactivateAction: "Reaktivieren",
|
|
1375
|
+
bundleReactivateConfirmTitle: "Bundle reaktivieren",
|
|
1376
|
+
bundleReactivateConfirmBody: "Die K\xFCndigung wird r\xFCckg\xE4ngig gemacht. Das Bundle bleibt gebucht, l\xE4uft regul\xE4r weiter und wird wieder abgerechnet.",
|
|
1377
|
+
bundleCanceledAt: "Gek\xFCndigt zum",
|
|
1378
|
+
bundleMinimumTermUntil: "Mindestlaufzeit bis",
|
|
1379
|
+
bundleIncludesLabel: "Enth\xE4lt",
|
|
1380
|
+
bundleAlreadyBooked: "Bereits gebucht",
|
|
1381
|
+
bundleIncompatible: "Nicht kompatibel mit aktuellem Paket",
|
|
1382
|
+
bundleMissingRequires: "Ben\xF6tigt",
|
|
1383
|
+
bundlePreviewAddTitle: "Bundle buchen",
|
|
1384
|
+
bundlePreviewCancelTitle: "Bundle k\xFCndigen",
|
|
1385
|
+
bundlePreviewLoading: "Vorschau wird berechnet \u2026",
|
|
1386
|
+
bundlePreviewProrationTitle: "Anteilige Abrechnung",
|
|
1387
|
+
bundlePreviewProratedNow: "Heute anteilig f\xE4llig",
|
|
1388
|
+
bundlePreviewProrationDays: "Tage",
|
|
1389
|
+
bundlePreviewNextPeriod: "Regul\xE4r ab n\xE4chster Periode",
|
|
1390
|
+
bundlePreviewTrialNote: "W\xE4hrend der Testphase f\xE4llt nichts an \u2014 die Abrechnung beginnt mit der ersten bezahlten Periode.",
|
|
1391
|
+
bundlePreviewNoPrice: "F\xFCr den aktuellen Abrechnungszyklus ist kein Listenpreis gepflegt.",
|
|
1392
|
+
bundlePreviewMinimumTermLabel: "Mindestlaufzeit",
|
|
1393
|
+
bundlePreviewMinimumTermMonths: "Monate, bis",
|
|
1394
|
+
bundlePreviewMinimumTermNone: "Keine Mindestlaufzeit",
|
|
1395
|
+
bundlePreviewRedundantTitle: "Bereits enthaltene Features (w\xFCrden doppelt bezahlt)",
|
|
1396
|
+
bundlePreviewRedundantCoveredByPlan: "bereits im Plan",
|
|
1397
|
+
bundlePreviewRedundantCoveredByBundle: "bereits im Bundle",
|
|
1398
|
+
bundlePreviewMissingRequiresTitle: "Fehlende Voraussetzungen",
|
|
1399
|
+
bundlePreviewBlockersTitle: "Buchung nicht m\xF6glich",
|
|
1400
|
+
bundlePreviewWarningsTitle: "Hinweise",
|
|
1401
|
+
bundlePreviewEffectiveAt: "K\xFCndigung wirksam zum",
|
|
1402
|
+
bundlePreviewSavings: "Ersparnis pro Periode ab Wirksamkeit",
|
|
1403
|
+
bundlePreviewConfirmAdd: "Kostenpflichtig buchen",
|
|
1404
|
+
bundlePreviewConfirmCancel: "K\xFCndigung best\xE4tigen",
|
|
1405
|
+
bundlePreviewInProgress: "Wird ausgef\xFChrt \u2026",
|
|
1406
|
+
bundlePreviewClose: "Abbrechen",
|
|
1407
|
+
pendingVersionTitle: "Anstehende Plan-\xC4nderung",
|
|
1408
|
+
pendingVersionChipNonRegressive: "Verbessernd",
|
|
1409
|
+
pendingVersionChipRegressive: "Best\xE4tigung erforderlich",
|
|
1410
|
+
pendingVersionEffectiveAt: "Wirksam ab",
|
|
1411
|
+
pendingVersionAcceptAction: "\xC4nderungen akzeptieren",
|
|
1412
|
+
pendingVersionAcceptInProgress: "Akzeptiere \u2026",
|
|
1413
|
+
pendingVersionAcceptedAt: "Akzeptiert am",
|
|
1414
|
+
wizardTitle: "Paket wechseln",
|
|
1415
|
+
wizardClose: "Schlie\xDFen",
|
|
1416
|
+
wizardCurrent: "Aktuell",
|
|
1417
|
+
wizardBadgeCurrent: "aktiv",
|
|
1418
|
+
wizardBadgePopular: "beliebt",
|
|
1419
|
+
wizardPriceUnitMonthly: "netto/Monat",
|
|
1420
|
+
wizardPriceUnitYearly: "netto/Jahr",
|
|
1421
|
+
wizardPriceOnRequest: "auf Anfrage",
|
|
1422
|
+
wizardStepChoose: "Paket w\xE4hlen",
|
|
1423
|
+
wizardStepChooseIntro: "W\xE4hle dein Ziel-Paket und den Abrechnungszyklus. Im n\xE4chsten Schritt zeigen wir Verbrauchs-Check und Feature-Diff.",
|
|
1424
|
+
wizardStepPreview: "Vorschau",
|
|
1425
|
+
wizardStepConfirm: "Best\xE4tigen",
|
|
1426
|
+
wizardNext: "Weiter",
|
|
1427
|
+
wizardBack: "Zur\xFCck",
|
|
1428
|
+
wizardPreviewLoading: "Vorschau wird berechnet \u2026",
|
|
1429
|
+
wizardEffectiveAtLabel: "Wirksam ab",
|
|
1430
|
+
wizardEffectiveImmediate: "Wirksam sofort",
|
|
1431
|
+
wizardProrationTitle: "Anteilige Abrechnung",
|
|
1432
|
+
wizardProrationLine: "Mehrbetrag bis Periodenende:",
|
|
1433
|
+
wizardProrationDays: "Tage",
|
|
1434
|
+
wizardLimitsTitle: "Limit-Vergleich",
|
|
1435
|
+
wizardLimitsUsed: "Verbrauch",
|
|
1436
|
+
wizardLimitsCurrent: "Aktuell",
|
|
1437
|
+
wizardLimitsTarget: "Ziel",
|
|
1438
|
+
wizardFeaturesGained: "Neu freigeschaltet",
|
|
1439
|
+
wizardFeaturesLost: "Wegfallende Features",
|
|
1440
|
+
wizardBlockersTitle: "Verhinderungs-Gr\xFCnde",
|
|
1441
|
+
wizardConfirmImmediate: "Der Wechsel wird sofort wirksam.",
|
|
1442
|
+
wizardConfirmScheduled: "Der Wechsel wird wirksam zum",
|
|
1443
|
+
wizardConfirmAction: "Wechsel best\xE4tigen",
|
|
1444
|
+
wizardConfirmInProgress: "Wechsel l\xE4uft \u2026",
|
|
1445
|
+
wizardConfirmPriceTitle: "Preis\xFCbersicht",
|
|
1446
|
+
wizardConfirmProratedNow: "Heute anteilig f\xE4llig",
|
|
1447
|
+
wizardConfirmRecurringNext: "Regul\xE4r ab n\xE4chster Periode",
|
|
1448
|
+
wizardConfirmRecurringFrom: "Regul\xE4r f\xE4llig ab",
|
|
1449
|
+
wizardConfirmPerCycleMonthly: "pro Monat",
|
|
1450
|
+
wizardConfirmPerCycleYearly: "pro Jahr",
|
|
1451
|
+
wizardConfirmTrialNote: "W\xE4hrend der Testphase f\xE4llt nichts an.",
|
|
1452
|
+
wizardConfirmRecurringTrialEnd: "Regul\xE4r ab Ende der Testphase",
|
|
1453
|
+
wizardChangeTypeUpgrade: "Upgrade",
|
|
1454
|
+
wizardChangeTypeDowngrade: "Downgrade",
|
|
1455
|
+
wizardChangeTypeCycle: "Zyklus-Wechsel",
|
|
1456
|
+
wizardChangeTypeNoop: "Keine \xC4nderung",
|
|
1457
|
+
packageSnapshotTitle: "Gebuchtes Paket (Snapshot)",
|
|
1458
|
+
packageSnapshotSubtitle: "Schreibgesch\xFCtzte Kopie des Pakets, wie es beim Abschluss vermarktet wurde.",
|
|
1459
|
+
packageSnapshotCapturedAt: "Erfasst am",
|
|
1460
|
+
packageSnapshotOfferRef: "Angebots-Referenz",
|
|
1461
|
+
packageSnapshotPlanLabel: "Plan",
|
|
1462
|
+
packageSnapshotPlanVersionLabel: "Plan-Version",
|
|
1463
|
+
packageSnapshotCycleLabel: "Abrechnungszyklus",
|
|
1464
|
+
packageSnapshotBundlesLabel: "Enthaltene Bundles",
|
|
1465
|
+
packageSnapshotBundlesEmpty: "Keine Bundles im Paket.",
|
|
1466
|
+
packageSnapshotPriceMonthly: "Preis monatlich",
|
|
1467
|
+
packageSnapshotPriceYearly: "Preis j\xE4hrlich",
|
|
1468
|
+
packageSnapshotPriceTotal: "Gesamtpreis",
|
|
1469
|
+
packageSnapshotNone: "Diese Subscription wurde nicht \xFCber ein Webseiten-Angebot abgeschlossen.",
|
|
1470
|
+
packageSnapshotShowRaw: "Rohdaten anzeigen",
|
|
1471
|
+
packageSnapshotHideRaw: "Rohdaten ausblenden"
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
// src/use-plan-versions.ts
|
|
1475
|
+
import { ref as ref9 } from "vue";
|
|
1476
|
+
function buildVersionsComposable(label) {
|
|
1477
|
+
return function(options) {
|
|
1478
|
+
if (!options?.endpoint) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
`${label}: \`endpoint\` ist Pflicht (z. B. "/api/admin/${label}" oder "/api/v1/admin/${label}"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
const filter = options.filter ?? ref9({});
|
|
1484
|
+
const list = useApiList({
|
|
1485
|
+
endpoint: options.endpoint,
|
|
1486
|
+
filter,
|
|
1487
|
+
http: options.http,
|
|
1488
|
+
getAuthToken: options.getAuthToken,
|
|
1489
|
+
autoLoad: options.autoLoad
|
|
1490
|
+
});
|
|
1491
|
+
return { ...list, filter };
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
var usePlanVersionsCatalog = buildVersionsComposable("plan-versions");
|
|
1495
|
+
|
|
1496
|
+
// src/use-bulk-publish.ts
|
|
1497
|
+
import { computed as computed3, ref as ref10 } from "vue";
|
|
1498
|
+
function useBulkPublish(options) {
|
|
1499
|
+
if (!options?.endpoints) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
"useBulkPublish: `endpoints` ist Pflicht (Mapping plan \u2192 URL-Builder). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben."
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const http = options.http ?? defaultHttpClient();
|
|
1505
|
+
const endpoints = options.endpoints;
|
|
1506
|
+
const items = ref10([]);
|
|
1507
|
+
const successCount = computed3(() => items.value.filter((i) => i.status === "published").length);
|
|
1508
|
+
const failureCount = computed3(() => items.value.filter((i) => i.status === "failed").length);
|
|
1509
|
+
const progress = computed3(() => {
|
|
1510
|
+
if (items.value.length === 0) return 0;
|
|
1511
|
+
const done2 = successCount.value + failureCount.value;
|
|
1512
|
+
return done2 / items.value.length;
|
|
1513
|
+
});
|
|
1514
|
+
const done = computed3(
|
|
1515
|
+
() => items.value.length > 0 && items.value.every((i) => i.status === "published" || i.status === "failed")
|
|
1516
|
+
);
|
|
1517
|
+
function setItems(next) {
|
|
1518
|
+
items.value = next.map((i) => ({ ...i, status: "pending" }));
|
|
1519
|
+
}
|
|
1520
|
+
async function publishOne(item, body, mfaCode) {
|
|
1521
|
+
item.status = "publishing";
|
|
1522
|
+
try {
|
|
1523
|
+
const url = endpoints[item.kind](item.draftId);
|
|
1524
|
+
const headers = {
|
|
1525
|
+
"Content-Type": "application/json"
|
|
1526
|
+
};
|
|
1527
|
+
const token = options.getAuthToken?.();
|
|
1528
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1529
|
+
if (mfaCode) headers["X-Mfa-Code"] = mfaCode;
|
|
1530
|
+
const res = await http(url, {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
headers,
|
|
1533
|
+
body: JSON.stringify(body)
|
|
1534
|
+
});
|
|
1535
|
+
if (res.status >= 200 && res.status < 300) {
|
|
1536
|
+
item.result = await res.json();
|
|
1537
|
+
item.status = "published";
|
|
1538
|
+
} else {
|
|
1539
|
+
item.error = `HTTP ${res.status}: ${await res.text()}`;
|
|
1540
|
+
item.status = "failed";
|
|
1541
|
+
}
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
item.error = err instanceof Error ? err.message : String(err);
|
|
1544
|
+
item.status = "failed";
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async function run(input) {
|
|
1548
|
+
const note = input.changeNote.trim();
|
|
1549
|
+
if (note.length === 0) {
|
|
1550
|
+
for (const item of items.value) {
|
|
1551
|
+
item.status = "failed";
|
|
1552
|
+
item.error = "changeNote ist Pflicht beim Publish.";
|
|
1553
|
+
}
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
await Promise.all(
|
|
1557
|
+
items.value.map((item) => publishOne(item, { changeNote: note }, input.mfaCode))
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
items,
|
|
1562
|
+
progress,
|
|
1563
|
+
done,
|
|
1564
|
+
successCount,
|
|
1565
|
+
failureCount,
|
|
1566
|
+
setItems,
|
|
1567
|
+
run
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/use-plan-editor.ts
|
|
1572
|
+
import { computed as computed4, ref as ref11 } from "vue";
|
|
1573
|
+
var PlannedOnlyFeatureError = class extends Error {
|
|
1574
|
+
constructor(violations) {
|
|
1575
|
+
super(
|
|
1576
|
+
`Folgende Features sind im Catalog als plannedOnly markiert und d\xFCrfen nicht in einer Plan-Version stehen: ${violations.sort().join(", ")}.`
|
|
1577
|
+
);
|
|
1578
|
+
this.violations = violations;
|
|
1579
|
+
this.name = "PlannedOnlyFeatureError";
|
|
1580
|
+
}
|
|
1581
|
+
violations;
|
|
1582
|
+
};
|
|
1583
|
+
var TIER_ORDER = ["CORE", "ADVANCED", "PRO", "BUSINESS", "ENTERPRISE_ONLY"];
|
|
1584
|
+
function tierWeight(tier) {
|
|
1585
|
+
if (!tier) return TIER_ORDER.length;
|
|
1586
|
+
const i = TIER_ORDER.indexOf(tier);
|
|
1587
|
+
return i === -1 ? TIER_ORDER.length : i;
|
|
1588
|
+
}
|
|
1589
|
+
function usePlanEditor(manifest, options = {}) {
|
|
1590
|
+
const baseSet = new Set(options.baseFeatures ?? []);
|
|
1591
|
+
const nonRegressive = options.nonRegressive ?? true;
|
|
1592
|
+
const selectedFeatures = ref11(new Set(options.initialFeatures ?? []));
|
|
1593
|
+
const catalogFeatures = manifest.planCatalogSnapshot.features ?? [];
|
|
1594
|
+
const plannedOnlySet = new Set(catalogFeatures.filter((f) => f.plannedOnly).map((f) => f.key));
|
|
1595
|
+
const availableFeatures = computed4(() => {
|
|
1596
|
+
return catalogFeatures.map((def) => {
|
|
1597
|
+
const isSelected = selectedFeatures.value.has(def.key);
|
|
1598
|
+
const isPlannedOnly = !!def.plannedOnly;
|
|
1599
|
+
const isInherited = baseSet.has(def.key);
|
|
1600
|
+
const canToggle = !isPlannedOnly && (!nonRegressive || !isInherited || !isSelected);
|
|
1601
|
+
return { def, isSelected, isPlannedOnly, isInherited, canToggle };
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
const featuresByTier = computed4(() => {
|
|
1605
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1606
|
+
for (const row of availableFeatures.value) {
|
|
1607
|
+
const tier = row.def.tier ?? "OTHER";
|
|
1608
|
+
const list = groups.get(tier) ?? [];
|
|
1609
|
+
list.push(row);
|
|
1610
|
+
groups.set(tier, list);
|
|
1611
|
+
}
|
|
1612
|
+
return [...groups.entries()].sort((a, b) => {
|
|
1613
|
+
const wa = tierWeight(a[0]);
|
|
1614
|
+
const wb = tierWeight(b[0]);
|
|
1615
|
+
return wa !== wb ? wa - wb : a[0].localeCompare(b[0]);
|
|
1616
|
+
}).map(([tier, rows]) => ({ tier, rows }));
|
|
1617
|
+
});
|
|
1618
|
+
function toggleFeature(key) {
|
|
1619
|
+
if (plannedOnlySet.has(key)) return;
|
|
1620
|
+
const next = new Set(selectedFeatures.value);
|
|
1621
|
+
if (next.has(key)) {
|
|
1622
|
+
if (nonRegressive && baseSet.has(key)) return;
|
|
1623
|
+
next.delete(key);
|
|
1624
|
+
} else {
|
|
1625
|
+
next.add(key);
|
|
1626
|
+
}
|
|
1627
|
+
selectedFeatures.value = next;
|
|
1628
|
+
}
|
|
1629
|
+
function validateDraft() {
|
|
1630
|
+
const violations = [...selectedFeatures.value].filter((k) => plannedOnlySet.has(k));
|
|
1631
|
+
if (violations.length > 0) {
|
|
1632
|
+
throw new PlannedOnlyFeatureError(violations);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function snapshot() {
|
|
1636
|
+
return [...selectedFeatures.value].sort();
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
selectedFeatures,
|
|
1640
|
+
availableFeatures,
|
|
1641
|
+
featuresByTier,
|
|
1642
|
+
toggleFeature,
|
|
1643
|
+
validateDraft,
|
|
1644
|
+
snapshot
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/use-public-boot.ts
|
|
1649
|
+
import { ref as ref12 } from "vue";
|
|
1650
|
+
function usePublicBoot(options) {
|
|
1651
|
+
const loader = new BootLoader(options);
|
|
1652
|
+
const boot = ref12(null);
|
|
1653
|
+
const loading = ref12(false);
|
|
1654
|
+
const error = ref12(null);
|
|
1655
|
+
async function load() {
|
|
1656
|
+
loading.value = true;
|
|
1657
|
+
error.value = null;
|
|
1658
|
+
try {
|
|
1659
|
+
boot.value = await loader.load();
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1662
|
+
boot.value = null;
|
|
1663
|
+
} finally {
|
|
1664
|
+
loading.value = false;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return { boot, loading, error, load };
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/use-discovery.ts
|
|
1671
|
+
import { ref as ref13 } from "vue";
|
|
1672
|
+
var DiscoveryLoadError = class extends Error {
|
|
1673
|
+
constructor(status, message) {
|
|
1674
|
+
super(message);
|
|
1675
|
+
this.status = status;
|
|
1676
|
+
this.name = "DiscoveryLoadError";
|
|
1677
|
+
}
|
|
1678
|
+
status;
|
|
1679
|
+
};
|
|
1680
|
+
function useDiscovery(options) {
|
|
1681
|
+
if (!options?.endpoint) {
|
|
1682
|
+
throw new Error(
|
|
1683
|
+
'useDiscovery: `endpoint` ist Pflicht (z. B. "/api/admin/discovery" oder "/api/v1/admin/discovery"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
const http = options.http ?? defaultHttpClient();
|
|
1687
|
+
const snapshot = ref13(null);
|
|
1688
|
+
const etag = ref13(null);
|
|
1689
|
+
const loading = ref13(false);
|
|
1690
|
+
const error = ref13(null);
|
|
1691
|
+
async function fetchSnapshot(includeEtag) {
|
|
1692
|
+
loading.value = true;
|
|
1693
|
+
error.value = null;
|
|
1694
|
+
try {
|
|
1695
|
+
const headers = {};
|
|
1696
|
+
const token = options.getAuthToken?.();
|
|
1697
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1698
|
+
if (includeEtag && etag.value) {
|
|
1699
|
+
headers["If-None-Match"] = etag.value;
|
|
1700
|
+
}
|
|
1701
|
+
const res = await http(options.endpoint, { method: "GET", headers });
|
|
1702
|
+
if (res.status === 304) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (res.status !== 200) {
|
|
1706
|
+
throw new DiscoveryLoadError(
|
|
1707
|
+
res.status,
|
|
1708
|
+
`Discovery-Endpoint antwortete mit HTTP ${res.status}`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
const body = await res.json();
|
|
1712
|
+
snapshot.value = body;
|
|
1713
|
+
etag.value = res.headers.get("ETag");
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1716
|
+
} finally {
|
|
1717
|
+
loading.value = false;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
async function load() {
|
|
1721
|
+
await fetchSnapshot(true);
|
|
1722
|
+
}
|
|
1723
|
+
async function reload() {
|
|
1724
|
+
etag.value = null;
|
|
1725
|
+
await fetchSnapshot(false);
|
|
1726
|
+
}
|
|
1727
|
+
async function rescan() {
|
|
1728
|
+
loading.value = true;
|
|
1729
|
+
error.value = null;
|
|
1730
|
+
try {
|
|
1731
|
+
const headers = { "content-type": "application/json" };
|
|
1732
|
+
const token = options.getAuthToken?.();
|
|
1733
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1734
|
+
const res = await http(`${options.endpoint}/rescan`, { method: "POST", headers });
|
|
1735
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
1736
|
+
throw new DiscoveryLoadError(
|
|
1737
|
+
res.status,
|
|
1738
|
+
`Discovery-Rescan antwortete mit HTTP ${res.status}`
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const body = await res.json();
|
|
1742
|
+
snapshot.value = body;
|
|
1743
|
+
etag.value = res.headers.get("ETag");
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1746
|
+
} finally {
|
|
1747
|
+
loading.value = false;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (options.autoLoad) {
|
|
1751
|
+
void load();
|
|
1752
|
+
}
|
|
1753
|
+
return { snapshot, etag, loading, error, load, reload, rescan };
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/use-catalog-entries.ts
|
|
1757
|
+
import { ref as ref14 } from "vue";
|
|
1758
|
+
var CatalogEntriesApiError = class extends Error {
|
|
1759
|
+
constructor(status, body, message) {
|
|
1760
|
+
super(message);
|
|
1761
|
+
this.status = status;
|
|
1762
|
+
this.body = body;
|
|
1763
|
+
this.name = "CatalogEntriesApiError";
|
|
1764
|
+
}
|
|
1765
|
+
status;
|
|
1766
|
+
body;
|
|
1767
|
+
};
|
|
1768
|
+
function useCatalogEntries(options) {
|
|
1769
|
+
if (!options?.adminEndpoint) {
|
|
1770
|
+
throw new Error("useCatalogEntries: `adminEndpoint` ist Pflicht.");
|
|
1771
|
+
}
|
|
1772
|
+
if (!options?.projectKey) {
|
|
1773
|
+
throw new Error("useCatalogEntries: `projectKey` ist Pflicht.");
|
|
1774
|
+
}
|
|
1775
|
+
const http = options.http ?? defaultHttpClient();
|
|
1776
|
+
const capabilities = ref14([]);
|
|
1777
|
+
const features = ref14([]);
|
|
1778
|
+
const quotas = ref14([]);
|
|
1779
|
+
const loading = ref14(false);
|
|
1780
|
+
const error = ref14(null);
|
|
1781
|
+
const base = `${options.adminEndpoint}/catalog`;
|
|
1782
|
+
const pk = encodeURIComponent(options.projectKey);
|
|
1783
|
+
function authHeaders() {
|
|
1784
|
+
const token = options.getAuthToken?.();
|
|
1785
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
1786
|
+
}
|
|
1787
|
+
async function fetchJson(url, init) {
|
|
1788
|
+
const res = await http(url, {
|
|
1789
|
+
method: init?.method ?? "GET",
|
|
1790
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
1791
|
+
body: init?.body
|
|
1792
|
+
});
|
|
1793
|
+
if (res.status === 204) return null;
|
|
1794
|
+
const body = await res.json().catch(() => null);
|
|
1795
|
+
if (res.status >= 400) {
|
|
1796
|
+
throw new CatalogEntriesApiError(
|
|
1797
|
+
res.status,
|
|
1798
|
+
body,
|
|
1799
|
+
`Catalog-Entries-API antwortete mit HTTP ${res.status}`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
return body;
|
|
1803
|
+
}
|
|
1804
|
+
async function load() {
|
|
1805
|
+
loading.value = true;
|
|
1806
|
+
error.value = null;
|
|
1807
|
+
try {
|
|
1808
|
+
const [caps, feats, qs] = await Promise.all([
|
|
1809
|
+
fetchJson(`${base}/capabilities?projectKey=${pk}`),
|
|
1810
|
+
fetchJson(`${base}/features?projectKey=${pk}`),
|
|
1811
|
+
fetchJson(`${base}/quotas?projectKey=${pk}`)
|
|
1812
|
+
]);
|
|
1813
|
+
capabilities.value = caps ?? [];
|
|
1814
|
+
features.value = feats ?? [];
|
|
1815
|
+
quotas.value = qs ?? [];
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1818
|
+
} finally {
|
|
1819
|
+
loading.value = false;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async function reviewFeature(featureKey, data) {
|
|
1823
|
+
const updated = await fetchJson(
|
|
1824
|
+
`${base}/features/${encodeURIComponent(featureKey)}/review?projectKey=${pk}`,
|
|
1825
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
1826
|
+
);
|
|
1827
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "Review gab keinen Body zur\xFCck");
|
|
1828
|
+
features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
|
|
1829
|
+
return updated;
|
|
1830
|
+
}
|
|
1831
|
+
async function reviewQuota(quotaKey, data) {
|
|
1832
|
+
const updated = await fetchJson(
|
|
1833
|
+
`${base}/quotas/${encodeURIComponent(quotaKey)}/review?projectKey=${pk}`,
|
|
1834
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
1835
|
+
);
|
|
1836
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "Review gab keinen Body zur\xFCck");
|
|
1837
|
+
quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
|
|
1838
|
+
return updated;
|
|
1839
|
+
}
|
|
1840
|
+
async function setFeatureI18n(featureKey, i18n) {
|
|
1841
|
+
const updated = await fetchJson(
|
|
1842
|
+
`${base}/features/${encodeURIComponent(featureKey)}/i18n?projectKey=${pk}`,
|
|
1843
|
+
{ method: "PATCH", body: JSON.stringify({ i18n }) }
|
|
1844
|
+
);
|
|
1845
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "i18n gab keinen Body zur\xFCck");
|
|
1846
|
+
features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
|
|
1847
|
+
return updated;
|
|
1848
|
+
}
|
|
1849
|
+
async function setQuotaI18n(quotaKey, i18n) {
|
|
1850
|
+
const updated = await fetchJson(
|
|
1851
|
+
`${base}/quotas/${encodeURIComponent(quotaKey)}/i18n?projectKey=${pk}`,
|
|
1852
|
+
{ method: "PATCH", body: JSON.stringify({ i18n }) }
|
|
1853
|
+
);
|
|
1854
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "i18n gab keinen Body zur\xFCck");
|
|
1855
|
+
quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
|
|
1856
|
+
return updated;
|
|
1857
|
+
}
|
|
1858
|
+
async function setFeatureBase(featureKey, data) {
|
|
1859
|
+
const updated = await fetchJson(
|
|
1860
|
+
`${base}/features/${encodeURIComponent(featureKey)}?projectKey=${pk}`,
|
|
1861
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
1862
|
+
);
|
|
1863
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "Base gab keinen Body zur\xFCck");
|
|
1864
|
+
features.value = features.value.map((f) => f.featureKey === featureKey ? updated : f);
|
|
1865
|
+
return updated;
|
|
1866
|
+
}
|
|
1867
|
+
async function setQuotaBase(quotaKey, data) {
|
|
1868
|
+
const updated = await fetchJson(
|
|
1869
|
+
`${base}/quotas/${encodeURIComponent(quotaKey)}?projectKey=${pk}`,
|
|
1870
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
1871
|
+
);
|
|
1872
|
+
if (!updated) throw new CatalogEntriesApiError(0, null, "Base gab keinen Body zur\xFCck");
|
|
1873
|
+
quotas.value = quotas.value.map((q) => q.quotaKey === quotaKey ? updated : q);
|
|
1874
|
+
return updated;
|
|
1875
|
+
}
|
|
1876
|
+
async function syncDiscovery(snapshot) {
|
|
1877
|
+
const result = await fetchJson(`${base}/discovery/sync`, {
|
|
1878
|
+
method: "POST",
|
|
1879
|
+
body: JSON.stringify({ snapshot })
|
|
1880
|
+
});
|
|
1881
|
+
if (!result) throw new CatalogEntriesApiError(0, null, "Sync gab keinen Body zur\xFCck");
|
|
1882
|
+
await load();
|
|
1883
|
+
return result;
|
|
1884
|
+
}
|
|
1885
|
+
if (options.autoLoad) {
|
|
1886
|
+
void load();
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
capabilities,
|
|
1890
|
+
features,
|
|
1891
|
+
quotas,
|
|
1892
|
+
loading,
|
|
1893
|
+
error,
|
|
1894
|
+
load,
|
|
1895
|
+
reviewFeature,
|
|
1896
|
+
reviewQuota,
|
|
1897
|
+
setFeatureI18n,
|
|
1898
|
+
setQuotaI18n,
|
|
1899
|
+
setFeatureBase,
|
|
1900
|
+
setQuotaBase,
|
|
1901
|
+
syncDiscovery
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/use-bundles.ts
|
|
1906
|
+
import { ref as ref15 } from "vue";
|
|
1907
|
+
var BundlesApiError = class extends Error {
|
|
1908
|
+
constructor(status, body, message) {
|
|
1909
|
+
super(message);
|
|
1910
|
+
this.status = status;
|
|
1911
|
+
this.body = body;
|
|
1912
|
+
this.name = "BundlesApiError";
|
|
1913
|
+
}
|
|
1914
|
+
status;
|
|
1915
|
+
body;
|
|
1916
|
+
};
|
|
1917
|
+
function useBundles(options) {
|
|
1918
|
+
if (!options?.adminEndpoint) {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
'useBundles: `adminEndpoint` ist Pflicht (z. B. "/api/admin" oder "/api/v1/admin"). Plattform hat keinen Default, weil Apps unterschiedliche globalPrefix-Konventionen haben.'
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (!options?.projectKey) {
|
|
1924
|
+
throw new Error("useBundles: `projectKey` ist Pflicht.");
|
|
1925
|
+
}
|
|
1926
|
+
const http = options.http ?? defaultHttpClient();
|
|
1927
|
+
const bundles = ref15([]);
|
|
1928
|
+
const loading = ref15(false);
|
|
1929
|
+
const error = ref15(null);
|
|
1930
|
+
const baseUrl = `${options.adminEndpoint}/catalog/bundles`;
|
|
1931
|
+
function authHeaders() {
|
|
1932
|
+
const token = options.getAuthToken?.();
|
|
1933
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
1934
|
+
}
|
|
1935
|
+
async function fetchJson(url, init) {
|
|
1936
|
+
const res = await http(url, {
|
|
1937
|
+
method: init?.method ?? "GET",
|
|
1938
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
1939
|
+
body: init?.body
|
|
1940
|
+
});
|
|
1941
|
+
if (res.status === 204) return null;
|
|
1942
|
+
const body = await res.json().catch(() => null);
|
|
1943
|
+
if (res.status >= 400) {
|
|
1944
|
+
throw new BundlesApiError(
|
|
1945
|
+
res.status,
|
|
1946
|
+
body,
|
|
1947
|
+
`Bundles-API antwortete mit HTTP ${res.status}`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
return body;
|
|
1951
|
+
}
|
|
1952
|
+
async function load() {
|
|
1953
|
+
loading.value = true;
|
|
1954
|
+
error.value = null;
|
|
1955
|
+
try {
|
|
1956
|
+
const data = await fetchJson(
|
|
1957
|
+
`${baseUrl}?projectKey=${encodeURIComponent(options.projectKey)}`
|
|
1958
|
+
);
|
|
1959
|
+
bundles.value = data ?? [];
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1962
|
+
} finally {
|
|
1963
|
+
loading.value = false;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function create(data) {
|
|
1967
|
+
const created = await fetchJson(baseUrl, {
|
|
1968
|
+
method: "POST",
|
|
1969
|
+
body: JSON.stringify(data)
|
|
1970
|
+
});
|
|
1971
|
+
if (!created) throw new BundlesApiError(0, null, "Create gab keinen Body zur\xFCck");
|
|
1972
|
+
bundles.value = [...bundles.value, created];
|
|
1973
|
+
return created;
|
|
1974
|
+
}
|
|
1975
|
+
async function update(bundleId, data) {
|
|
1976
|
+
const updated = await fetchJson(`${baseUrl}/${bundleId}`, {
|
|
1977
|
+
method: "PATCH",
|
|
1978
|
+
body: JSON.stringify(data)
|
|
1979
|
+
});
|
|
1980
|
+
if (!updated) throw new BundlesApiError(0, null, "Update gab keinen Body zur\xFCck");
|
|
1981
|
+
bundles.value = bundles.value.map((b) => b.id === bundleId ? updated : b);
|
|
1982
|
+
return updated;
|
|
1983
|
+
}
|
|
1984
|
+
async function softDelete(bundleId) {
|
|
1985
|
+
await fetchJson(`${baseUrl}/${bundleId}`, { method: "DELETE" });
|
|
1986
|
+
bundles.value = bundles.value.filter((b) => b.id !== bundleId);
|
|
1987
|
+
}
|
|
1988
|
+
if (options.autoLoad) {
|
|
1989
|
+
void load();
|
|
1990
|
+
}
|
|
1991
|
+
return { bundles, loading, error, load, create, update, softDelete };
|
|
1992
|
+
}
|
|
1993
|
+
function useBundleVersions(options) {
|
|
1994
|
+
if (!options?.adminEndpoint) {
|
|
1995
|
+
throw new Error("useBundleVersions: `adminEndpoint` ist Pflicht.");
|
|
1996
|
+
}
|
|
1997
|
+
if (!options?.bundleId) {
|
|
1998
|
+
throw new Error("useBundleVersions: `bundleId` ist Pflicht.");
|
|
1999
|
+
}
|
|
2000
|
+
const http = options.http ?? defaultHttpClient();
|
|
2001
|
+
const versions = ref15([]);
|
|
2002
|
+
const loading = ref15(false);
|
|
2003
|
+
const error = ref15(null);
|
|
2004
|
+
const bundleVersionsUrl = `${options.adminEndpoint}/catalog/bundles/${options.bundleId}/versions`;
|
|
2005
|
+
const versionUrlBase = `${options.adminEndpoint}/catalog/bundle-versions`;
|
|
2006
|
+
function authHeaders() {
|
|
2007
|
+
const token = options.getAuthToken?.();
|
|
2008
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2009
|
+
}
|
|
2010
|
+
async function fetchJson(url, init) {
|
|
2011
|
+
const res = await http(url, {
|
|
2012
|
+
method: init?.method ?? "GET",
|
|
2013
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2014
|
+
body: init?.body
|
|
2015
|
+
});
|
|
2016
|
+
if (res.status === 204) return null;
|
|
2017
|
+
const body = await res.json().catch(() => null);
|
|
2018
|
+
if (res.status >= 400) {
|
|
2019
|
+
throw new BundlesApiError(
|
|
2020
|
+
res.status,
|
|
2021
|
+
body,
|
|
2022
|
+
`BundleVersions-API antwortete mit HTTP ${res.status}`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return body;
|
|
2026
|
+
}
|
|
2027
|
+
async function load() {
|
|
2028
|
+
loading.value = true;
|
|
2029
|
+
error.value = null;
|
|
2030
|
+
try {
|
|
2031
|
+
const data = await fetchJson(bundleVersionsUrl);
|
|
2032
|
+
versions.value = data ?? [];
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2035
|
+
} finally {
|
|
2036
|
+
loading.value = false;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
async function createDraft(data) {
|
|
2040
|
+
const result = await fetchJson(bundleVersionsUrl, {
|
|
2041
|
+
method: "POST",
|
|
2042
|
+
body: JSON.stringify(data)
|
|
2043
|
+
});
|
|
2044
|
+
if (!result) throw new BundlesApiError(0, null, "CreateDraft gab keinen Body zur\xFCck");
|
|
2045
|
+
versions.value = [...versions.value, result.bundleVersion];
|
|
2046
|
+
return result;
|
|
2047
|
+
}
|
|
2048
|
+
async function updateDraft(versionId, data) {
|
|
2049
|
+
const result = await fetchJson(
|
|
2050
|
+
`${versionUrlBase}/${versionId}`,
|
|
2051
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
2052
|
+
);
|
|
2053
|
+
if (!result) throw new BundlesApiError(0, null, "UpdateDraft gab keinen Body zur\xFCck");
|
|
2054
|
+
versions.value = versions.value.map((v) => v.id === versionId ? result.bundleVersion : v);
|
|
2055
|
+
return result;
|
|
2056
|
+
}
|
|
2057
|
+
async function publish(versionId, opts = {}) {
|
|
2058
|
+
const result = await fetchJson(
|
|
2059
|
+
`${versionUrlBase}/${versionId}/publish`,
|
|
2060
|
+
{ method: "POST", body: JSON.stringify(opts) }
|
|
2061
|
+
);
|
|
2062
|
+
if (!result) throw new BundlesApiError(0, null, "Publish gab keinen Body zur\xFCck");
|
|
2063
|
+
await load();
|
|
2064
|
+
return result;
|
|
2065
|
+
}
|
|
2066
|
+
async function discardDraft(versionId) {
|
|
2067
|
+
await fetchJson(`${versionUrlBase}/${versionId}`, { method: "DELETE" });
|
|
2068
|
+
versions.value = versions.value.filter((v) => v.id !== versionId);
|
|
2069
|
+
}
|
|
2070
|
+
if (options.autoLoad) {
|
|
2071
|
+
void load();
|
|
2072
|
+
}
|
|
2073
|
+
return { versions, loading, error, load, createDraft, updateDraft, publish, discardDraft };
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/use-bundle-versions-map.ts
|
|
2077
|
+
import { ref as ref16, watch as watch2 } from "vue";
|
|
2078
|
+
function useBundleVersionsMap(options) {
|
|
2079
|
+
if (!options?.adminEndpoint) {
|
|
2080
|
+
throw new Error("useBundleVersionsMap: `adminEndpoint` ist Pflicht.");
|
|
2081
|
+
}
|
|
2082
|
+
if (!options?.bundles) {
|
|
2083
|
+
throw new Error("useBundleVersionsMap: `bundles` ist Pflicht.");
|
|
2084
|
+
}
|
|
2085
|
+
const http = options.http ?? defaultHttpClient();
|
|
2086
|
+
const versionsByBundle = ref16({});
|
|
2087
|
+
const loading = ref16(false);
|
|
2088
|
+
const error = ref16(null);
|
|
2089
|
+
function authHeaders() {
|
|
2090
|
+
const token = options.getAuthToken?.();
|
|
2091
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2092
|
+
}
|
|
2093
|
+
async function loadForBundle(bundleId) {
|
|
2094
|
+
const res = await http(`${options.adminEndpoint}/catalog/bundles/${bundleId}/versions`, {
|
|
2095
|
+
method: "GET",
|
|
2096
|
+
headers: { "content-type": "application/json", ...authHeaders() }
|
|
2097
|
+
});
|
|
2098
|
+
if (res.status >= 400) {
|
|
2099
|
+
throw new Error(`HTTP ${res.status} beim Laden der Versionen f\xFCr Bundle '${bundleId}'`);
|
|
2100
|
+
}
|
|
2101
|
+
return await res.json().catch(() => null) ?? [];
|
|
2102
|
+
}
|
|
2103
|
+
async function refresh() {
|
|
2104
|
+
loading.value = true;
|
|
2105
|
+
error.value = null;
|
|
2106
|
+
try {
|
|
2107
|
+
const entries = await Promise.all(
|
|
2108
|
+
options.bundles.value.map(async (b) => {
|
|
2109
|
+
try {
|
|
2110
|
+
const versions = await loadForBundle(b.id);
|
|
2111
|
+
return [b.id, versions];
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
console.warn(
|
|
2114
|
+
`useBundleVersionsMap: Bundle '${b.bundleKey}' Versions-Load failed`,
|
|
2115
|
+
err
|
|
2116
|
+
);
|
|
2117
|
+
return [b.id, []];
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
);
|
|
2121
|
+
versionsByBundle.value = Object.fromEntries(entries);
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2124
|
+
} finally {
|
|
2125
|
+
loading.value = false;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function refreshOne(bundleId) {
|
|
2129
|
+
try {
|
|
2130
|
+
const versions = await loadForBundle(bundleId);
|
|
2131
|
+
versionsByBundle.value = { ...versionsByBundle.value, [bundleId]: versions };
|
|
2132
|
+
} catch (err) {
|
|
2133
|
+
console.warn(`useBundleVersionsMap: refreshOne('${bundleId}') failed`, err);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
watch2(
|
|
2137
|
+
() => options.bundles.value.map((b) => b.id).join(","),
|
|
2138
|
+
() => {
|
|
2139
|
+
if (options.bundles.value.length > 0) void refresh();
|
|
2140
|
+
else versionsByBundle.value = {};
|
|
2141
|
+
},
|
|
2142
|
+
{ immediate: true }
|
|
2143
|
+
);
|
|
2144
|
+
return { versionsByBundle, loading, error, refresh, refreshOne };
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// src/use-tenant-subscription-bundles.ts
|
|
2148
|
+
import { ref as ref17 } from "vue";
|
|
2149
|
+
var TenantSubscriptionBundlesApiError = class extends Error {
|
|
2150
|
+
constructor(status, body, message) {
|
|
2151
|
+
super(message);
|
|
2152
|
+
this.status = status;
|
|
2153
|
+
this.body = body;
|
|
2154
|
+
this.name = "TenantSubscriptionBundlesApiError";
|
|
2155
|
+
}
|
|
2156
|
+
status;
|
|
2157
|
+
body;
|
|
2158
|
+
};
|
|
2159
|
+
function useTenantSubscriptionBundles(options) {
|
|
2160
|
+
if (!options?.billingEndpoint) {
|
|
2161
|
+
throw new Error(
|
|
2162
|
+
'useTenantSubscriptionBundles: `billingEndpoint` ist Pflicht (z. B. "/api/v1").'
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
const http = options.http ?? defaultHttpClient();
|
|
2166
|
+
const baseUrl = `${options.billingEndpoint}/billing/subscription-bundles`;
|
|
2167
|
+
const bundles = ref17([]);
|
|
2168
|
+
const loading = ref17(false);
|
|
2169
|
+
const error = ref17(null);
|
|
2170
|
+
function authHeaders() {
|
|
2171
|
+
const token = options.getAuthToken?.();
|
|
2172
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2173
|
+
}
|
|
2174
|
+
async function fetchJson(url, init) {
|
|
2175
|
+
const res = await http(url, {
|
|
2176
|
+
method: init?.method ?? "GET",
|
|
2177
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2178
|
+
body: init?.body
|
|
2179
|
+
});
|
|
2180
|
+
if (res.status === 204) return null;
|
|
2181
|
+
const body = await res.json().catch(() => null);
|
|
2182
|
+
if (res.status >= 400) {
|
|
2183
|
+
throw new TenantSubscriptionBundlesApiError(
|
|
2184
|
+
res.status,
|
|
2185
|
+
body,
|
|
2186
|
+
`SubscriptionBundle-API antwortete mit HTTP ${res.status}`
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
return body;
|
|
2190
|
+
}
|
|
2191
|
+
async function load() {
|
|
2192
|
+
loading.value = true;
|
|
2193
|
+
error.value = null;
|
|
2194
|
+
try {
|
|
2195
|
+
const data = await fetchJson(baseUrl);
|
|
2196
|
+
bundles.value = (data ?? []).map(rehydrateDates);
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2199
|
+
} finally {
|
|
2200
|
+
loading.value = false;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
async function add(data) {
|
|
2204
|
+
const result = await fetchJson(baseUrl, {
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
body: JSON.stringify(data)
|
|
2207
|
+
});
|
|
2208
|
+
if (!result) {
|
|
2209
|
+
throw new TenantSubscriptionBundlesApiError(0, null, "add gab keinen Body zur\xFCck");
|
|
2210
|
+
}
|
|
2211
|
+
const hydrated = rehydrateDates(result);
|
|
2212
|
+
bundles.value = [hydrated, ...bundles.value];
|
|
2213
|
+
return hydrated;
|
|
2214
|
+
}
|
|
2215
|
+
async function cancel(subscriptionBundleId, opts = {}) {
|
|
2216
|
+
const result = await fetchJson(
|
|
2217
|
+
`${baseUrl}/${subscriptionBundleId}`,
|
|
2218
|
+
{ method: "DELETE", body: JSON.stringify(opts) }
|
|
2219
|
+
);
|
|
2220
|
+
if (!result) {
|
|
2221
|
+
throw new TenantSubscriptionBundlesApiError(0, null, "cancel gab keinen Body zur\xFCck");
|
|
2222
|
+
}
|
|
2223
|
+
const hydrated = rehydrateDates(result);
|
|
2224
|
+
bundles.value = bundles.value.map((b) => b.id === subscriptionBundleId ? hydrated : b);
|
|
2225
|
+
return hydrated;
|
|
2226
|
+
}
|
|
2227
|
+
if (options.autoLoad) void load();
|
|
2228
|
+
return { bundles, loading, error, load, add, cancel };
|
|
2229
|
+
}
|
|
2230
|
+
function rehydrateDates(raw) {
|
|
2231
|
+
return {
|
|
2232
|
+
...raw,
|
|
2233
|
+
startedAt: new Date(raw.startedAt),
|
|
2234
|
+
minimumTermEndsAt: raw.minimumTermEndsAt ? new Date(raw.minimumTermEndsAt) : null,
|
|
2235
|
+
canceledAt: raw.canceledAt ? new Date(raw.canceledAt) : null,
|
|
2236
|
+
canceledEffectiveAt: raw.canceledEffectiveAt ? new Date(raw.canceledEffectiveAt) : null,
|
|
2237
|
+
createdAt: new Date(raw.createdAt),
|
|
2238
|
+
updatedAt: new Date(raw.updatedAt)
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/use-business-types.ts
|
|
2243
|
+
import { ref as ref18 } from "vue";
|
|
2244
|
+
var BusinessTypesApiError = class extends Error {
|
|
2245
|
+
constructor(status, body, message) {
|
|
2246
|
+
super(message);
|
|
2247
|
+
this.status = status;
|
|
2248
|
+
this.body = body;
|
|
2249
|
+
this.name = "BusinessTypesApiError";
|
|
2250
|
+
}
|
|
2251
|
+
status;
|
|
2252
|
+
body;
|
|
2253
|
+
};
|
|
2254
|
+
function useBusinessTypes(options) {
|
|
2255
|
+
if (!options?.adminEndpoint) {
|
|
2256
|
+
throw new Error("useBusinessTypes: `adminEndpoint` ist Pflicht.");
|
|
2257
|
+
}
|
|
2258
|
+
if (!options?.projectKey) {
|
|
2259
|
+
throw new Error("useBusinessTypes: `projectKey` ist Pflicht.");
|
|
2260
|
+
}
|
|
2261
|
+
const http = options.http ?? defaultHttpClient();
|
|
2262
|
+
const businessTypes = ref18([]);
|
|
2263
|
+
const loading = ref18(false);
|
|
2264
|
+
const error = ref18(null);
|
|
2265
|
+
const baseUrl = `${options.adminEndpoint}/catalog/business-types`;
|
|
2266
|
+
function authHeaders() {
|
|
2267
|
+
const token = options.getAuthToken?.();
|
|
2268
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2269
|
+
}
|
|
2270
|
+
async function fetchJson(url, init) {
|
|
2271
|
+
const res = await http(url, {
|
|
2272
|
+
method: init?.method ?? "GET",
|
|
2273
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2274
|
+
body: init?.body
|
|
2275
|
+
});
|
|
2276
|
+
if (res.status === 204) return null;
|
|
2277
|
+
const body = await res.json().catch(() => null);
|
|
2278
|
+
if (res.status >= 400) {
|
|
2279
|
+
throw new BusinessTypesApiError(
|
|
2280
|
+
res.status,
|
|
2281
|
+
body,
|
|
2282
|
+
`BusinessTypes-API antwortete mit HTTP ${res.status}`
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
return body;
|
|
2286
|
+
}
|
|
2287
|
+
async function load() {
|
|
2288
|
+
loading.value = true;
|
|
2289
|
+
error.value = null;
|
|
2290
|
+
try {
|
|
2291
|
+
const data = await fetchJson(
|
|
2292
|
+
`${baseUrl}?projectKey=${encodeURIComponent(options.projectKey)}`
|
|
2293
|
+
);
|
|
2294
|
+
businessTypes.value = data ?? [];
|
|
2295
|
+
} catch (err) {
|
|
2296
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2297
|
+
} finally {
|
|
2298
|
+
loading.value = false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
async function create(data) {
|
|
2302
|
+
const created = await fetchJson(baseUrl, {
|
|
2303
|
+
method: "POST",
|
|
2304
|
+
body: JSON.stringify(data)
|
|
2305
|
+
});
|
|
2306
|
+
if (!created) throw new BusinessTypesApiError(0, null, "Create gab keinen Body zur\xFCck");
|
|
2307
|
+
businessTypes.value = [...businessTypes.value, created];
|
|
2308
|
+
return created;
|
|
2309
|
+
}
|
|
2310
|
+
async function update(businessTypeId, data) {
|
|
2311
|
+
const updated = await fetchJson(`${baseUrl}/${businessTypeId}`, {
|
|
2312
|
+
method: "PATCH",
|
|
2313
|
+
body: JSON.stringify(data)
|
|
2314
|
+
});
|
|
2315
|
+
if (!updated) throw new BusinessTypesApiError(0, null, "Update gab keinen Body zur\xFCck");
|
|
2316
|
+
businessTypes.value = businessTypes.value.map(
|
|
2317
|
+
(b) => b.id === businessTypeId ? updated : b
|
|
2318
|
+
);
|
|
2319
|
+
return updated;
|
|
2320
|
+
}
|
|
2321
|
+
async function softDelete(businessTypeId) {
|
|
2322
|
+
await fetchJson(`${baseUrl}/${businessTypeId}`, { method: "DELETE" });
|
|
2323
|
+
businessTypes.value = businessTypes.value.filter((b) => b.id !== businessTypeId);
|
|
2324
|
+
}
|
|
2325
|
+
if (options.autoLoad) {
|
|
2326
|
+
void load();
|
|
2327
|
+
}
|
|
2328
|
+
return { businessTypes, loading, error, load, create, update, softDelete };
|
|
2329
|
+
}
|
|
2330
|
+
function useBusinessTypeVersions(options) {
|
|
2331
|
+
if (!options?.adminEndpoint) {
|
|
2332
|
+
throw new Error("useBusinessTypeVersions: `adminEndpoint` ist Pflicht.");
|
|
2333
|
+
}
|
|
2334
|
+
if (!options?.businessTypeId) {
|
|
2335
|
+
throw new Error("useBusinessTypeVersions: `businessTypeId` ist Pflicht.");
|
|
2336
|
+
}
|
|
2337
|
+
const http = options.http ?? defaultHttpClient();
|
|
2338
|
+
const versions = ref18([]);
|
|
2339
|
+
const loading = ref18(false);
|
|
2340
|
+
const error = ref18(null);
|
|
2341
|
+
const versionsUrl = `${options.adminEndpoint}/catalog/business-types/${options.businessTypeId}/versions`;
|
|
2342
|
+
const versionUrlBase = `${options.adminEndpoint}/catalog/business-type-versions`;
|
|
2343
|
+
function authHeaders() {
|
|
2344
|
+
const token = options.getAuthToken?.();
|
|
2345
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2346
|
+
}
|
|
2347
|
+
async function fetchJson(url, init) {
|
|
2348
|
+
const res = await http(url, {
|
|
2349
|
+
method: init?.method ?? "GET",
|
|
2350
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2351
|
+
body: init?.body
|
|
2352
|
+
});
|
|
2353
|
+
if (res.status === 204) return null;
|
|
2354
|
+
const body = await res.json().catch(() => null);
|
|
2355
|
+
if (res.status >= 400) {
|
|
2356
|
+
throw new BusinessTypesApiError(
|
|
2357
|
+
res.status,
|
|
2358
|
+
body,
|
|
2359
|
+
`BusinessTypeVersions-API antwortete mit HTTP ${res.status}`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
return body;
|
|
2363
|
+
}
|
|
2364
|
+
async function load() {
|
|
2365
|
+
loading.value = true;
|
|
2366
|
+
error.value = null;
|
|
2367
|
+
try {
|
|
2368
|
+
const data = await fetchJson(versionsUrl);
|
|
2369
|
+
versions.value = data ?? [];
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2372
|
+
} finally {
|
|
2373
|
+
loading.value = false;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
async function createDraft(data) {
|
|
2377
|
+
const result = await fetchJson(versionsUrl, {
|
|
2378
|
+
method: "POST",
|
|
2379
|
+
body: JSON.stringify(data)
|
|
2380
|
+
});
|
|
2381
|
+
if (!result) throw new BusinessTypesApiError(0, null, "CreateDraft gab keinen Body zur\xFCck");
|
|
2382
|
+
versions.value = [...versions.value, result.businessTypeVersion];
|
|
2383
|
+
return result;
|
|
2384
|
+
}
|
|
2385
|
+
async function updateDraft(versionId, data) {
|
|
2386
|
+
const result = await fetchJson(
|
|
2387
|
+
`${versionUrlBase}/${versionId}`,
|
|
2388
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
2389
|
+
);
|
|
2390
|
+
if (!result) throw new BusinessTypesApiError(0, null, "UpdateDraft gab keinen Body zur\xFCck");
|
|
2391
|
+
versions.value = versions.value.map(
|
|
2392
|
+
(v) => v.id === versionId ? result.businessTypeVersion : v
|
|
2393
|
+
);
|
|
2394
|
+
return result;
|
|
2395
|
+
}
|
|
2396
|
+
async function publish(versionId, opts = {}) {
|
|
2397
|
+
const result = await fetchJson(
|
|
2398
|
+
`${versionUrlBase}/${versionId}/publish`,
|
|
2399
|
+
{ method: "POST", body: JSON.stringify(opts) }
|
|
2400
|
+
);
|
|
2401
|
+
if (!result) throw new BusinessTypesApiError(0, null, "Publish gab keinen Body zur\xFCck");
|
|
2402
|
+
await load();
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
if (options.autoLoad) {
|
|
2406
|
+
void load();
|
|
2407
|
+
}
|
|
2408
|
+
return { versions, loading, error, load, createDraft, updateDraft, publish };
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// src/use-marketing-projections.ts
|
|
2412
|
+
import { ref as ref19 } from "vue";
|
|
2413
|
+
var MarketingProjectionsApiError = class extends Error {
|
|
2414
|
+
constructor(status, body, message) {
|
|
2415
|
+
super(message);
|
|
2416
|
+
this.status = status;
|
|
2417
|
+
this.body = body;
|
|
2418
|
+
this.name = "MarketingProjectionsApiError";
|
|
2419
|
+
}
|
|
2420
|
+
status;
|
|
2421
|
+
body;
|
|
2422
|
+
};
|
|
2423
|
+
function useMarketingProjections(options) {
|
|
2424
|
+
if (!options?.adminEndpoint) {
|
|
2425
|
+
throw new Error("useMarketingProjections: `adminEndpoint` ist Pflicht.");
|
|
2426
|
+
}
|
|
2427
|
+
if (!options?.filter?.projectKey) {
|
|
2428
|
+
throw new Error("useMarketingProjections: `filter.projectKey` ist Pflicht.");
|
|
2429
|
+
}
|
|
2430
|
+
const http = options.http ?? defaultHttpClient();
|
|
2431
|
+
const projections = ref19([]);
|
|
2432
|
+
const filter = ref19({ ...options.filter });
|
|
2433
|
+
const loading = ref19(false);
|
|
2434
|
+
const error = ref19(null);
|
|
2435
|
+
const baseUrl = `${options.adminEndpoint}/catalog/marketing-projections`;
|
|
2436
|
+
function authHeaders() {
|
|
2437
|
+
const token = options.getAuthToken?.();
|
|
2438
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2439
|
+
}
|
|
2440
|
+
function buildListUrl() {
|
|
2441
|
+
const params = new URLSearchParams();
|
|
2442
|
+
params.set("projectKey", filter.value.projectKey);
|
|
2443
|
+
if (filter.value.targetType) params.set("targetType", filter.value.targetType);
|
|
2444
|
+
if (filter.value.targetVersionId)
|
|
2445
|
+
params.set("targetVersionId", filter.value.targetVersionId);
|
|
2446
|
+
if (filter.value.locale) params.set("locale", filter.value.locale);
|
|
2447
|
+
return `${baseUrl}?${params.toString()}`;
|
|
2448
|
+
}
|
|
2449
|
+
async function fetchJson(url, init) {
|
|
2450
|
+
const res = await http(url, {
|
|
2451
|
+
method: init?.method ?? "GET",
|
|
2452
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2453
|
+
body: init?.body
|
|
2454
|
+
});
|
|
2455
|
+
if (res.status === 204) return null;
|
|
2456
|
+
const body = await res.json().catch(() => null);
|
|
2457
|
+
if (res.status >= 400) {
|
|
2458
|
+
throw new MarketingProjectionsApiError(
|
|
2459
|
+
res.status,
|
|
2460
|
+
body,
|
|
2461
|
+
`MarketingProjections-API antwortete mit HTTP ${res.status}`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
return body;
|
|
2465
|
+
}
|
|
2466
|
+
async function load() {
|
|
2467
|
+
loading.value = true;
|
|
2468
|
+
error.value = null;
|
|
2469
|
+
try {
|
|
2470
|
+
const data = await fetchJson(buildListUrl());
|
|
2471
|
+
projections.value = data ?? [];
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2474
|
+
} finally {
|
|
2475
|
+
loading.value = false;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
async function setFilter(next) {
|
|
2479
|
+
filter.value = { ...next };
|
|
2480
|
+
await load();
|
|
2481
|
+
}
|
|
2482
|
+
async function create(data) {
|
|
2483
|
+
const created = await fetchJson(baseUrl, {
|
|
2484
|
+
method: "POST",
|
|
2485
|
+
body: JSON.stringify(data)
|
|
2486
|
+
});
|
|
2487
|
+
if (!created) {
|
|
2488
|
+
throw new MarketingProjectionsApiError(0, null, "Create gab keinen Body zur\xFCck");
|
|
2489
|
+
}
|
|
2490
|
+
await load();
|
|
2491
|
+
return created;
|
|
2492
|
+
}
|
|
2493
|
+
async function update(id, data) {
|
|
2494
|
+
const updated = await fetchJson(`${baseUrl}/${id}`, {
|
|
2495
|
+
method: "PATCH",
|
|
2496
|
+
body: JSON.stringify(data)
|
|
2497
|
+
});
|
|
2498
|
+
if (!updated) {
|
|
2499
|
+
throw new MarketingProjectionsApiError(0, null, "Update gab keinen Body zur\xFCck");
|
|
2500
|
+
}
|
|
2501
|
+
projections.value = projections.value.map((p) => p.id === id ? updated : p);
|
|
2502
|
+
return updated;
|
|
2503
|
+
}
|
|
2504
|
+
async function remove(id) {
|
|
2505
|
+
await fetchJson(`${baseUrl}/${id}`, { method: "DELETE" });
|
|
2506
|
+
projections.value = projections.value.filter((p) => p.id !== id);
|
|
2507
|
+
}
|
|
2508
|
+
if (options.autoLoad) {
|
|
2509
|
+
void load();
|
|
2510
|
+
}
|
|
2511
|
+
return { projections, filter, loading, error, setFilter, load, create, update, remove };
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/use-promotions.ts
|
|
2515
|
+
import { ref as ref20 } from "vue";
|
|
2516
|
+
var PromotionsApiError = class extends Error {
|
|
2517
|
+
constructor(status, body, message) {
|
|
2518
|
+
super(message);
|
|
2519
|
+
this.status = status;
|
|
2520
|
+
this.body = body;
|
|
2521
|
+
this.name = "PromotionsApiError";
|
|
2522
|
+
}
|
|
2523
|
+
status;
|
|
2524
|
+
body;
|
|
2525
|
+
};
|
|
2526
|
+
function usePromotions(options) {
|
|
2527
|
+
if (!options?.adminEndpoint) {
|
|
2528
|
+
throw new Error("usePromotions: `adminEndpoint` ist Pflicht.");
|
|
2529
|
+
}
|
|
2530
|
+
if (!options?.projectKey) {
|
|
2531
|
+
throw new Error("usePromotions: `projectKey` ist Pflicht.");
|
|
2532
|
+
}
|
|
2533
|
+
const http = options.http ?? defaultHttpClient();
|
|
2534
|
+
const promotions = ref20([]);
|
|
2535
|
+
const loading = ref20(false);
|
|
2536
|
+
const error = ref20(null);
|
|
2537
|
+
const baseUrl = `${options.adminEndpoint}/catalog/promotions`;
|
|
2538
|
+
const pk = encodeURIComponent(options.projectKey);
|
|
2539
|
+
function authHeaders() {
|
|
2540
|
+
const token = options.getAuthToken?.();
|
|
2541
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2542
|
+
}
|
|
2543
|
+
async function fetchJson(url, init) {
|
|
2544
|
+
const res = await http(url, {
|
|
2545
|
+
method: init?.method ?? "GET",
|
|
2546
|
+
headers: { "content-type": "application/json", ...authHeaders(), ...init?.headers },
|
|
2547
|
+
body: init?.body
|
|
2548
|
+
});
|
|
2549
|
+
if (res.status === 204) return null;
|
|
2550
|
+
const body = await res.json().catch(() => null);
|
|
2551
|
+
if (res.status >= 400) {
|
|
2552
|
+
throw new PromotionsApiError(
|
|
2553
|
+
res.status,
|
|
2554
|
+
body,
|
|
2555
|
+
`Promotions-API antwortete mit HTTP ${res.status}`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
return body;
|
|
2559
|
+
}
|
|
2560
|
+
async function load() {
|
|
2561
|
+
loading.value = true;
|
|
2562
|
+
error.value = null;
|
|
2563
|
+
try {
|
|
2564
|
+
const data = await fetchJson(`${baseUrl}?projectKey=${pk}`);
|
|
2565
|
+
promotions.value = data ?? [];
|
|
2566
|
+
} catch (err) {
|
|
2567
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2568
|
+
} finally {
|
|
2569
|
+
loading.value = false;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
async function create(data) {
|
|
2573
|
+
const created = await fetchJson(baseUrl, {
|
|
2574
|
+
method: "POST",
|
|
2575
|
+
body: JSON.stringify(data)
|
|
2576
|
+
});
|
|
2577
|
+
if (!created) throw new PromotionsApiError(0, null, "Create gab keinen Body zur\xFCck");
|
|
2578
|
+
promotions.value = [...promotions.value, created];
|
|
2579
|
+
return created;
|
|
2580
|
+
}
|
|
2581
|
+
async function update(id, data) {
|
|
2582
|
+
const updated = await fetchJson(`${baseUrl}/${id}`, {
|
|
2583
|
+
method: "PATCH",
|
|
2584
|
+
body: JSON.stringify(data)
|
|
2585
|
+
});
|
|
2586
|
+
if (!updated) throw new PromotionsApiError(0, null, "Update gab keinen Body zur\xFCck");
|
|
2587
|
+
promotions.value = promotions.value.map((p) => p.id === id ? updated : p);
|
|
2588
|
+
return updated;
|
|
2589
|
+
}
|
|
2590
|
+
async function remove(id) {
|
|
2591
|
+
await fetchJson(`${baseUrl}/${id}`, { method: "DELETE" });
|
|
2592
|
+
promotions.value = promotions.value.filter((p) => p.id !== id);
|
|
2593
|
+
}
|
|
2594
|
+
if (options.autoLoad) {
|
|
2595
|
+
void load();
|
|
2596
|
+
}
|
|
2597
|
+
return { promotions, loading, error, load, create, update, remove };
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// src/use-plans.ts
|
|
2601
|
+
import { ref as ref21 } from "vue";
|
|
2602
|
+
var PlansApiError = class extends Error {
|
|
2603
|
+
constructor(status, body, message) {
|
|
2604
|
+
super(message);
|
|
2605
|
+
this.status = status;
|
|
2606
|
+
this.body = body;
|
|
2607
|
+
this.name = "PlansApiError";
|
|
2608
|
+
}
|
|
2609
|
+
status;
|
|
2610
|
+
body;
|
|
2611
|
+
};
|
|
2612
|
+
function usePlans(options) {
|
|
2613
|
+
if (!options?.adminEndpoint) {
|
|
2614
|
+
throw new Error(
|
|
2615
|
+
'usePlans: `adminEndpoint` ist Pflicht (z. B. "/api/admin" oder "/api/v1/admin").'
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
if (!options?.projectKey) {
|
|
2619
|
+
throw new Error("usePlans: `projectKey` ist Pflicht.");
|
|
2620
|
+
}
|
|
2621
|
+
const http = options.http ?? defaultHttpClient();
|
|
2622
|
+
const plans = ref21([]);
|
|
2623
|
+
const loading = ref21(false);
|
|
2624
|
+
const error = ref21(null);
|
|
2625
|
+
const tenantCountsByPlanKey = ref21({});
|
|
2626
|
+
const baseUrl = `${options.adminEndpoint}/catalog/plans`;
|
|
2627
|
+
function authHeaders() {
|
|
2628
|
+
const token = options.getAuthToken?.();
|
|
2629
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2630
|
+
}
|
|
2631
|
+
async function fetchJson(url, init) {
|
|
2632
|
+
const res = await http(url, {
|
|
2633
|
+
method: init?.method ?? "GET",
|
|
2634
|
+
headers: {
|
|
2635
|
+
"content-type": "application/json",
|
|
2636
|
+
...authHeaders(),
|
|
2637
|
+
...init?.headers
|
|
2638
|
+
},
|
|
2639
|
+
body: init?.body
|
|
2640
|
+
});
|
|
2641
|
+
if (res.status === 204) return null;
|
|
2642
|
+
const body = await res.json().catch(() => null);
|
|
2643
|
+
if (res.status >= 400) {
|
|
2644
|
+
throw new PlansApiError(
|
|
2645
|
+
res.status,
|
|
2646
|
+
body,
|
|
2647
|
+
`Plans-API antwortete mit HTTP ${res.status}`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
return body;
|
|
2651
|
+
}
|
|
2652
|
+
async function load() {
|
|
2653
|
+
loading.value = true;
|
|
2654
|
+
error.value = null;
|
|
2655
|
+
try {
|
|
2656
|
+
const data = await fetchJson(
|
|
2657
|
+
`${baseUrl}?projectKey=${encodeURIComponent(options.projectKey)}`
|
|
2658
|
+
);
|
|
2659
|
+
plans.value = data ?? [];
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2662
|
+
} finally {
|
|
2663
|
+
loading.value = false;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async function loadTenantCounts() {
|
|
2667
|
+
try {
|
|
2668
|
+
const data = await fetchJson(
|
|
2669
|
+
`${baseUrl}/tenant-counts?projectKey=${encodeURIComponent(options.projectKey)}`
|
|
2670
|
+
);
|
|
2671
|
+
tenantCountsByPlanKey.value = data ?? {};
|
|
2672
|
+
} catch {
|
|
2673
|
+
tenantCountsByPlanKey.value = {};
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
async function create(data) {
|
|
2677
|
+
const created = await fetchJson(baseUrl, {
|
|
2678
|
+
method: "POST",
|
|
2679
|
+
body: JSON.stringify(data)
|
|
2680
|
+
});
|
|
2681
|
+
if (!created) throw new PlansApiError(0, null, "Create gab keinen Body zur\xFCck");
|
|
2682
|
+
plans.value = [...plans.value, created];
|
|
2683
|
+
return created;
|
|
2684
|
+
}
|
|
2685
|
+
async function update(planId, data) {
|
|
2686
|
+
const updated = await fetchJson(`${baseUrl}/${planId}`, {
|
|
2687
|
+
method: "PATCH",
|
|
2688
|
+
body: JSON.stringify(data)
|
|
2689
|
+
});
|
|
2690
|
+
if (!updated) throw new PlansApiError(0, null, "Update gab keinen Body zur\xFCck");
|
|
2691
|
+
plans.value = plans.value.map((p) => p.id === planId ? updated : p);
|
|
2692
|
+
return updated;
|
|
2693
|
+
}
|
|
2694
|
+
async function softDelete(planId) {
|
|
2695
|
+
await fetchJson(`${baseUrl}/${planId}`, { method: "DELETE" });
|
|
2696
|
+
plans.value = plans.value.filter((p) => p.id !== planId);
|
|
2697
|
+
}
|
|
2698
|
+
async function hardDelete(planId) {
|
|
2699
|
+
await fetchJson(`${baseUrl}/${planId}/purge`, { method: "DELETE" });
|
|
2700
|
+
plans.value = plans.value.filter((p) => p.id !== planId);
|
|
2701
|
+
}
|
|
2702
|
+
if (options.autoLoad) {
|
|
2703
|
+
void load();
|
|
2704
|
+
}
|
|
2705
|
+
return {
|
|
2706
|
+
plans,
|
|
2707
|
+
loading,
|
|
2708
|
+
error,
|
|
2709
|
+
tenantCountsByPlanKey,
|
|
2710
|
+
load,
|
|
2711
|
+
loadTenantCounts,
|
|
2712
|
+
create,
|
|
2713
|
+
update,
|
|
2714
|
+
softDelete,
|
|
2715
|
+
hardDelete
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
function usePlanVersions(options) {
|
|
2719
|
+
if (!options?.adminEndpoint) {
|
|
2720
|
+
throw new Error("usePlanVersions: `adminEndpoint` ist Pflicht.");
|
|
2721
|
+
}
|
|
2722
|
+
if (!options?.planId) {
|
|
2723
|
+
throw new Error("usePlanVersions: `planId` ist Pflicht.");
|
|
2724
|
+
}
|
|
2725
|
+
const http = options.http ?? defaultHttpClient();
|
|
2726
|
+
const versions = ref21([]);
|
|
2727
|
+
const loading = ref21(false);
|
|
2728
|
+
const error = ref21(null);
|
|
2729
|
+
const planVersionsUrl = `${options.adminEndpoint}/catalog/plans/${options.planId}/versions`;
|
|
2730
|
+
const versionUrlBase = `${options.adminEndpoint}/catalog/plan-versions`;
|
|
2731
|
+
function authHeaders() {
|
|
2732
|
+
const token = options.getAuthToken?.();
|
|
2733
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2734
|
+
}
|
|
2735
|
+
async function fetchJson(url, init) {
|
|
2736
|
+
const res = await http(url, {
|
|
2737
|
+
method: init?.method ?? "GET",
|
|
2738
|
+
headers: {
|
|
2739
|
+
"content-type": "application/json",
|
|
2740
|
+
...authHeaders(),
|
|
2741
|
+
...init?.headers
|
|
2742
|
+
},
|
|
2743
|
+
body: init?.body
|
|
2744
|
+
});
|
|
2745
|
+
if (res.status === 204) return null;
|
|
2746
|
+
const body = await res.json().catch(() => null);
|
|
2747
|
+
if (res.status >= 400) {
|
|
2748
|
+
throw new PlansApiError(
|
|
2749
|
+
res.status,
|
|
2750
|
+
body,
|
|
2751
|
+
`PlanVersions-API antwortete mit HTTP ${res.status}`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
return body;
|
|
2755
|
+
}
|
|
2756
|
+
async function load() {
|
|
2757
|
+
loading.value = true;
|
|
2758
|
+
error.value = null;
|
|
2759
|
+
try {
|
|
2760
|
+
const data = await fetchJson(planVersionsUrl);
|
|
2761
|
+
versions.value = data ?? [];
|
|
2762
|
+
} catch (err) {
|
|
2763
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2764
|
+
} finally {
|
|
2765
|
+
loading.value = false;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
async function createDraft(data) {
|
|
2769
|
+
const result = await fetchJson(planVersionsUrl, {
|
|
2770
|
+
method: "POST",
|
|
2771
|
+
body: JSON.stringify(data)
|
|
2772
|
+
});
|
|
2773
|
+
if (!result) throw new PlansApiError(0, null, "CreateDraft gab keinen Body zur\xFCck");
|
|
2774
|
+
versions.value = [...versions.value, result.planVersion];
|
|
2775
|
+
return result;
|
|
2776
|
+
}
|
|
2777
|
+
async function updateDraft(versionId, data) {
|
|
2778
|
+
const result = await fetchJson(
|
|
2779
|
+
`${versionUrlBase}/${versionId}`,
|
|
2780
|
+
{ method: "PATCH", body: JSON.stringify(data) }
|
|
2781
|
+
);
|
|
2782
|
+
if (!result) throw new PlansApiError(0, null, "UpdateDraft gab keinen Body zur\xFCck");
|
|
2783
|
+
versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
|
|
2784
|
+
return result;
|
|
2785
|
+
}
|
|
2786
|
+
async function publish(versionId, opts = {}) {
|
|
2787
|
+
const result = await fetchJson(
|
|
2788
|
+
`${versionUrlBase}/${versionId}/publish`,
|
|
2789
|
+
{ method: "POST", body: JSON.stringify(opts) }
|
|
2790
|
+
);
|
|
2791
|
+
if (!result) throw new PlansApiError(0, null, "Publish gab keinen Body zur\xFCck");
|
|
2792
|
+
versions.value = versions.value.map((v) => v.id === versionId ? result.planVersion : v);
|
|
2793
|
+
return result;
|
|
2794
|
+
}
|
|
2795
|
+
async function discardDraft(versionId) {
|
|
2796
|
+
await fetchJson(`${versionUrlBase}/${versionId}`, { method: "DELETE" });
|
|
2797
|
+
versions.value = versions.value.filter((v) => v.id !== versionId);
|
|
2798
|
+
}
|
|
2799
|
+
async function terminateVersion(versionId, endsAt) {
|
|
2800
|
+
const updated = await fetchJson(
|
|
2801
|
+
`${versionUrlBase}/${versionId}/terminate`,
|
|
2802
|
+
{ method: "POST", body: JSON.stringify({ endsAt }) }
|
|
2803
|
+
);
|
|
2804
|
+
if (!updated) throw new PlansApiError(0, null, "Terminate gab keinen Body zur\xFCck");
|
|
2805
|
+
versions.value = versions.value.map((v) => v.id === versionId ? updated : v);
|
|
2806
|
+
return updated;
|
|
2807
|
+
}
|
|
2808
|
+
if (options.autoLoad) {
|
|
2809
|
+
void load();
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
versions,
|
|
2813
|
+
loading,
|
|
2814
|
+
error,
|
|
2815
|
+
load,
|
|
2816
|
+
createDraft,
|
|
2817
|
+
updateDraft,
|
|
2818
|
+
publish,
|
|
2819
|
+
discardDraft,
|
|
2820
|
+
terminateVersion
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// src/use-live-plan-versions.ts
|
|
2825
|
+
import { ref as ref22, watch as watch3 } from "vue";
|
|
2826
|
+
function useLivePlanVersions(options) {
|
|
2827
|
+
if (!options?.adminEndpoint) {
|
|
2828
|
+
throw new Error("useLivePlanVersions: `adminEndpoint` ist Pflicht.");
|
|
2829
|
+
}
|
|
2830
|
+
if (!options?.plans) {
|
|
2831
|
+
throw new Error("useLivePlanVersions: `plans` ist Pflicht.");
|
|
2832
|
+
}
|
|
2833
|
+
const http = options.http ?? defaultHttpClient();
|
|
2834
|
+
const livePlanVersions = ref22({});
|
|
2835
|
+
const loading = ref22(false);
|
|
2836
|
+
const error = ref22(null);
|
|
2837
|
+
function authHeaders() {
|
|
2838
|
+
const token = options.getAuthToken?.();
|
|
2839
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
2840
|
+
}
|
|
2841
|
+
async function loadForPlan(planId) {
|
|
2842
|
+
const res = await http(`${options.adminEndpoint}/catalog/plans/${planId}/versions`, {
|
|
2843
|
+
method: "GET",
|
|
2844
|
+
headers: { "content-type": "application/json", ...authHeaders() }
|
|
2845
|
+
});
|
|
2846
|
+
if (res.status >= 400) {
|
|
2847
|
+
throw new Error(`HTTP ${res.status} beim Laden der Versionen f\xFCr Plan '${planId}'`);
|
|
2848
|
+
}
|
|
2849
|
+
const versions = await res.json().catch(() => null);
|
|
2850
|
+
if (!versions || versions.length === 0) return null;
|
|
2851
|
+
return findLatestLive(versions);
|
|
2852
|
+
}
|
|
2853
|
+
async function refresh() {
|
|
2854
|
+
loading.value = true;
|
|
2855
|
+
error.value = null;
|
|
2856
|
+
try {
|
|
2857
|
+
const entries = await Promise.all(
|
|
2858
|
+
options.plans.value.map(async (plan) => {
|
|
2859
|
+
try {
|
|
2860
|
+
const live = await loadForPlan(plan.id);
|
|
2861
|
+
return [plan.planKey, live];
|
|
2862
|
+
} catch (err) {
|
|
2863
|
+
console.warn(
|
|
2864
|
+
`useLivePlanVersions: Plan '${plan.planKey}' Versions-Load failed`,
|
|
2865
|
+
err
|
|
2866
|
+
);
|
|
2867
|
+
return [plan.planKey, null];
|
|
2868
|
+
}
|
|
2869
|
+
})
|
|
2870
|
+
);
|
|
2871
|
+
livePlanVersions.value = Object.fromEntries(entries);
|
|
2872
|
+
} catch (err) {
|
|
2873
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2874
|
+
} finally {
|
|
2875
|
+
loading.value = false;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
watch3(
|
|
2879
|
+
() => options.plans.value.map((p) => p.id).join(","),
|
|
2880
|
+
() => {
|
|
2881
|
+
if (options.plans.value.length > 0) void refresh();
|
|
2882
|
+
else livePlanVersions.value = {};
|
|
2883
|
+
},
|
|
2884
|
+
{ immediate: true }
|
|
2885
|
+
);
|
|
2886
|
+
return { livePlanVersions, loading, error, refresh };
|
|
2887
|
+
}
|
|
2888
|
+
function findLatestLive(versions) {
|
|
2889
|
+
const candidates = versions.filter((v) => v.publishedAt !== null && v.supersededAt === null);
|
|
2890
|
+
if (candidates.length === 0) return null;
|
|
2891
|
+
return [...candidates].sort((a, b) => {
|
|
2892
|
+
const av = a.validFrom ?? "";
|
|
2893
|
+
const bv = b.validFrom ?? "";
|
|
2894
|
+
if (av === bv) return b.version - a.version;
|
|
2895
|
+
return bv.localeCompare(av);
|
|
2896
|
+
})[0];
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/use-manifest.ts
|
|
2900
|
+
import { ref as ref23 } from "vue";
|
|
2901
|
+
function useManifest(options) {
|
|
2902
|
+
const loader = new ManifestLoader(options);
|
|
2903
|
+
const manifest = ref23(null);
|
|
2904
|
+
const loading = ref23(false);
|
|
2905
|
+
const error = ref23(null);
|
|
2906
|
+
async function load() {
|
|
2907
|
+
loading.value = true;
|
|
2908
|
+
error.value = null;
|
|
2909
|
+
try {
|
|
2910
|
+
manifest.value = await loader.load();
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
2913
|
+
manifest.value = null;
|
|
2914
|
+
} finally {
|
|
2915
|
+
loading.value = false;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
async function reload() {
|
|
2919
|
+
loader.clearCache();
|
|
2920
|
+
manifest.value = null;
|
|
2921
|
+
await load();
|
|
2922
|
+
}
|
|
2923
|
+
function clearCache() {
|
|
2924
|
+
loader.clearCache();
|
|
2925
|
+
manifest.value = null;
|
|
2926
|
+
}
|
|
2927
|
+
return { manifest, loading, error, load, reload, clearCache };
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/use-nav.ts
|
|
2931
|
+
import { computed as computed5 } from "vue";
|
|
2932
|
+
function useNav(manifest, options = {}) {
|
|
2933
|
+
const routes = computed5(() => {
|
|
2934
|
+
if (!manifest.value) return [];
|
|
2935
|
+
return buildRoutes(manifest.value, options);
|
|
2936
|
+
});
|
|
2937
|
+
const sidebar = computed5(() => buildSidebar(routes.value));
|
|
2938
|
+
return { routes, sidebar };
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// src/use-actions.ts
|
|
2942
|
+
import { computed as computed6 } from "vue";
|
|
2943
|
+
function useActions(manifest, actions) {
|
|
2944
|
+
const registry = computed6(() => {
|
|
2945
|
+
if (!manifest.value) return null;
|
|
2946
|
+
return new ActionRegistry(manifest.value, actions);
|
|
2947
|
+
});
|
|
2948
|
+
return { registry };
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
// src/use-tenant-action-flow.ts
|
|
2952
|
+
import { computed as computed7 } from "vue";
|
|
2953
|
+
|
|
2954
|
+
// src/use-super-admin-context.ts
|
|
2955
|
+
import { inject as inject3 } from "vue";
|
|
2956
|
+
function useSuperAdminExtensions() {
|
|
2957
|
+
return inject3(SUPER_ADMIN_EXTENSIONS_KEY, {});
|
|
2958
|
+
}
|
|
2959
|
+
function useSuperAdminActions() {
|
|
2960
|
+
return inject3(SUPER_ADMIN_ACTIONS_KEY, {});
|
|
2961
|
+
}
|
|
2962
|
+
function useSuperAdminBrand() {
|
|
2963
|
+
const brand = inject3(SUPER_ADMIN_BRAND_KEY);
|
|
2964
|
+
if (!brand) {
|
|
2965
|
+
throw new Error(
|
|
2966
|
+
"useSuperAdminBrand(): kein SuperAdmin-Branding im Inject-Scope. Wurde die Komponente innerhalb createSuperAdminApp() gemounted?"
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
return brand;
|
|
2970
|
+
}
|
|
2971
|
+
function useSuperAdminEndpoints() {
|
|
2972
|
+
const endpoints = inject3(SUPER_ADMIN_ENDPOINTS_KEY);
|
|
2973
|
+
if (!endpoints) {
|
|
2974
|
+
throw new Error(
|
|
2975
|
+
"useSuperAdminEndpoints(): keine Endpoints im Inject-Scope. Wurde die Komponente innerhalb createSuperAdminApp() gemounted?"
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
return endpoints;
|
|
2979
|
+
}
|
|
2980
|
+
function useSuperAdminManifest() {
|
|
2981
|
+
const accessor = inject3(SUPER_ADMIN_MANIFEST_KEY, null);
|
|
2982
|
+
return accessor ? accessor() : null;
|
|
2983
|
+
}
|
|
2984
|
+
function useSuperAdminLoginAdapter() {
|
|
2985
|
+
const adapter = inject3(SUPER_ADMIN_LOGIN_ADAPTER_KEY);
|
|
2986
|
+
if (!adapter) {
|
|
2987
|
+
throw new Error(
|
|
2988
|
+
"useSuperAdminLoginAdapter(): kein Login-Adapter registriert. Reiche ihn via createSuperAdminApp({ loginAdapter }) ein."
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
return adapter;
|
|
2992
|
+
}
|
|
2993
|
+
function useSuperAdminHttp() {
|
|
2994
|
+
return inject3(SUPER_ADMIN_HTTP_KEY, defaultHttpClient());
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// src/use-tenant-action-flow.ts
|
|
2998
|
+
function useTenantActionFlow(manifest, providers = {}) {
|
|
2999
|
+
const handlers = useSuperAdminActions();
|
|
3000
|
+
const registry = computed7(() => {
|
|
3001
|
+
if (!manifest.value) return null;
|
|
3002
|
+
return new ActionRegistry(manifest.value, handlers);
|
|
3003
|
+
});
|
|
3004
|
+
const orphanedDefs = computed7(
|
|
3005
|
+
() => registry.value ? registry.value.listOrphanedDefs() : []
|
|
3006
|
+
);
|
|
3007
|
+
const availableActions = computed7(() => {
|
|
3008
|
+
const m = manifest.value;
|
|
3009
|
+
const reg = registry.value;
|
|
3010
|
+
if (!m || !reg) return [];
|
|
3011
|
+
const defs = m.tenants?.actions ?? [];
|
|
3012
|
+
const caps = m.capabilities ?? {};
|
|
3013
|
+
const orphans = new Set(reg.listOrphanedDefs());
|
|
3014
|
+
return defs.filter((def) => passesStaticGates(def, caps, orphans));
|
|
3015
|
+
});
|
|
3016
|
+
function actionsForRow(row) {
|
|
3017
|
+
const reg = registry.value;
|
|
3018
|
+
if (!reg) return [];
|
|
3019
|
+
return availableActions.value.filter((def) => !providers.visibleForRow || providers.visibleForRow(def, row)).map((def) => ({
|
|
3020
|
+
def,
|
|
3021
|
+
invoke: (r) => runFlow(def, r ?? row, reg, providers)
|
|
3022
|
+
}));
|
|
3023
|
+
}
|
|
3024
|
+
return { registry, availableActions, actionsForRow, orphanedDefs };
|
|
3025
|
+
}
|
|
3026
|
+
function passesStaticGates(def, capabilities, orphanedKeys) {
|
|
3027
|
+
if (def.requiredCapability && capabilities[def.requiredCapability] === false) {
|
|
3028
|
+
return false;
|
|
3029
|
+
}
|
|
3030
|
+
if (orphanedKeys.has(def.actionKey)) {
|
|
3031
|
+
return false;
|
|
3032
|
+
}
|
|
3033
|
+
return true;
|
|
3034
|
+
}
|
|
3035
|
+
async function runFlow(def, row, registry, providers) {
|
|
3036
|
+
let reason = null;
|
|
3037
|
+
let extras = {};
|
|
3038
|
+
if (def.confirmType && def.confirmType !== "none") {
|
|
3039
|
+
if (!providers.confirm) {
|
|
3040
|
+
throw new Error(
|
|
3041
|
+
`useTenantActionFlow: Action "${def.id}" verlangt confirmType="${def.confirmType}", aber kein \`confirm\`-Provider \xFCbergeben.`
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
const c = await providers.confirm(def, { row });
|
|
3045
|
+
if (!c.ok) return void 0;
|
|
3046
|
+
reason = c.reason ?? null;
|
|
3047
|
+
if (c.extras) extras = c.extras;
|
|
3048
|
+
}
|
|
3049
|
+
let mfaCode = null;
|
|
3050
|
+
if (def.requiresMfa) {
|
|
3051
|
+
if (!providers.mfa) {
|
|
3052
|
+
throw new Error(
|
|
3053
|
+
`useTenantActionFlow: Action "${def.id}" verlangt MFA, aber kein \`mfa\`-Provider \xFCbergeben.`
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
mfaCode = await providers.mfa(def, { row });
|
|
3057
|
+
if (mfaCode === null) return void 0;
|
|
3058
|
+
}
|
|
3059
|
+
let resolved;
|
|
3060
|
+
try {
|
|
3061
|
+
resolved = registry.get(def.actionKey);
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
providers.notify?.("negative", err instanceof Error ? err.message : String(err));
|
|
3064
|
+
throw err;
|
|
3065
|
+
}
|
|
3066
|
+
try {
|
|
3067
|
+
const input = { row, mfaCode, reason, extras };
|
|
3068
|
+
const result = await resolved.handler(input);
|
|
3069
|
+
providers.notify?.("positive", `${def.label}: erfolgreich.`);
|
|
3070
|
+
providers.onSuccess?.(def, { row });
|
|
3071
|
+
return result;
|
|
3072
|
+
} catch (err) {
|
|
3073
|
+
const message = err?.response?.data?.message ?? (err instanceof Error ? err.message : String(err));
|
|
3074
|
+
providers.notify?.("negative", message);
|
|
3075
|
+
throw err;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// src/use-platform-tenant-actions.ts
|
|
3080
|
+
import { computed as computed8, ref as ref24 } from "vue";
|
|
3081
|
+
var DEFAULT_VISIBLE_FOR_ROW = (def, row) => {
|
|
3082
|
+
if (def.actionKey === "tenants.suspend") return row.isActive;
|
|
3083
|
+
if (def.actionKey === "tenants.reactivate") return !row.isActive;
|
|
3084
|
+
return true;
|
|
3085
|
+
};
|
|
3086
|
+
var DEFAULT_MFA_DESCRIPTION = (def, row) => `${def.label} \u2014 Tenant \u201E${row.name}". TOTP-Code aus Authenticator eingeben.`;
|
|
3087
|
+
var DEFAULT_ICON_FOR_ACTION_KEY = (actionKey) => {
|
|
3088
|
+
if (actionKey.endsWith(".suspend")) return "block";
|
|
3089
|
+
if (actionKey.endsWith(".reactivate")) return "play_arrow";
|
|
3090
|
+
if (actionKey.endsWith(".grant")) return "star";
|
|
3091
|
+
if (actionKey.endsWith(".revoke")) return "star_outline";
|
|
3092
|
+
if (actionKey.endsWith(".impersonate")) return "switch_account";
|
|
3093
|
+
if (actionKey.endsWith(".export")) return "download";
|
|
3094
|
+
return "bolt";
|
|
3095
|
+
};
|
|
3096
|
+
var DEFAULT_TONE_FOR_ACTION_KEY = (actionKey) => {
|
|
3097
|
+
if (actionKey.endsWith(".suspend") || actionKey.endsWith(".revoke")) return "negative";
|
|
3098
|
+
if (actionKey.endsWith(".reactivate") || actionKey.endsWith(".grant")) return "positive";
|
|
3099
|
+
return "primary";
|
|
3100
|
+
};
|
|
3101
|
+
function defaultIconForActionKey(actionKey) {
|
|
3102
|
+
return DEFAULT_ICON_FOR_ACTION_KEY(actionKey);
|
|
3103
|
+
}
|
|
3104
|
+
function defaultToneForActionKey(actionKey) {
|
|
3105
|
+
return DEFAULT_TONE_FOR_ACTION_KEY(actionKey);
|
|
3106
|
+
}
|
|
3107
|
+
function usePlatformTenantActions(options) {
|
|
3108
|
+
const mfaDescription = options.mfaDescription ?? DEFAULT_MFA_DESCRIPTION;
|
|
3109
|
+
const iconForActionKey = options.iconForActionKey ?? DEFAULT_ICON_FOR_ACTION_KEY;
|
|
3110
|
+
const toneForActionKey = options.toneForActionKey ?? DEFAULT_TONE_FOR_ACTION_KEY;
|
|
3111
|
+
const visibleForRow = options.visibleForRow ?? DEFAULT_VISIBLE_FOR_ROW;
|
|
3112
|
+
const mfa = ref24({ show: false, description: "", error: "" });
|
|
3113
|
+
let pendingMfaResolve = null;
|
|
3114
|
+
function showMfaDialog(def, ctx) {
|
|
3115
|
+
return new Promise((resolve) => {
|
|
3116
|
+
mfa.value = {
|
|
3117
|
+
show: true,
|
|
3118
|
+
description: mfaDescription(def, ctx.row),
|
|
3119
|
+
error: ""
|
|
3120
|
+
};
|
|
3121
|
+
pendingMfaResolve = (code) => {
|
|
3122
|
+
pendingMfaResolve = null;
|
|
3123
|
+
resolve(code);
|
|
3124
|
+
};
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
function onMfaConfirm(code) {
|
|
3128
|
+
pendingMfaResolve?.(code);
|
|
3129
|
+
mfa.value.show = false;
|
|
3130
|
+
}
|
|
3131
|
+
function onMfaDialogVisibility(open) {
|
|
3132
|
+
mfa.value.show = open;
|
|
3133
|
+
if (!open && pendingMfaResolve) {
|
|
3134
|
+
pendingMfaResolve(null);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const confirmDialog = ref24({
|
|
3138
|
+
show: false,
|
|
3139
|
+
def: null,
|
|
3140
|
+
row: null
|
|
3141
|
+
});
|
|
3142
|
+
let pendingConfirmResolve = null;
|
|
3143
|
+
function showConfirmDialog(def, ctx) {
|
|
3144
|
+
return new Promise((resolve) => {
|
|
3145
|
+
confirmDialog.value = { show: true, def, row: ctx.row };
|
|
3146
|
+
pendingConfirmResolve = (result) => {
|
|
3147
|
+
pendingConfirmResolve = null;
|
|
3148
|
+
resolve(result);
|
|
3149
|
+
};
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
function onConfirmSubmit(payload) {
|
|
3153
|
+
pendingConfirmResolve?.({
|
|
3154
|
+
ok: true,
|
|
3155
|
+
reason: payload.reason,
|
|
3156
|
+
extras: payload.extras
|
|
3157
|
+
});
|
|
3158
|
+
confirmDialog.value.show = false;
|
|
3159
|
+
}
|
|
3160
|
+
function onConfirmCancel() {
|
|
3161
|
+
pendingConfirmResolve?.({ ok: false });
|
|
3162
|
+
confirmDialog.value.show = false;
|
|
3163
|
+
}
|
|
3164
|
+
function onConfirmDialogVisibility(open) {
|
|
3165
|
+
confirmDialog.value.show = open;
|
|
3166
|
+
if (!open && pendingConfirmResolve) {
|
|
3167
|
+
pendingConfirmResolve({ ok: false });
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
const flow = useTenantActionFlow(options.manifest, {
|
|
3171
|
+
confirm: showConfirmDialog,
|
|
3172
|
+
mfa: showMfaDialog,
|
|
3173
|
+
notify: options.notify,
|
|
3174
|
+
onSuccess: () => {
|
|
3175
|
+
void options.onSuccess();
|
|
3176
|
+
},
|
|
3177
|
+
visibleForRow
|
|
3178
|
+
});
|
|
3179
|
+
const manifestActions = computed8(
|
|
3180
|
+
() => flow.availableActions.value.map((def) => ({
|
|
3181
|
+
id: def.id,
|
|
3182
|
+
label: def.label,
|
|
3183
|
+
icon: iconForActionKey(def.actionKey),
|
|
3184
|
+
tone: toneForActionKey(def.actionKey),
|
|
3185
|
+
actionKey: def.actionKey,
|
|
3186
|
+
condition: (row) => flow.actionsForRow(row).some((a) => a.def.actionKey === def.actionKey),
|
|
3187
|
+
handler: (row) => {
|
|
3188
|
+
const resolved = flow.actionsForRow(row).find((a) => a.def.actionKey === def.actionKey);
|
|
3189
|
+
void resolved?.invoke(row);
|
|
3190
|
+
}
|
|
3191
|
+
}))
|
|
3192
|
+
);
|
|
3193
|
+
const realOrphans = computed8(() => {
|
|
3194
|
+
const caps = options.manifest.value?.capabilities ?? {};
|
|
3195
|
+
const defs = options.manifest.value?.tenants?.actions ?? [];
|
|
3196
|
+
const defByActionKey = new Map(defs.map((d) => [d.actionKey, d]));
|
|
3197
|
+
return flow.orphanedDefs.value.filter((actionKey) => {
|
|
3198
|
+
const def = defByActionKey.get(actionKey);
|
|
3199
|
+
const requiredCap = def?.requiredCapability;
|
|
3200
|
+
if (!requiredCap) return true;
|
|
3201
|
+
return caps[requiredCap] === true;
|
|
3202
|
+
});
|
|
3203
|
+
});
|
|
3204
|
+
return {
|
|
3205
|
+
manifestActions,
|
|
3206
|
+
realOrphans,
|
|
3207
|
+
mfa,
|
|
3208
|
+
onMfaConfirm,
|
|
3209
|
+
onMfaDialogVisibility,
|
|
3210
|
+
confirmDialog,
|
|
3211
|
+
onConfirmSubmit,
|
|
3212
|
+
onConfirmCancel,
|
|
3213
|
+
onConfirmDialogVisibility
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// src/use-batch-columns.ts
|
|
3218
|
+
import { ref as ref25, watch as watch4 } from "vue";
|
|
3219
|
+
function useBatchColumns(manifest, tenantIds, options = {}) {
|
|
3220
|
+
const fetcher = new BatchColumnFetcher(options);
|
|
3221
|
+
const data = ref25({});
|
|
3222
|
+
const loading = ref25(false);
|
|
3223
|
+
const error = ref25(null);
|
|
3224
|
+
async function load() {
|
|
3225
|
+
if (!manifest.value || tenantIds.value.length === 0) {
|
|
3226
|
+
data.value = {};
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
loading.value = true;
|
|
3230
|
+
error.value = null;
|
|
3231
|
+
try {
|
|
3232
|
+
data.value = await fetcher.fetchAll(manifest.value, tenantIds.value);
|
|
3233
|
+
} catch (err) {
|
|
3234
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
3235
|
+
data.value = {};
|
|
3236
|
+
} finally {
|
|
3237
|
+
loading.value = false;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
watch4([manifest, tenantIds], () => {
|
|
3241
|
+
void load();
|
|
3242
|
+
});
|
|
3243
|
+
return { data, loading, error, reload: load };
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// src/plan-versions-catalog.ts
|
|
3247
|
+
var dec = (s) => Number(s);
|
|
3248
|
+
function extractQuotas(p) {
|
|
3249
|
+
if (p.quotas && Object.keys(p.quotas).length > 0) {
|
|
3250
|
+
return { ...p.quotas };
|
|
3251
|
+
}
|
|
3252
|
+
const out = {};
|
|
3253
|
+
if (typeof p.maxUsers === "number") out.users = p.maxUsers;
|
|
3254
|
+
if (typeof p.maxVehicles === "number") out.vehicles = p.maxVehicles;
|
|
3255
|
+
if (typeof p.maxStorageGb === "number") out.storageGb = p.maxStorageGb;
|
|
3256
|
+
return out;
|
|
3257
|
+
}
|
|
3258
|
+
var sortByPublishedDesc = (rows) => [...rows].sort((a, b) => {
|
|
3259
|
+
const at = a.publishedAt ? Date.parse(a.publishedAt) : Number.POSITIVE_INFINITY;
|
|
3260
|
+
const bt = b.publishedAt ? Date.parse(b.publishedAt) : Number.POSITIVE_INFINITY;
|
|
3261
|
+
if (at !== bt) return bt - at;
|
|
3262
|
+
return b.version - a.version;
|
|
3263
|
+
});
|
|
3264
|
+
function findLiveAt(rows, asOfMs) {
|
|
3265
|
+
const candidates = rows.filter((r) => {
|
|
3266
|
+
if (r.publishedAt === null) return false;
|
|
3267
|
+
if (Date.parse(r.publishedAt) > asOfMs) return false;
|
|
3268
|
+
if (r.supersededAt !== null && Date.parse(r.supersededAt) <= asOfMs) return false;
|
|
3269
|
+
return true;
|
|
3270
|
+
});
|
|
3271
|
+
if (candidates.length === 0) return null;
|
|
3272
|
+
return [...candidates].sort((a, b) => b.version - a.version)[0] ?? null;
|
|
3273
|
+
}
|
|
3274
|
+
function findCurrentLive(rows) {
|
|
3275
|
+
return rows.find((r) => r.publishedAt !== null && r.supersededAt === null) ?? null;
|
|
3276
|
+
}
|
|
3277
|
+
function findDraft(rows) {
|
|
3278
|
+
return rows.find((r) => r.publishedAt === null) ?? null;
|
|
3279
|
+
}
|
|
3280
|
+
function groupBy(rows, keyOf) {
|
|
3281
|
+
const out = /* @__PURE__ */ new Map();
|
|
3282
|
+
for (const r of rows) {
|
|
3283
|
+
const k = keyOf(r);
|
|
3284
|
+
const list = out.get(k) ?? [];
|
|
3285
|
+
list.push(r);
|
|
3286
|
+
out.set(k, list);
|
|
3287
|
+
}
|
|
3288
|
+
return out;
|
|
3289
|
+
}
|
|
3290
|
+
function resolvePlan(source, liveBase) {
|
|
3291
|
+
const quotas = extractQuotas(source);
|
|
3292
|
+
return {
|
|
3293
|
+
source,
|
|
3294
|
+
liveBase,
|
|
3295
|
+
isDraft: source.publishedAt === null,
|
|
3296
|
+
planId: source.planId,
|
|
3297
|
+
features: source.features,
|
|
3298
|
+
quotas,
|
|
3299
|
+
monthlyNet: dec(source.monthlyNet),
|
|
3300
|
+
yearlyNet: dec(source.yearlyNet),
|
|
3301
|
+
marketed: source.marketed,
|
|
3302
|
+
version: source.version,
|
|
3303
|
+
maxUsers: quotas.users,
|
|
3304
|
+
maxVehicles: quotas.vehicles,
|
|
3305
|
+
maxStorageGb: quotas.storageGb
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
function dateLabelDe(iso) {
|
|
3309
|
+
return new Date(iso).toLocaleDateString("de-DE", {
|
|
3310
|
+
day: "2-digit",
|
|
3311
|
+
month: "short",
|
|
3312
|
+
year: "numeric"
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
function buildDraftsSnapshot(data, sortPlansFn) {
|
|
3316
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
3317
|
+
const plans = [];
|
|
3318
|
+
for (const [, rows] of planByKey) {
|
|
3319
|
+
const draft = findDraft(rows);
|
|
3320
|
+
const live = findCurrentLive(rows);
|
|
3321
|
+
if (draft) plans.push(resolvePlan(draft, live));
|
|
3322
|
+
else if (live) plans.push(resolvePlan(live, null));
|
|
3323
|
+
}
|
|
3324
|
+
const draftCount = plans.filter((p) => p.isDraft).length;
|
|
3325
|
+
return {
|
|
3326
|
+
id: "drafts",
|
|
3327
|
+
kind: "drafts",
|
|
3328
|
+
status: "DRAFT",
|
|
3329
|
+
label: "Arbeitsstand",
|
|
3330
|
+
title: draftCount === 0 ? "Keine offenen Drafts" : `${draftCount} offene${draftCount === 1 ? "r" : ""} Draft${draftCount === 1 ? "" : "s"} bereit f\xFCr Publish`,
|
|
3331
|
+
description: draftCount === 0 ? "Lege einen Draft an, um Limits, Preise oder Features zu \xE4ndern. Drafts sind ohne MFA editierbar und werden erst beim Publish wirksam." : "Diese Tabelle zeigt, wie der Catalog nach Publish aller Drafts aussehen w\xFCrde. Bestehende Mandanten bleiben auf der vorherigen Version, bis sie aktiv migriert werden.",
|
|
3332
|
+
asOf: null,
|
|
3333
|
+
createdAt: null,
|
|
3334
|
+
publishedAt: null,
|
|
3335
|
+
authorEmail: null,
|
|
3336
|
+
plans: sortPlansFn(plans),
|
|
3337
|
+
draftCount,
|
|
3338
|
+
regressionCount: 0
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
function buildActiveSnapshot(data, sortPlansFn) {
|
|
3342
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
3343
|
+
const plans = [];
|
|
3344
|
+
for (const [, rows] of planByKey) {
|
|
3345
|
+
const live = findCurrentLive(rows);
|
|
3346
|
+
if (live) plans.push(resolvePlan(live, null));
|
|
3347
|
+
}
|
|
3348
|
+
if (plans.length === 0) return null;
|
|
3349
|
+
const publishedDates = plans.map((p) => p.source.publishedAt).filter((t) => t !== null).sort();
|
|
3350
|
+
const latestPublished = publishedDates[publishedDates.length - 1];
|
|
3351
|
+
return {
|
|
3352
|
+
id: "active",
|
|
3353
|
+
kind: "active",
|
|
3354
|
+
status: "ACTIVE",
|
|
3355
|
+
label: "Aktuell",
|
|
3356
|
+
title: "Live-Catalog",
|
|
3357
|
+
description: "Diese Plan-Versionen sind aktuell f\xFCr neue Onboardings und f\xFCr Renewals nach Stichtag aktiv.",
|
|
3358
|
+
asOf: latestPublished ?? null,
|
|
3359
|
+
createdAt: null,
|
|
3360
|
+
publishedAt: latestPublished ?? null,
|
|
3361
|
+
authorEmail: null,
|
|
3362
|
+
plans: sortPlansFn(plans),
|
|
3363
|
+
draftCount: 0,
|
|
3364
|
+
regressionCount: 0
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
function buildHistoricalSnapshots(data, sortPlansFn) {
|
|
3368
|
+
const allPublishedAt = /* @__PURE__ */ new Set();
|
|
3369
|
+
for (const r of data.planVersions) if (r.publishedAt) allPublishedAt.add(r.publishedAt);
|
|
3370
|
+
if (allPublishedAt.size === 0) return [];
|
|
3371
|
+
const planByKey = groupBy(data.planVersions, (r) => r.planId);
|
|
3372
|
+
const liveTimestamps = /* @__PURE__ */ new Set();
|
|
3373
|
+
for (const rows of planByKey.values()) {
|
|
3374
|
+
const live = findCurrentLive(rows);
|
|
3375
|
+
if (live?.publishedAt) liveTimestamps.add(live.publishedAt);
|
|
3376
|
+
}
|
|
3377
|
+
const liveSorted = [...liveTimestamps].sort();
|
|
3378
|
+
const latestLive = liveSorted[liveSorted.length - 1];
|
|
3379
|
+
const sortedDesc = [...allPublishedAt].sort((a, b) => Date.parse(b) - Date.parse(a));
|
|
3380
|
+
const out = [];
|
|
3381
|
+
for (const ts of sortedDesc) {
|
|
3382
|
+
if (latestLive && Date.parse(ts) >= Date.parse(latestLive)) continue;
|
|
3383
|
+
const tMs = Date.parse(ts) + 1;
|
|
3384
|
+
const plans = [];
|
|
3385
|
+
for (const [, rows] of planByKey) {
|
|
3386
|
+
const live = findLiveAt(rows, tMs);
|
|
3387
|
+
if (live) plans.push(resolvePlan(live, null));
|
|
3388
|
+
}
|
|
3389
|
+
if (plans.length === 0) continue;
|
|
3390
|
+
const publishedAtThisEvent = data.planVersions.filter((r) => r.publishedAt === ts);
|
|
3391
|
+
const regressionCount = publishedAtThisEvent.filter((r) => !r.nonRegressive).length;
|
|
3392
|
+
const author = publishedAtThisEvent[0]?.publishedByUserId ?? null;
|
|
3393
|
+
out.push({
|
|
3394
|
+
id: `hist-${ts}`,
|
|
3395
|
+
kind: "historical",
|
|
3396
|
+
status: "ARCHIVED",
|
|
3397
|
+
label: dateLabelDe(ts),
|
|
3398
|
+
title: summarizeEvent(publishedAtThisEvent),
|
|
3399
|
+
description: detailEvent(publishedAtThisEvent),
|
|
3400
|
+
asOf: ts,
|
|
3401
|
+
createdAt: ts,
|
|
3402
|
+
publishedAt: ts,
|
|
3403
|
+
authorEmail: author,
|
|
3404
|
+
plans: sortPlansFn(plans),
|
|
3405
|
+
draftCount: 0,
|
|
3406
|
+
regressionCount
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
return out;
|
|
3410
|
+
}
|
|
3411
|
+
function summarizeEvent(rows) {
|
|
3412
|
+
if (rows.length === 0) return "Publish-Event";
|
|
3413
|
+
if (rows.length === 1) {
|
|
3414
|
+
const r = rows[0];
|
|
3415
|
+
return `${r.planId} v${r.version} publiziert`;
|
|
3416
|
+
}
|
|
3417
|
+
return `${rows.length} Versionen publiziert`;
|
|
3418
|
+
}
|
|
3419
|
+
function detailEvent(rows) {
|
|
3420
|
+
if (rows.length === 0) return "";
|
|
3421
|
+
const note = rows[0]?.changeNote ?? "";
|
|
3422
|
+
return note.length > 0 ? note : "Snapshot rekonstruiert aus per-Entity-Versionen.";
|
|
3423
|
+
}
|
|
3424
|
+
function sortPlansBy(order) {
|
|
3425
|
+
if (!order || order.length === 0) {
|
|
3426
|
+
return (rows) => [...rows].sort((a, b) => a.planId.localeCompare(b.planId));
|
|
3427
|
+
}
|
|
3428
|
+
const idx = {};
|
|
3429
|
+
for (let i = 0; i < order.length; i++) idx[order[i]] = i;
|
|
3430
|
+
return (rows) => [...rows].sort((a, b) => {
|
|
3431
|
+
const ai = idx[a.planId] ?? Number.POSITIVE_INFINITY;
|
|
3432
|
+
const bi = idx[b.planId] ?? Number.POSITIVE_INFINITY;
|
|
3433
|
+
if (ai !== bi) return ai - bi;
|
|
3434
|
+
return a.planId.localeCompare(b.planId);
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
function buildSnapshots(data, options = {}) {
|
|
3438
|
+
const sortPlansFn = sortPlansBy(options.planSortOrder);
|
|
3439
|
+
const drafts = buildDraftsSnapshot(data, sortPlansFn);
|
|
3440
|
+
const active = buildActiveSnapshot(data, sortPlansFn);
|
|
3441
|
+
const historical = buildHistoricalSnapshots(data, sortPlansFn);
|
|
3442
|
+
const out = [drafts];
|
|
3443
|
+
if (active) out.push(active);
|
|
3444
|
+
out.push(...historical);
|
|
3445
|
+
return out;
|
|
3446
|
+
}
|
|
3447
|
+
function listOpenDrafts(data) {
|
|
3448
|
+
return {
|
|
3449
|
+
plans: sortByPublishedDesc(data.planVersions).filter((r) => r.publishedAt === null)
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// src/components/dialogs/types.ts
|
|
3454
|
+
var DEFAULT_PILOT_COPY = {
|
|
3455
|
+
tenantSubtitle: "Stammdaten des Mandanten",
|
|
3456
|
+
tenantNameLabel: "Name des Mandanten",
|
|
3457
|
+
tenantNamePlaceholder: "z. B. Muster GmbH",
|
|
3458
|
+
slugPlaceholder: "muster-mandant",
|
|
3459
|
+
adminEmailPlaceholder: "admin@example.com",
|
|
3460
|
+
notePlaceholder: "z. B. Sales-Pilot \xB7 Demo-Zugang"
|
|
3461
|
+
};
|
|
3462
|
+
|
|
3463
|
+
// src/components/bundle-editor/catalog-i18n.ts
|
|
3464
|
+
var CATALOG_DEFAULT_LOCALE = "de";
|
|
3465
|
+
function localized(i18n, locale, field) {
|
|
3466
|
+
if (locale === CATALOG_DEFAULT_LOCALE) return void 0;
|
|
3467
|
+
const value = i18n?.[locale]?.[field];
|
|
3468
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
3469
|
+
}
|
|
3470
|
+
function resolveLabel(baseLabel, i18n, locale) {
|
|
3471
|
+
return localized(i18n, locale, "label") ?? (baseLabel || void 0);
|
|
3472
|
+
}
|
|
3473
|
+
function buildFeatureRegistry(featureCatalog, locale) {
|
|
3474
|
+
const registry = {};
|
|
3475
|
+
for (const feature of featureCatalog) {
|
|
3476
|
+
const label = resolveLabel(feature.label, feature.i18n, locale);
|
|
3477
|
+
if (label) registry[feature.featureKey] = { label, core: feature.core };
|
|
3478
|
+
}
|
|
3479
|
+
return registry;
|
|
3480
|
+
}
|
|
3481
|
+
function buildQuotaRegistry(quotaCatalog, locale) {
|
|
3482
|
+
const registry = {};
|
|
3483
|
+
for (const quota of quotaCatalog) {
|
|
3484
|
+
registry[quota.quotaKey] = {
|
|
3485
|
+
label: resolveLabel(quota.label, quota.i18n, locale),
|
|
3486
|
+
unit: localized(quota.i18n, locale, "unit") ?? (quota.unit || void 0)
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
return registry;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// src/platform-loaders.ts
|
|
3493
|
+
function resolveEndpoints(endpoints) {
|
|
3494
|
+
return {
|
|
3495
|
+
apiBase: endpoints.apiBase,
|
|
3496
|
+
publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
|
|
3497
|
+
manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
function createPlatformLoaders(options) {
|
|
3501
|
+
const resolved = resolveEndpoints(options.endpoints);
|
|
3502
|
+
const bootLoader = new BootLoader({
|
|
3503
|
+
endpoint: resolved.publicBootEndpoint,
|
|
3504
|
+
http: options.http
|
|
3505
|
+
});
|
|
3506
|
+
const manifestLoader = new ManifestLoader({
|
|
3507
|
+
endpoint: resolved.manifestEndpoint,
|
|
3508
|
+
http: options.http,
|
|
3509
|
+
storage: options.storage,
|
|
3510
|
+
storageKeyPrefix: options.storageKeyPrefix,
|
|
3511
|
+
getAuthToken: options.getAuthToken
|
|
3512
|
+
});
|
|
3513
|
+
return { bootLoader, manifestLoader };
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// src/manifest-store-factory.ts
|
|
3517
|
+
import { defineStore } from "pinia";
|
|
3518
|
+
import { ref as ref26 } from "vue";
|
|
3519
|
+
function createManifestStore(options) {
|
|
3520
|
+
return defineStore(options.id ?? "admin-manifest", () => {
|
|
3521
|
+
const manifest = ref26(null);
|
|
3522
|
+
const loading = ref26(false);
|
|
3523
|
+
const error = ref26(null);
|
|
3524
|
+
const loaded = ref26(false);
|
|
3525
|
+
let inflight = null;
|
|
3526
|
+
async function load() {
|
|
3527
|
+
loading.value = true;
|
|
3528
|
+
error.value = null;
|
|
3529
|
+
try {
|
|
3530
|
+
manifest.value = await options.loader.load();
|
|
3531
|
+
loaded.value = true;
|
|
3532
|
+
} catch (err) {
|
|
3533
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
3534
|
+
error.value = wrapped;
|
|
3535
|
+
manifest.value = null;
|
|
3536
|
+
loaded.value = false;
|
|
3537
|
+
throw wrapped;
|
|
3538
|
+
} finally {
|
|
3539
|
+
loading.value = false;
|
|
3540
|
+
inflight = null;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
function ensureLoaded() {
|
|
3544
|
+
if (loaded.value) return Promise.resolve();
|
|
3545
|
+
if (inflight) return inflight;
|
|
3546
|
+
inflight = load();
|
|
3547
|
+
return inflight;
|
|
3548
|
+
}
|
|
3549
|
+
function clearCache() {
|
|
3550
|
+
options.loader.clearCache();
|
|
3551
|
+
manifest.value = null;
|
|
3552
|
+
loaded.value = false;
|
|
3553
|
+
inflight = null;
|
|
3554
|
+
}
|
|
3555
|
+
async function reload() {
|
|
3556
|
+
clearCache();
|
|
3557
|
+
await ensureLoaded();
|
|
3558
|
+
}
|
|
3559
|
+
return { manifest, loading, error, loaded, ensureLoaded, clearCache, reload };
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
export {
|
|
3563
|
+
ADMIN_UI_VERSION,
|
|
3564
|
+
ActionDefNotInManifestError,
|
|
3565
|
+
ActionRegistry,
|
|
3566
|
+
BatchColumnDriftError,
|
|
3567
|
+
BatchColumnFetcher,
|
|
3568
|
+
BootLoadError,
|
|
3569
|
+
BootLoader,
|
|
3570
|
+
BundlesApiError,
|
|
3571
|
+
BusinessTypesApiError,
|
|
3572
|
+
CATALOG_DEFAULT_LOCALE,
|
|
3573
|
+
CatalogEntriesApiError,
|
|
3574
|
+
DEFAULT_I18N_DE,
|
|
3575
|
+
DEFAULT_PILOT_COPY,
|
|
3576
|
+
DEFAULT_SECTION_ORDER,
|
|
3577
|
+
DEFAULT_STANDARD_PAGE_ROUTES,
|
|
3578
|
+
DEFAULT_YEARLY_FACTOR,
|
|
3579
|
+
DiscoveryLoadError,
|
|
3580
|
+
ENTITLEMENT_INJECTION_KEY,
|
|
3581
|
+
HttpJsonError,
|
|
3582
|
+
ManifestLoadError,
|
|
3583
|
+
ManifestLoader,
|
|
3584
|
+
MarketingProjectionsApiError,
|
|
3585
|
+
MissingHandlerError,
|
|
3586
|
+
PlannedOnlyFeatureError,
|
|
3587
|
+
PlansApiError,
|
|
3588
|
+
ProjectPageHost,
|
|
3589
|
+
PromotionsApiError,
|
|
3590
|
+
SUPER_ADMIN_ACTIONS_KEY,
|
|
3591
|
+
SUPER_ADMIN_BRAND_KEY,
|
|
3592
|
+
SUPER_ADMIN_ENDPOINTS_KEY,
|
|
3593
|
+
SUPER_ADMIN_EXTENSIONS_KEY,
|
|
3594
|
+
SUPER_ADMIN_HTTP_KEY,
|
|
3595
|
+
SUPER_ADMIN_LOGIN_ADAPTER_KEY,
|
|
3596
|
+
SUPER_ADMIN_MANIFEST_KEY,
|
|
3597
|
+
TenantSubscriptionBundlesApiError,
|
|
3598
|
+
buildFeatureRegistry,
|
|
3599
|
+
buildFeatureRouterGuard,
|
|
3600
|
+
buildNavigationGuard,
|
|
3601
|
+
buildQuotaRegistry,
|
|
3602
|
+
buildRoutes,
|
|
3603
|
+
buildSidebar,
|
|
3604
|
+
buildSnapshots,
|
|
3605
|
+
createAdminRoutes,
|
|
3606
|
+
createManifestStore,
|
|
3607
|
+
createPlatformLoaders,
|
|
3608
|
+
createProjectPageHostRoute,
|
|
3609
|
+
createSuperAdminApp,
|
|
3610
|
+
defaultHttpClient,
|
|
3611
|
+
defaultIconForActionKey,
|
|
3612
|
+
defaultKvStore,
|
|
3613
|
+
defaultToneForActionKey,
|
|
3614
|
+
getJson,
|
|
3615
|
+
listOpenDrafts,
|
|
3616
|
+
postJson,
|
|
3617
|
+
provideEntitlement,
|
|
3618
|
+
resolveExtension,
|
|
3619
|
+
trimTrailingSlashes,
|
|
3620
|
+
useActions,
|
|
3621
|
+
useApiList,
|
|
3622
|
+
useAuditEntries,
|
|
3623
|
+
useBatchColumns,
|
|
3624
|
+
useBulkPublish,
|
|
3625
|
+
useBundleVersions,
|
|
3626
|
+
useBundleVersionsMap,
|
|
3627
|
+
useBundles,
|
|
3628
|
+
useBusinessTypeVersions,
|
|
3629
|
+
useBusinessTypes,
|
|
3630
|
+
useCatalogEntries,
|
|
3631
|
+
useDiscovery,
|
|
3632
|
+
useEntitlement,
|
|
3633
|
+
useInjectedEntitlement,
|
|
3634
|
+
useLivePlanVersions,
|
|
3635
|
+
useManifest,
|
|
3636
|
+
useMarketingProjections,
|
|
3637
|
+
useNav,
|
|
3638
|
+
usePlanEditor,
|
|
3639
|
+
usePlanVersions,
|
|
3640
|
+
usePlanVersionsCatalog,
|
|
3641
|
+
usePlans,
|
|
3642
|
+
usePlatformTenantActions,
|
|
3643
|
+
usePromotions,
|
|
3644
|
+
usePublicBoot,
|
|
3645
|
+
useSubscriptionDraft,
|
|
3646
|
+
useSuperAdminActions,
|
|
3647
|
+
useSuperAdminBrand,
|
|
3648
|
+
useSuperAdminEndpoints,
|
|
3649
|
+
useSuperAdminExtensions,
|
|
3650
|
+
useSuperAdminHttp,
|
|
3651
|
+
useSuperAdminLoginAdapter,
|
|
3652
|
+
useSuperAdminManifest,
|
|
3653
|
+
useTenantActionFlow,
|
|
3654
|
+
useTenantBilling,
|
|
3655
|
+
useTenantBillingCatalog,
|
|
3656
|
+
useTenantManifest,
|
|
3657
|
+
useTenantSubscriptionBundles,
|
|
3658
|
+
useTenants
|
|
3659
|
+
};
|