@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// BootLoader — fetches the public boot endpoint (public, pre-login).
|
|
2
|
+
//
|
|
3
|
+
// Provides minimal branding data for the SuperAdmin login page (project.key,
|
|
4
|
+
// displayName, logoUrl, environment) — deliberately NO capabilities or page
|
|
5
|
+
// definitions before login.
|
|
6
|
+
//
|
|
7
|
+
// **Endpoint is mandatory** and supplied by the app consumer, because apps
|
|
8
|
+
// have different `globalPrefix` conventions:
|
|
9
|
+
// - `globalPrefix='api/v1'` → `/api/v1/admin/boot`
|
|
10
|
+
// - `globalPrefix='api'` → `/api/admin/boot`
|
|
11
|
+
//
|
|
12
|
+
// Spec: admin-api.openapi.yaml.
|
|
13
|
+
|
|
14
|
+
import type { PublicBootResponse } from '@saasicat/types';
|
|
15
|
+
import { defaultHttpClient, type HttpClient } from './types.js';
|
|
16
|
+
|
|
17
|
+
export interface BootLoaderOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Fully-qualified boot endpoint incl. app globalPrefix
|
|
20
|
+
* (`/api/admin/boot`, `/api/v1/admin/boot`, …). Mandatory.
|
|
21
|
+
*/
|
|
22
|
+
endpoint: string;
|
|
23
|
+
/** Defaults to `defaultHttpClient()` (= `fetch`). */
|
|
24
|
+
http?: HttpClient;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class BootLoadError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
public readonly status: number,
|
|
30
|
+
message: string,
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'BootLoadError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class BootLoader {
|
|
38
|
+
private readonly endpoint: string;
|
|
39
|
+
private readonly http: HttpClient;
|
|
40
|
+
|
|
41
|
+
constructor(options: BootLoaderOptions) {
|
|
42
|
+
if (!options?.endpoint) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'BootLoader: `endpoint` ist Pflicht (z. B. "/api/admin/boot" ' +
|
|
45
|
+
'oder "/api/v1/admin/boot"). Plattform hat keinen Default, ' +
|
|
46
|
+
'weil Apps unterschiedliche globalPrefix-Konventionen haben.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
this.endpoint = options.endpoint;
|
|
50
|
+
this.http = options.http ?? defaultHttpClient();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async load(): Promise<PublicBootResponse> {
|
|
54
|
+
const res = await this.http(this.endpoint);
|
|
55
|
+
if (res.status !== 200) {
|
|
56
|
+
throw new BootLoadError(res.status, `Boot-Endpunkt antwortete HTTP ${res.status}`);
|
|
57
|
+
}
|
|
58
|
+
return (await res.json()) as PublicBootResponse;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-dialog v-model="open" persistent>
|
|
3
|
+
<q-card style="min-width: 560px; max-width: 96vw">
|
|
4
|
+
<q-card-section>
|
|
5
|
+
<div class="text-h6">BundleVersion publishen</div>
|
|
6
|
+
<p class="text-caption text-grey-7">
|
|
7
|
+
Bundle <code>{{ bundleKey }}</code> · v{{ draft.version }}
|
|
8
|
+
</p>
|
|
9
|
+
</q-card-section>
|
|
10
|
+
|
|
11
|
+
<q-card-section v-if="!previewLoaded" class="bvpd__loading">
|
|
12
|
+
<q-spinner color="primary" size="32px" />
|
|
13
|
+
<span class="q-ml-sm">Diff zur Vorgänger-Version wird geladen …</span>
|
|
14
|
+
</q-card-section>
|
|
15
|
+
|
|
16
|
+
<q-card-section v-else class="bvpd__body">
|
|
17
|
+
<!-- Pre-emptive strict-mode warnings from the last mutate -->
|
|
18
|
+
<q-banner v-if="warnings.length > 0" class="bvpd__warnings" inline-actions rounded>
|
|
19
|
+
<template #avatar>
|
|
20
|
+
<q-icon name="warning" color="warning" />
|
|
21
|
+
</template>
|
|
22
|
+
<strong>{{ warnings.length }} Strict-Mode-Warnung(en)</strong>
|
|
23
|
+
<ul class="bvpd__list">
|
|
24
|
+
<li v-for="(w, i) in warnings" :key="i">
|
|
25
|
+
<code>{{ w.code }}</code>
|
|
26
|
+
<template v-if="w.value">
|
|
27
|
+
· <code>{{ w.value }}</code></template
|
|
28
|
+
>
|
|
29
|
+
— {{ w.message }}
|
|
30
|
+
</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</q-banner>
|
|
33
|
+
|
|
34
|
+
<div class="bvpd__validity">
|
|
35
|
+
<div class="bvpd__label">Gültigkeit</div>
|
|
36
|
+
<div class="bvpd__validity-grid">
|
|
37
|
+
<label class="bvpd__field">
|
|
38
|
+
<span>Gültig ab</span>
|
|
39
|
+
<input v-model="validFromInput" class="bvpd__input" type="date" />
|
|
40
|
+
</label>
|
|
41
|
+
<label class="bvpd__field">
|
|
42
|
+
<span>Gültig bis</span>
|
|
43
|
+
<input v-model="validUntilInput" class="bvpd__input" type="date" />
|
|
44
|
+
</label>
|
|
45
|
+
</div>
|
|
46
|
+
<p v-if="validityError" class="bvpd__error">{{ validityError }}</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div>
|
|
50
|
+
<q-toggle
|
|
51
|
+
v-model="allowZeroPrice"
|
|
52
|
+
label="Preis 0,00 bewusst zulassen (kostenloses Bundle)"
|
|
53
|
+
/>
|
|
54
|
+
<p class="bvpd__label" style="margin-top: 6px">
|
|
55
|
+
Standard: Publish mit explizitem Preis 0,00 wird blockiert (Seed-Schutz).
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Diff against the previous version -->
|
|
60
|
+
<div>
|
|
61
|
+
<div class="bvpd__label">
|
|
62
|
+
Änderungen gegenüber
|
|
63
|
+
<span v-if="previousVersion"> v{{ previousVersion }} </span>
|
|
64
|
+
<span v-else class="text-grey-7">
|
|
65
|
+
— keine Vorgänger-Version (Erst-Veröffentlichung)
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<q-banner
|
|
69
|
+
v-if="changes.length === 0 && previousVersion"
|
|
70
|
+
class="bvpd__neutral"
|
|
71
|
+
inline-actions
|
|
72
|
+
rounded
|
|
73
|
+
>
|
|
74
|
+
<template #avatar>
|
|
75
|
+
<q-icon name="info" color="grey-7" />
|
|
76
|
+
</template>
|
|
77
|
+
Keine Änderungen — die Versionen sind inhaltlich gleich.
|
|
78
|
+
</q-banner>
|
|
79
|
+
<q-list v-else-if="changes.length > 0" bordered separator>
|
|
80
|
+
<q-item v-for="(c, i) in changes" :key="i">
|
|
81
|
+
<q-item-section side>
|
|
82
|
+
<q-chip
|
|
83
|
+
dense
|
|
84
|
+
:color="directionColor(c.direction)"
|
|
85
|
+
text-color="white"
|
|
86
|
+
>
|
|
87
|
+
{{ directionLabel(c.direction) }}
|
|
88
|
+
</q-chip>
|
|
89
|
+
</q-item-section>
|
|
90
|
+
<q-item-section>
|
|
91
|
+
<q-item-label>
|
|
92
|
+
<code>{{ c.field }}</code>
|
|
93
|
+
</q-item-label>
|
|
94
|
+
<q-item-label caption>
|
|
95
|
+
<span class="bvpd__old">{{ formatValue(c.oldValue) }}</span>
|
|
96
|
+
<q-icon name="arrow_forward" size="14px" class="q-mx-xs" />
|
|
97
|
+
<span class="bvpd__new">{{ formatValue(c.newValue) }}</span>
|
|
98
|
+
</q-item-label>
|
|
99
|
+
</q-item-section>
|
|
100
|
+
</q-item>
|
|
101
|
+
</q-list>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Regression warning -->
|
|
105
|
+
<q-banner v-if="isRegression" class="bvpd__regression" inline-actions rounded>
|
|
106
|
+
<template #avatar>
|
|
107
|
+
<q-icon name="error" color="negative" />
|
|
108
|
+
</template>
|
|
109
|
+
<strong>Regressive Änderung erkannt.</strong>
|
|
110
|
+
Diese Version entfernt Features, senkt Quotas oder erhöht Preise. Vertragsschutz
|
|
111
|
+
P3 (SPEC.md §6) verlangt Bestand-Opt-in. Publish erfordert deinen ausdrücklichen
|
|
112
|
+
<code>force</code>-Confirm.
|
|
113
|
+
<template #action>
|
|
114
|
+
<q-toggle
|
|
115
|
+
v-model="forceRegressive"
|
|
116
|
+
label="Trotzdem publishen"
|
|
117
|
+
color="negative"
|
|
118
|
+
/>
|
|
119
|
+
</template>
|
|
120
|
+
</q-banner>
|
|
121
|
+
</q-card-section>
|
|
122
|
+
|
|
123
|
+
<q-card-actions align="right">
|
|
124
|
+
<q-btn flat label="Abbrechen" @click="close" />
|
|
125
|
+
<q-btn
|
|
126
|
+
unelevated
|
|
127
|
+
:color="isRegression ? 'negative' : 'positive'"
|
|
128
|
+
:label="isRegression ? 'Regressiv publishen' : 'Publishen'"
|
|
129
|
+
:loading="submitting"
|
|
130
|
+
:disable="!canSubmit"
|
|
131
|
+
@click="submit"
|
|
132
|
+
/>
|
|
133
|
+
</q-card-actions>
|
|
134
|
+
</q-card>
|
|
135
|
+
</q-dialog>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<script setup lang="ts">
|
|
139
|
+
import { computed, ref, watch } from 'vue';
|
|
140
|
+
import type {
|
|
141
|
+
BundleVersionMutationResult,
|
|
142
|
+
BundleVersionRow,
|
|
143
|
+
StrictModeWarning,
|
|
144
|
+
VersionChange,
|
|
145
|
+
} from '@saasicat/types';
|
|
146
|
+
|
|
147
|
+
// BundleVersionPublishDialog — confirm modal for publishing a draft version.
|
|
148
|
+
// Shows the diff against the previous version (publishedChanges) and blocks
|
|
149
|
+
// regressive versions without a forceRegressive confirm.
|
|
150
|
+
|
|
151
|
+
const props = defineProps<{
|
|
152
|
+
modelValue: boolean;
|
|
153
|
+
bundleKey: string;
|
|
154
|
+
draft: BundleVersionRow;
|
|
155
|
+
/** Previous version (or null on first publication). */
|
|
156
|
+
previous: BundleVersionRow | null;
|
|
157
|
+
/** Pre-detected strict-mode warnings (e.g. from the last draft mutation). */
|
|
158
|
+
warnings: StrictModeWarning[];
|
|
159
|
+
/**
|
|
160
|
+
* Pure-function diff classifier. The consumer wrapper passes the
|
|
161
|
+
* appropriate `classifyBundleVersionDiff(...)` from the platform package
|
|
162
|
+
* through — the UI does not depend directly on the NestJS implementation.
|
|
163
|
+
*/
|
|
164
|
+
classifyDiff: (
|
|
165
|
+
previous: BundleVersionRow,
|
|
166
|
+
draft: BundleVersionRow,
|
|
167
|
+
) => { changes: VersionChange[]; nonRegressive: boolean };
|
|
168
|
+
submit: (opts: {
|
|
169
|
+
forceRegressive: boolean;
|
|
170
|
+
allowZeroPrice?: boolean;
|
|
171
|
+
validFrom?: string | null;
|
|
172
|
+
validUntil?: string | null;
|
|
173
|
+
}) => Promise<BundleVersionMutationResult>;
|
|
174
|
+
}>();
|
|
175
|
+
|
|
176
|
+
const emit = defineEmits<{
|
|
177
|
+
(e: 'update:modelValue', value: boolean): void;
|
|
178
|
+
(e: 'submitted', result: BundleVersionMutationResult): void;
|
|
179
|
+
}>();
|
|
180
|
+
|
|
181
|
+
const open = computed({
|
|
182
|
+
get: () => props.modelValue,
|
|
183
|
+
set: (v) => emit('update:modelValue', v),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const previewLoaded = ref(false);
|
|
187
|
+
const changes = ref<VersionChange[]>([]);
|
|
188
|
+
const nonRegressive = ref(true);
|
|
189
|
+
const forceRegressive = ref(false);
|
|
190
|
+
const allowZeroPrice = ref(false);
|
|
191
|
+
const submitting = ref(false);
|
|
192
|
+
const validFromInput = ref('');
|
|
193
|
+
const validUntilInput = ref('');
|
|
194
|
+
|
|
195
|
+
const previousVersion = computed(() => props.previous?.version ?? null);
|
|
196
|
+
const isRegression = computed(() => previewLoaded.value && !nonRegressive.value);
|
|
197
|
+
const validityError = computed<string | null>(() => {
|
|
198
|
+
if (!validFromInput.value) return 'Gültig ab ist Pflicht.';
|
|
199
|
+
if (validUntilInput.value && validUntilInput.value <= validFromInput.value) {
|
|
200
|
+
return 'Gültig bis muss nach Gültig ab liegen.';
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
});
|
|
204
|
+
const canSubmit = computed(
|
|
205
|
+
() =>
|
|
206
|
+
previewLoaded.value &&
|
|
207
|
+
!submitting.value &&
|
|
208
|
+
!validityError.value &&
|
|
209
|
+
(!isRegression.value || forceRegressive.value),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
watch(
|
|
213
|
+
() => [props.modelValue, props.draft, props.previous],
|
|
214
|
+
() => {
|
|
215
|
+
if (!props.modelValue) return;
|
|
216
|
+
previewLoaded.value = false;
|
|
217
|
+
forceRegressive.value = false;
|
|
218
|
+
allowZeroPrice.value = false;
|
|
219
|
+
validFromInput.value = props.draft.validFrom ? props.draft.validFrom.slice(0, 10) : '';
|
|
220
|
+
validUntilInput.value = props.draft.validUntil ? props.draft.validUntil.slice(0, 10) : '';
|
|
221
|
+
try {
|
|
222
|
+
const diff = props.previous
|
|
223
|
+
? props.classifyDiff(props.previous, props.draft)
|
|
224
|
+
: { changes: [], nonRegressive: true };
|
|
225
|
+
changes.value = diff.changes;
|
|
226
|
+
nonRegressive.value = diff.nonRegressive;
|
|
227
|
+
} finally {
|
|
228
|
+
previewLoaded.value = true;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{ immediate: true },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
function close(): void {
|
|
235
|
+
open.value = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function submit(): Promise<void> {
|
|
239
|
+
if (!canSubmit.value) return;
|
|
240
|
+
submitting.value = true;
|
|
241
|
+
try {
|
|
242
|
+
const result = await props.submit({
|
|
243
|
+
forceRegressive: forceRegressive.value,
|
|
244
|
+
allowZeroPrice: allowZeroPrice.value,
|
|
245
|
+
validFrom: validFromInput.value || null,
|
|
246
|
+
validUntil: validUntilInput.value || null,
|
|
247
|
+
});
|
|
248
|
+
emit('submitted', result);
|
|
249
|
+
close();
|
|
250
|
+
} finally {
|
|
251
|
+
submitting.value = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function directionColor(d: VersionChange['direction']): string {
|
|
256
|
+
switch (d) {
|
|
257
|
+
case 'IMPROVEMENT':
|
|
258
|
+
return 'positive';
|
|
259
|
+
case 'REGRESSION':
|
|
260
|
+
return 'negative';
|
|
261
|
+
default:
|
|
262
|
+
return 'grey-7';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function directionLabel(d: VersionChange['direction']): string {
|
|
267
|
+
switch (d) {
|
|
268
|
+
case 'IMPROVEMENT':
|
|
269
|
+
return '+';
|
|
270
|
+
case 'REGRESSION':
|
|
271
|
+
return '−';
|
|
272
|
+
default:
|
|
273
|
+
return '○';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatValue(v: unknown): string {
|
|
278
|
+
if (v === null || v === undefined) return '—';
|
|
279
|
+
if (Array.isArray(v)) return v.length === 0 ? '[]' : v.join(', ');
|
|
280
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
281
|
+
return String(v);
|
|
282
|
+
}
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<style scoped>
|
|
286
|
+
.bvpd__body {
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
gap: 16px;
|
|
290
|
+
}
|
|
291
|
+
.bvpd__label {
|
|
292
|
+
font-size: 12px;
|
|
293
|
+
color: var(--q-grey-7, #757575);
|
|
294
|
+
text-transform: uppercase;
|
|
295
|
+
letter-spacing: 0.5px;
|
|
296
|
+
margin-bottom: 6px;
|
|
297
|
+
}
|
|
298
|
+
.bvpd__loading {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
padding: 32px;
|
|
303
|
+
}
|
|
304
|
+
.bvpd__warnings {
|
|
305
|
+
border-left: 4px solid var(--q-warning, #f2c037);
|
|
306
|
+
}
|
|
307
|
+
.bvpd__neutral {
|
|
308
|
+
border-left: 4px solid var(--q-grey-7, #757575);
|
|
309
|
+
}
|
|
310
|
+
.bvpd__regression {
|
|
311
|
+
border-left: 4px solid var(--q-negative, #c10015);
|
|
312
|
+
}
|
|
313
|
+
.bvpd__validity {
|
|
314
|
+
padding: 12px;
|
|
315
|
+
background: #f8fafc;
|
|
316
|
+
border: 1px solid #e2e8f0;
|
|
317
|
+
border-radius: 8px;
|
|
318
|
+
}
|
|
319
|
+
.bvpd__validity-grid {
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
322
|
+
gap: 12px;
|
|
323
|
+
}
|
|
324
|
+
.bvpd__field {
|
|
325
|
+
display: grid;
|
|
326
|
+
gap: 6px;
|
|
327
|
+
font-size: 12px;
|
|
328
|
+
font-weight: 700;
|
|
329
|
+
color: #475569;
|
|
330
|
+
}
|
|
331
|
+
.bvpd__input {
|
|
332
|
+
min-height: 36px;
|
|
333
|
+
border: 1px solid #cbd5e1;
|
|
334
|
+
border-radius: 6px;
|
|
335
|
+
padding: 0 10px;
|
|
336
|
+
font: inherit;
|
|
337
|
+
color: #0f172a;
|
|
338
|
+
background: #fff;
|
|
339
|
+
}
|
|
340
|
+
.bvpd__error {
|
|
341
|
+
margin: 8px 0 0;
|
|
342
|
+
color: var(--q-negative, #c10015);
|
|
343
|
+
font-size: 12px;
|
|
344
|
+
font-weight: 700;
|
|
345
|
+
}
|
|
346
|
+
.bvpd__list {
|
|
347
|
+
margin: 8px 0 0;
|
|
348
|
+
padding-left: 20px;
|
|
349
|
+
font-size: 13px;
|
|
350
|
+
}
|
|
351
|
+
.bvpd__old {
|
|
352
|
+
color: var(--q-negative, #c10015);
|
|
353
|
+
text-decoration: line-through;
|
|
354
|
+
}
|
|
355
|
+
.bvpd__new {
|
|
356
|
+
color: var(--q-positive, #21ba45);
|
|
357
|
+
font-weight: 600;
|
|
358
|
+
}
|
|
359
|
+
</style>
|