@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,910 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sa-bundles">
|
|
3
|
+
<BundlesHeader
|
|
4
|
+
:loading="loading"
|
|
5
|
+
:display-locale="displayLocale"
|
|
6
|
+
:locales="locales"
|
|
7
|
+
@create="openCreatePanel"
|
|
8
|
+
@refresh="load"
|
|
9
|
+
@update:display-locale="(value) => (displayLocale = value)"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<q-banner v-if="error" class="sa-bundles__error" rounded>
|
|
13
|
+
<template #avatar><q-icon name="warning" color="negative" /></template>
|
|
14
|
+
Fehler beim Laden: {{ error.message }}
|
|
15
|
+
</q-banner>
|
|
16
|
+
|
|
17
|
+
<BundlesKpis
|
|
18
|
+
:bundles-total="bundles.length"
|
|
19
|
+
:live-count="liveCount"
|
|
20
|
+
:scheduled-bundles-count="scheduledBundlesCount"
|
|
21
|
+
:total-scheduled-versions="totalScheduledVersions"
|
|
22
|
+
:total-draft-versions="totalDraftVersions"
|
|
23
|
+
:draft-bundles-count="draftBundlesCount"
|
|
24
|
+
:translated-count="translatedCount"
|
|
25
|
+
:locales-count="locales.length"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<BundlesFilterBar
|
|
29
|
+
v-model:query="query"
|
|
30
|
+
v-model:status-filter="statusFilter"
|
|
31
|
+
:status-filter-options="statusFilterOptions"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<!-- Inline creation (collapsible panel, replaces the former dialog modal) -->
|
|
35
|
+
<BundleCreatePanel
|
|
36
|
+
v-if="createOpen"
|
|
37
|
+
:project-key="projectKey"
|
|
38
|
+
:available-features="snapshot?.features ?? []"
|
|
39
|
+
:available-quotas="snapshot?.quotas ?? []"
|
|
40
|
+
:plans="plans"
|
|
41
|
+
:live-plan-versions="livePlanVersions"
|
|
42
|
+
:feature-registry="featureRegistryResolved"
|
|
43
|
+
:quota-registry="quotaRegistryResolved"
|
|
44
|
+
:existing-bundle-keys="existingBundleKeys"
|
|
45
|
+
:create="create"
|
|
46
|
+
:create-draft="createDraft"
|
|
47
|
+
@cancel="createOpen = false"
|
|
48
|
+
@created="onWizardCreated"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<q-banner
|
|
52
|
+
v-if="bundles.length === 0 && !loading && !error"
|
|
53
|
+
class="sa-bundles__empty"
|
|
54
|
+
rounded
|
|
55
|
+
>
|
|
56
|
+
<template #avatar><q-icon name="info" color="info" /></template>
|
|
57
|
+
Noch keine Bundles angelegt. Über <strong>Neues Bundle</strong> einen Bundle-Stamm
|
|
58
|
+
anlegen, dann Features & Quotas in einer Draft-Version kuratieren.
|
|
59
|
+
</q-banner>
|
|
60
|
+
|
|
61
|
+
<BundleAccordionList
|
|
62
|
+
:filtered-bundles="filteredBundles"
|
|
63
|
+
:bundles-total="bundles.length"
|
|
64
|
+
:open-key="openKey"
|
|
65
|
+
:aggregate-status-of="aggregateStatusOf"
|
|
66
|
+
:i18n-locale-count="i18nLocaleCount"
|
|
67
|
+
@toggle="toggle"
|
|
68
|
+
>
|
|
69
|
+
<template #detail="{ bundle }">
|
|
70
|
+
<BundleDetailPanel
|
|
71
|
+
:bundle="bundle"
|
|
72
|
+
v-model:edit-form="editForm"
|
|
73
|
+
:i18n-draft="i18nDraft"
|
|
74
|
+
:translatable-locales="translatableLocales"
|
|
75
|
+
:edit-submitting="editSubmitting"
|
|
76
|
+
:detail-versions="detailVersions"
|
|
77
|
+
:selected-version-id="selectedVersionIdByBundle[bundle.id] ?? null"
|
|
78
|
+
:selected-version="selectedVersion"
|
|
79
|
+
:available-features="snapshot?.features ?? []"
|
|
80
|
+
:available-quotas="snapshot?.quotas ?? []"
|
|
81
|
+
:plans="plans"
|
|
82
|
+
:live-plan-versions="livePlanVersions"
|
|
83
|
+
:feature-registry="featureRegistryResolved"
|
|
84
|
+
:quota-registry="quotaRegistryResolved"
|
|
85
|
+
:inline-editor-saving="inlineEditorSaving"
|
|
86
|
+
:inline-editor-error="inlineEditorError"
|
|
87
|
+
@set-i18n="setI18n"
|
|
88
|
+
@submit-edit="submitEdit"
|
|
89
|
+
@select-version="onSelectVersion"
|
|
90
|
+
@add-version="onAddVersion"
|
|
91
|
+
@inline-save="onInlineSave"
|
|
92
|
+
@discard-version="onDiscardVersion"
|
|
93
|
+
@publish-version="openPublish"
|
|
94
|
+
@delete-bundle="confirmDelete"
|
|
95
|
+
/>
|
|
96
|
+
</template>
|
|
97
|
+
</BundleAccordionList>
|
|
98
|
+
|
|
99
|
+
<!-- Strict mode warnings after the last mutation -->
|
|
100
|
+
<q-banner
|
|
101
|
+
v-if="lastWarnings.length > 0"
|
|
102
|
+
class="sa-bundles__warnings-banner"
|
|
103
|
+
inline-actions
|
|
104
|
+
rounded
|
|
105
|
+
>
|
|
106
|
+
<template #avatar><q-icon name="warning" color="warning" /></template>
|
|
107
|
+
<strong>{{ lastWarnings.length }} Strict-Mode-Warnung(en) bei letzter Operation</strong>
|
|
108
|
+
<ul class="sa-bundles__warnings-list">
|
|
109
|
+
<li v-for="(w, i) in lastWarnings" :key="i">
|
|
110
|
+
<code>{{ w.code }}</code>
|
|
111
|
+
<template v-if="w.value">
|
|
112
|
+
· <code>{{ w.value }}</code></template
|
|
113
|
+
>
|
|
114
|
+
— {{ w.message }}
|
|
115
|
+
</li>
|
|
116
|
+
</ul>
|
|
117
|
+
<template #action>
|
|
118
|
+
<q-btn flat dense label="Schließen" @click="lastWarnings = []" />
|
|
119
|
+
</template>
|
|
120
|
+
</q-banner>
|
|
121
|
+
|
|
122
|
+
<!-- Publish confirmation modal -->
|
|
123
|
+
<BundleVersionPublishDialog
|
|
124
|
+
v-if="detailBundle && publishOpen && publishDraft"
|
|
125
|
+
v-model="publishOpen"
|
|
126
|
+
:bundle-key="detailBundle.bundleKey"
|
|
127
|
+
:draft="publishDraft"
|
|
128
|
+
:previous="publishPrevious"
|
|
129
|
+
:warnings="lastWarnings"
|
|
130
|
+
:classify-diff="classifyDiff"
|
|
131
|
+
:submit="onPublishSubmit"
|
|
132
|
+
@submitted="onPublishSubmitted"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<script setup lang="ts">
|
|
138
|
+
import { computed, ref, watch } from 'vue';
|
|
139
|
+
import type {
|
|
140
|
+
BundleRow,
|
|
141
|
+
BundleVersionMutationResult,
|
|
142
|
+
BundleVersionRow,
|
|
143
|
+
CatalogEntryI18n,
|
|
144
|
+
CreateBundleData,
|
|
145
|
+
CreateBundleVersionDraftData,
|
|
146
|
+
DiscoverySnapshot,
|
|
147
|
+
FeatureCatalogEntryRow,
|
|
148
|
+
PlanRow,
|
|
149
|
+
PlanVersionRow,
|
|
150
|
+
QuotaCatalogEntryRow,
|
|
151
|
+
StrictModeWarning,
|
|
152
|
+
UpdateBundleData,
|
|
153
|
+
UpdateBundleVersionDraftData,
|
|
154
|
+
VersionChange,
|
|
155
|
+
} from '@saasicat/types';
|
|
156
|
+
|
|
157
|
+
import BundleVersionPublishDialog from '../components/BundleVersionPublishDialog.vue';
|
|
158
|
+
import BundleCreatePanel from '../components/bundle-editor/BundleCreatePanel.vue';
|
|
159
|
+
import type { FeatureMeta } from '../components/bundle-editor/BundleFeaturesEditor.vue';
|
|
160
|
+
import {
|
|
161
|
+
buildFeatureRegistry,
|
|
162
|
+
buildQuotaRegistry,
|
|
163
|
+
type QuotaMeta,
|
|
164
|
+
} from '../components/bundle-editor/catalog-i18n.js';
|
|
165
|
+
import {
|
|
166
|
+
bundleActiveVersionAt,
|
|
167
|
+
bundleAggregateStatus,
|
|
168
|
+
bundleVersionStatus,
|
|
169
|
+
bundleVersionsSorted,
|
|
170
|
+
type BundleAggregateStatus,
|
|
171
|
+
} from '../components/bundle-editor/bundle-version-status';
|
|
172
|
+
import BundleAccordionList from './bundles-page/BundleAccordionList.vue';
|
|
173
|
+
import BundleDetailPanel from './bundles-page/BundleDetailPanel.vue';
|
|
174
|
+
import BundlesFilterBar from './bundles-page/BundlesFilterBar.vue';
|
|
175
|
+
import BundlesHeader from './bundles-page/BundlesHeader.vue';
|
|
176
|
+
import BundlesKpis from './bundles-page/BundlesKpis.vue';
|
|
177
|
+
import type {
|
|
178
|
+
BundleEditForm,
|
|
179
|
+
BundlesStatusFilter,
|
|
180
|
+
BundlesStatusFilterOption,
|
|
181
|
+
} from './bundles-page/types.js';
|
|
182
|
+
|
|
183
|
+
// Platform standard page: Bundles (SPEC_V2 §6.4, §11.1 M6 Pack 2c +
|
|
184
|
+
// Pack 2d inline editor after plan simulation). Dumb component —
|
|
185
|
+
// the consumer wrapper supplies the composable results + plan-root
|
|
186
|
+
// list + live PlanVersion index as props.
|
|
187
|
+
|
|
188
|
+
const DEFAULT_LOCALE = 'de';
|
|
189
|
+
|
|
190
|
+
const props = withDefaults(
|
|
191
|
+
defineProps<{
|
|
192
|
+
projectKey: string;
|
|
193
|
+
bundles: BundleRow[];
|
|
194
|
+
loading: boolean;
|
|
195
|
+
error: Error | null;
|
|
196
|
+
/** Active locales from the project (incl. default). Default: only `de`. */
|
|
197
|
+
activeLocales?: string[];
|
|
198
|
+
/** Plan roots for the compat picker in the inline editor. */
|
|
199
|
+
plans?: PlanRow[];
|
|
200
|
+
/**
|
|
201
|
+
* Live (or latest) PlanVersion per `planKey` — source for the
|
|
202
|
+
* Plan↔Bundle overlap calculation. The wrapper can assemble it via
|
|
203
|
+
* `usePlanVersions`.
|
|
204
|
+
*/
|
|
205
|
+
livePlanVersions?: Record<string, PlanVersionRow | null>;
|
|
206
|
+
/**
|
|
207
|
+
* Optional manual feature label/group mapping. Overrides the
|
|
208
|
+
* labels derived from `featureCatalog` (rarely needed).
|
|
209
|
+
*/
|
|
210
|
+
featureRegistry?: Record<string, FeatureMeta>;
|
|
211
|
+
/**
|
|
212
|
+
* Feature catalog entries (incl. `i18n`) for label resolution in the
|
|
213
|
+
* chosen display language. The wrapper supplies them via `useCatalogEntries`.
|
|
214
|
+
*/
|
|
215
|
+
featureCatalog?: FeatureCatalogEntryRow[];
|
|
216
|
+
/** Quota catalog entries (incl. `i18n`) for label/unit resolution. */
|
|
217
|
+
quotaCatalog?: QuotaCatalogEntryRow[];
|
|
218
|
+
load: () => Promise<void>;
|
|
219
|
+
create: (data: CreateBundleData) => Promise<BundleRow>;
|
|
220
|
+
update: (bundleId: string, data: UpdateBundleData) => Promise<BundleRow>;
|
|
221
|
+
softDelete: (bundleId: string) => Promise<void>;
|
|
222
|
+
loadVersions: (bundleId: string) => Promise<BundleVersionRow[]>;
|
|
223
|
+
createDraft: (
|
|
224
|
+
bundleId: string,
|
|
225
|
+
data: Omit<CreateBundleVersionDraftData, 'bundleId'>,
|
|
226
|
+
) => Promise<BundleVersionMutationResult>;
|
|
227
|
+
updateDraft: (
|
|
228
|
+
versionId: string,
|
|
229
|
+
data: UpdateBundleVersionDraftData,
|
|
230
|
+
) => Promise<BundleVersionMutationResult>;
|
|
231
|
+
publish: (
|
|
232
|
+
versionId: string,
|
|
233
|
+
opts: {
|
|
234
|
+
forceRegressive?: boolean;
|
|
235
|
+
validFrom?: string | null;
|
|
236
|
+
validUntil?: string | null;
|
|
237
|
+
},
|
|
238
|
+
) => Promise<BundleVersionMutationResult>;
|
|
239
|
+
/** Optional: discard a draft (DELETE /catalog/bundle-versions/:id). */
|
|
240
|
+
discardDraft?: (versionId: string) => Promise<void>;
|
|
241
|
+
/**
|
|
242
|
+
* Optional: mapping `bundleId → BundleVersionRow[]` for correct
|
|
243
|
+
* KPI/status-filter display across all bundles. Without this prop
|
|
244
|
+
* the KPIs fall back to "0", because the lazily loaded
|
|
245
|
+
* `detailVersions` only cover the open bundle. The wrapper usually
|
|
246
|
+
* supplies it via `useBundleVersionsMap`.
|
|
247
|
+
*/
|
|
248
|
+
versionsByBundle?: Record<string, BundleVersionRow[]>;
|
|
249
|
+
snapshot: DiscoverySnapshot | null;
|
|
250
|
+
classifyDiff: (
|
|
251
|
+
previous: BundleVersionRow,
|
|
252
|
+
draft: BundleVersionRow,
|
|
253
|
+
) => { changes: VersionChange[]; nonRegressive: boolean };
|
|
254
|
+
}>(),
|
|
255
|
+
{
|
|
256
|
+
plans: () => [],
|
|
257
|
+
livePlanVersions: () => ({}),
|
|
258
|
+
featureRegistry: () => ({}),
|
|
259
|
+
featureCatalog: () => [],
|
|
260
|
+
quotaCatalog: () => [],
|
|
261
|
+
activeLocales: () => [DEFAULT_LOCALE],
|
|
262
|
+
versionsByBundle: () => ({}),
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const locales = computed(() => props.activeLocales ?? [DEFAULT_LOCALE]);
|
|
267
|
+
const translatableLocales = computed(() => locales.value.filter((l) => l !== DEFAULT_LOCALE));
|
|
268
|
+
|
|
269
|
+
// ─── Display language for feature/quota labels (creation + detail) ───
|
|
270
|
+
const displayLocale = ref(DEFAULT_LOCALE);
|
|
271
|
+
|
|
272
|
+
// Catalog-derived registries in the chosen display language; a
|
|
273
|
+
// manually passed `featureRegistry` takes precedence (override).
|
|
274
|
+
const featureRegistryResolved = computed<Record<string, FeatureMeta>>(() => ({
|
|
275
|
+
...buildFeatureRegistry(props.featureCatalog, displayLocale.value),
|
|
276
|
+
...props.featureRegistry,
|
|
277
|
+
}));
|
|
278
|
+
const quotaRegistryResolved = computed<Record<string, QuotaMeta>>(() =>
|
|
279
|
+
buildQuotaRegistry(props.quotaCatalog, displayLocale.value),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const query = ref('');
|
|
283
|
+
|
|
284
|
+
// ─── Status filter + KPI aggregates across all bundles ───
|
|
285
|
+
const statusFilter = ref<BundlesStatusFilter>('all');
|
|
286
|
+
|
|
287
|
+
function versionsOf(bundleId: string): BundleVersionRow[] {
|
|
288
|
+
// Prefers the versionsByBundle supplied by the wrapper; falls back to
|
|
289
|
+
// detailVersions when the open bundle was just loaded
|
|
290
|
+
// (between wrapper refresh and map update).
|
|
291
|
+
const fromMap = props.versionsByBundle[bundleId];
|
|
292
|
+
if (fromMap && fromMap.length > 0) return fromMap;
|
|
293
|
+
if (openKey.value === bundleId) return detailVersions.value;
|
|
294
|
+
return fromMap ?? [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function aggregateStatusOf(b: BundleRow): BundleAggregateStatus {
|
|
298
|
+
return bundleAggregateStatus(versionsOf(b.id), b.deletedAt);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const filteredBundles = computed(() => {
|
|
302
|
+
const q = query.value?.trim().toLowerCase() ?? '';
|
|
303
|
+
return props.bundles.filter((b) => {
|
|
304
|
+
if (q && !b.bundleKey.toLowerCase().includes(q) && !b.label.toLowerCase().includes(q)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (statusFilter.value !== 'all' && aggregateStatusOf(b) !== statusFilter.value) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
function i18nLocaleCount(b: BundleRow): number {
|
|
315
|
+
return Object.keys(b.i18n ?? {}).length;
|
|
316
|
+
}
|
|
317
|
+
const translatedCount = computed(() => props.bundles.filter((b) => i18nLocaleCount(b) > 0).length);
|
|
318
|
+
|
|
319
|
+
const liveCount = computed(
|
|
320
|
+
() => props.bundles.filter((b) => aggregateStatusOf(b) === 'live').length,
|
|
321
|
+
);
|
|
322
|
+
const scheduledBundlesCount = computed(
|
|
323
|
+
() => props.bundles.filter((b) => aggregateStatusOf(b) === 'scheduled').length,
|
|
324
|
+
);
|
|
325
|
+
const draftBundlesCount = computed(
|
|
326
|
+
() => props.bundles.filter((b) => aggregateStatusOf(b) === 'draft').length,
|
|
327
|
+
);
|
|
328
|
+
const totalDraftVersions = computed(() => {
|
|
329
|
+
let n = 0;
|
|
330
|
+
for (const b of props.bundles) {
|
|
331
|
+
for (const v of versionsOf(b.id)) {
|
|
332
|
+
if (v.publishedAt === null) n += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return n;
|
|
336
|
+
});
|
|
337
|
+
const totalScheduledVersions = computed(() => {
|
|
338
|
+
let n = 0;
|
|
339
|
+
for (const b of props.bundles) {
|
|
340
|
+
for (const v of versionsOf(b.id)) {
|
|
341
|
+
if (bundleVersionStatus(v) === 'scheduled') n += 1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return n;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─── Inline creation panel (5 sections, at the top of the list) ───
|
|
348
|
+
const createOpen = ref(false);
|
|
349
|
+
|
|
350
|
+
/** For the bundle-key conflict check in the wizard. */
|
|
351
|
+
const existingBundleKeys = computed(() => props.bundles.map((b) => b.bundleKey));
|
|
352
|
+
|
|
353
|
+
const statusFilterOptions: BundlesStatusFilterOption[] = [
|
|
354
|
+
{ label: 'Alle Status', value: 'all' as const },
|
|
355
|
+
{ label: 'Nur Live', value: 'live' as const },
|
|
356
|
+
{ label: 'Mit geplanter Version', value: 'scheduled' as const },
|
|
357
|
+
{ label: 'Drafts', value: 'draft' as const },
|
|
358
|
+
{ label: 'Abgelöste', value: 'superseded' as const },
|
|
359
|
+
{ label: 'Retired', value: 'retired' as const },
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
function openCreatePanel(): void {
|
|
363
|
+
createOpen.value = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function onWizardCreated(bundle: BundleRow): Promise<void> {
|
|
367
|
+
// Creation wrote the root + v1 draft in a single atom. Close the
|
|
368
|
+
// panel, reload the list, then expand the freshly created bundle
|
|
369
|
+
// so the user can keep working directly in the inline editor.
|
|
370
|
+
createOpen.value = false;
|
|
371
|
+
await props.load();
|
|
372
|
+
if (openKey.value !== bundle.id) {
|
|
373
|
+
openKey.value = null;
|
|
374
|
+
const next = props.bundles.find((b) => b.id === bundle.id);
|
|
375
|
+
if (next) await toggle(next);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── Accordion / detail ───
|
|
380
|
+
const openKey = ref<string | null>(null);
|
|
381
|
+
const detailVersions = ref<BundleVersionRow[]>([]);
|
|
382
|
+
const editForm = ref<BundleEditForm>({
|
|
383
|
+
label: '',
|
|
384
|
+
description: '',
|
|
385
|
+
icon: '',
|
|
386
|
+
sortOrder: 0,
|
|
387
|
+
});
|
|
388
|
+
const i18nDraft = ref<CatalogEntryI18n>({});
|
|
389
|
+
const editSubmitting = ref(false);
|
|
390
|
+
|
|
391
|
+
const detailBundle = computed<BundleRow | null>(
|
|
392
|
+
() => props.bundles.find((b) => b.id === openKey.value) ?? null,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
async function toggle(bundle: BundleRow): Promise<void> {
|
|
396
|
+
if (openKey.value === bundle.id) {
|
|
397
|
+
openKey.value = null;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
openKey.value = bundle.id;
|
|
401
|
+
editForm.value = {
|
|
402
|
+
label: bundle.label,
|
|
403
|
+
description: bundle.description ?? '',
|
|
404
|
+
icon: bundle.icon ?? '',
|
|
405
|
+
sortOrder: bundle.sortOrder,
|
|
406
|
+
};
|
|
407
|
+
i18nDraft.value = JSON.parse(JSON.stringify(bundle.i18n ?? {})) as CatalogEntryI18n;
|
|
408
|
+
detailVersions.value = [];
|
|
409
|
+
inlineEditorError.value = null;
|
|
410
|
+
detailVersions.value = await props.loadVersions(bundle.id);
|
|
411
|
+
if (!selectedVersionIdByBundle.value[bundle.id]) {
|
|
412
|
+
const defaultVersion = defaultSelectedVersion(detailVersions.value);
|
|
413
|
+
if (defaultVersion) {
|
|
414
|
+
onSelectVersion(bundle.id, defaultVersion.id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function setI18n(locale: string, field: 'label' | 'description', value: string): void {
|
|
420
|
+
const next: CatalogEntryI18n = { ...i18nDraft.value };
|
|
421
|
+
const entry = { ...(next[locale] ?? {}) };
|
|
422
|
+
if (value) {
|
|
423
|
+
entry[field] = value;
|
|
424
|
+
} else {
|
|
425
|
+
delete entry[field];
|
|
426
|
+
}
|
|
427
|
+
next[locale] = entry;
|
|
428
|
+
i18nDraft.value = next;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function submitEdit(): Promise<void> {
|
|
432
|
+
if (!detailBundle.value) return;
|
|
433
|
+
editSubmitting.value = true;
|
|
434
|
+
try {
|
|
435
|
+
await props.update(detailBundle.value.id, {
|
|
436
|
+
label: editForm.value.label,
|
|
437
|
+
description: editForm.value.description || null,
|
|
438
|
+
icon: editForm.value.icon || null,
|
|
439
|
+
sortOrder: editForm.value.sortOrder,
|
|
440
|
+
i18n: i18nDraft.value,
|
|
441
|
+
});
|
|
442
|
+
} finally {
|
|
443
|
+
editSubmitting.value = false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function confirmDelete(bundle: BundleRow): Promise<void> {
|
|
448
|
+
const ok = window.confirm(
|
|
449
|
+
`Bundle '${bundle.bundleKey}' wirklich soft-deleten? Bestand bleibt durch ` +
|
|
450
|
+
`published BundleVersions geschützt.`,
|
|
451
|
+
);
|
|
452
|
+
if (!ok) return;
|
|
453
|
+
await props.softDelete(bundle.id);
|
|
454
|
+
if (openKey.value === bundle.id) openKey.value = null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
watch(
|
|
458
|
+
() => props.bundles,
|
|
459
|
+
(next) => {
|
|
460
|
+
if (openKey.value && !next.some((b) => b.id === openKey.value)) {
|
|
461
|
+
openKey.value = null;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// ─── Strict mode warnings ───
|
|
467
|
+
const lastWarnings = ref<StrictModeWarning[]>([]);
|
|
468
|
+
|
|
469
|
+
// ─── Inline editor (selected version per bundle) ───
|
|
470
|
+
const selectedVersionIdByBundle = ref<Record<string, string | null>>({});
|
|
471
|
+
const inlineEditorSaving = ref(false);
|
|
472
|
+
const inlineEditorError = ref<string | null>(null);
|
|
473
|
+
|
|
474
|
+
const selectedVersion = computed<BundleVersionRow | null>(() => {
|
|
475
|
+
if (!detailBundle.value) return null;
|
|
476
|
+
const id = selectedVersionIdByBundle.value[detailBundle.value.id];
|
|
477
|
+
if (!id) return null;
|
|
478
|
+
return detailVersions.value.find((v) => v.id === id) ?? null;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
function defaultSelectedVersion(versions: BundleVersionRow[]): BundleVersionRow | null {
|
|
482
|
+
if (versions.length === 0) return null;
|
|
483
|
+
// Preferred: the draft (exactly one allowed) → then scheduled →
|
|
484
|
+
// then live → then latest.
|
|
485
|
+
const draft = versions.find((v) => v.publishedAt === null);
|
|
486
|
+
if (draft) return draft;
|
|
487
|
+
const sorted = bundleVersionsSorted(versions);
|
|
488
|
+
const scheduled = sorted.find(
|
|
489
|
+
(v) => v.validFrom && new Date(v.validFrom).getTime() > Date.now(),
|
|
490
|
+
);
|
|
491
|
+
if (scheduled) return scheduled;
|
|
492
|
+
const live = bundleActiveVersionAt(versions);
|
|
493
|
+
if (live) return live;
|
|
494
|
+
return sorted[sorted.length - 1];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function onSelectVersion(bundleId: string, versionId: string): void {
|
|
498
|
+
selectedVersionIdByBundle.value = {
|
|
499
|
+
...selectedVersionIdByBundle.value,
|
|
500
|
+
[bundleId]: versionId,
|
|
501
|
+
};
|
|
502
|
+
inlineEditorError.value = null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* "Neue Version" button in the version strip: creates a draft (defaults
|
|
507
|
+
* from the last live version) and selects it in the inline editor.
|
|
508
|
+
* The backend blocks a second draft via a partial unique index — the strip
|
|
509
|
+
* disables the button accordingly, but we double-check defensively.
|
|
510
|
+
*/
|
|
511
|
+
async function onAddVersion(bundleId: string): Promise<void> {
|
|
512
|
+
if (detailVersions.value.some((v) => v.publishedAt === null)) return;
|
|
513
|
+
const sortedVersions = bundleVersionsSorted(detailVersions.value);
|
|
514
|
+
const previous =
|
|
515
|
+
bundleActiveVersionAt(detailVersions.value) ??
|
|
516
|
+
sortedVersions[sortedVersions.length - 1] ??
|
|
517
|
+
null;
|
|
518
|
+
inlineEditorError.value = null;
|
|
519
|
+
inlineEditorSaving.value = true;
|
|
520
|
+
try {
|
|
521
|
+
const result = await props.createDraft(bundleId, {
|
|
522
|
+
features: previous ? [...previous.features] : [],
|
|
523
|
+
quotas: previous ? { ...previous.quotas } : {},
|
|
524
|
+
compatibility: previous?.compatibility ?? {},
|
|
525
|
+
pricingOverrides: previous?.pricingOverrides ?? [],
|
|
526
|
+
monthlyNet: previous?.monthlyNet ?? null,
|
|
527
|
+
yearlyNet: previous?.yearlyNet ?? null,
|
|
528
|
+
marketed: previous?.marketed ?? true,
|
|
529
|
+
changeNote: '',
|
|
530
|
+
});
|
|
531
|
+
lastWarnings.value = result.warnings;
|
|
532
|
+
detailVersions.value = await props.loadVersions(bundleId);
|
|
533
|
+
onSelectVersion(bundleId, result.bundleVersion.id);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
inlineEditorError.value = err instanceof Error ? err.message : String(err);
|
|
536
|
+
} finally {
|
|
537
|
+
inlineEditorSaving.value = false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function onInlineSave(
|
|
542
|
+
bundleId: string,
|
|
543
|
+
versionId: string,
|
|
544
|
+
data: UpdateBundleVersionDraftData,
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
inlineEditorSaving.value = true;
|
|
547
|
+
inlineEditorError.value = null;
|
|
548
|
+
try {
|
|
549
|
+
const result = await props.updateDraft(versionId, data);
|
|
550
|
+
lastWarnings.value = result.warnings;
|
|
551
|
+
detailVersions.value = await props.loadVersions(bundleId);
|
|
552
|
+
// If the backend does not change the ID, the selection stays put.
|
|
553
|
+
} catch (err) {
|
|
554
|
+
inlineEditorError.value = err instanceof Error ? err.message : String(err);
|
|
555
|
+
} finally {
|
|
556
|
+
inlineEditorSaving.value = false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Discards a draft or scheduled version. Works only if the
|
|
562
|
+
* wrapper passes `discardDraft` through; without the prop we show a
|
|
563
|
+
* clear error message instead of a silent no-op.
|
|
564
|
+
*/
|
|
565
|
+
async function onDiscardVersion(bundleId: string, versionId: string): Promise<void> {
|
|
566
|
+
if (!props.discardDraft) {
|
|
567
|
+
inlineEditorError.value =
|
|
568
|
+
'Discard ist im Wrapper nicht verdrahtet — bitte `discardDraft`-Prop ergänzen.';
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const ok = window.confirm('Diese Draft-Version verwerfen? Der Inhalt geht verloren.');
|
|
572
|
+
if (!ok) return;
|
|
573
|
+
inlineEditorSaving.value = true;
|
|
574
|
+
inlineEditorError.value = null;
|
|
575
|
+
try {
|
|
576
|
+
await props.discardDraft(versionId);
|
|
577
|
+
// Remove from local list + re-select sensibly.
|
|
578
|
+
detailVersions.value = await props.loadVersions(bundleId);
|
|
579
|
+
const next = defaultSelectedVersion(detailVersions.value);
|
|
580
|
+
selectedVersionIdByBundle.value = {
|
|
581
|
+
...selectedVersionIdByBundle.value,
|
|
582
|
+
[bundleId]: next?.id ?? null,
|
|
583
|
+
};
|
|
584
|
+
} catch (err) {
|
|
585
|
+
inlineEditorError.value = err instanceof Error ? err.message : String(err);
|
|
586
|
+
} finally {
|
|
587
|
+
inlineEditorSaving.value = false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ─── Publish confirmation modal ───
|
|
592
|
+
const publishOpen = ref(false);
|
|
593
|
+
const publishDraft = ref<BundleVersionRow | null>(null);
|
|
594
|
+
|
|
595
|
+
const publishPrevious = computed<BundleVersionRow | null>(() => {
|
|
596
|
+
if (!publishDraft.value) return null;
|
|
597
|
+
return (
|
|
598
|
+
detailVersions.value
|
|
599
|
+
.filter((v) => v.publishedAt !== null && v.supersededAt === null)
|
|
600
|
+
.sort((a, b) => b.version - a.version)[0] ?? null
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
function openPublish(version: BundleVersionRow): void {
|
|
605
|
+
if (version.publishedAt !== null) return;
|
|
606
|
+
publishDraft.value = version;
|
|
607
|
+
publishOpen.value = true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function onPublishSubmit(opts: {
|
|
611
|
+
forceRegressive: boolean;
|
|
612
|
+
allowZeroPrice?: boolean;
|
|
613
|
+
validFrom?: string | null;
|
|
614
|
+
validUntil?: string | null;
|
|
615
|
+
}): Promise<BundleVersionMutationResult> {
|
|
616
|
+
if (!publishDraft.value) {
|
|
617
|
+
throw new Error('BundlesPage: publish submit ohne Draft-Kontext');
|
|
618
|
+
}
|
|
619
|
+
return props.publish(publishDraft.value.id, opts);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function onPublishSubmitted(result: BundleVersionMutationResult): Promise<void> {
|
|
623
|
+
lastWarnings.value = result.warnings;
|
|
624
|
+
publishDraft.value = null;
|
|
625
|
+
if (detailBundle.value) {
|
|
626
|
+
detailVersions.value = await props.loadVersions(detailBundle.value.id);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const classifyDiff = computed(() => props.classifyDiff);
|
|
631
|
+
</script>
|
|
632
|
+
|
|
633
|
+
<style>
|
|
634
|
+
.sa-bundles {
|
|
635
|
+
padding: 16px;
|
|
636
|
+
display: flex;
|
|
637
|
+
flex-direction: column;
|
|
638
|
+
gap: 14px;
|
|
639
|
+
}
|
|
640
|
+
.sa-bundles__head {
|
|
641
|
+
display: flex;
|
|
642
|
+
justify-content: space-between;
|
|
643
|
+
align-items: flex-end;
|
|
644
|
+
gap: 16px;
|
|
645
|
+
}
|
|
646
|
+
.sa-bundles__title {
|
|
647
|
+
font-size: 20px;
|
|
648
|
+
font-weight: 700;
|
|
649
|
+
margin: 0;
|
|
650
|
+
}
|
|
651
|
+
.sa-bundles__sub {
|
|
652
|
+
margin: 4px 0 0;
|
|
653
|
+
color: #64748b;
|
|
654
|
+
font-size: 12px;
|
|
655
|
+
max-width: 560px;
|
|
656
|
+
}
|
|
657
|
+
.sa-bundles__head-actions {
|
|
658
|
+
display: flex;
|
|
659
|
+
align-items: center;
|
|
660
|
+
gap: 12px;
|
|
661
|
+
}
|
|
662
|
+
.sa-bundles__error {
|
|
663
|
+
border-left: 4px solid #dc2626;
|
|
664
|
+
}
|
|
665
|
+
.sa-bundles__empty {
|
|
666
|
+
border-left: 4px solid #31ccec;
|
|
667
|
+
}
|
|
668
|
+
.sa-bundles__kpis {
|
|
669
|
+
display: grid;
|
|
670
|
+
/* Responsive: as many KPIs as fit (up to 4), then 3 → 2 → 1. */
|
|
671
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
672
|
+
gap: 10px;
|
|
673
|
+
}
|
|
674
|
+
.sa-bundles__kpi {
|
|
675
|
+
background: #fff;
|
|
676
|
+
border: 1px solid #e2e8f0;
|
|
677
|
+
border-radius: 10px;
|
|
678
|
+
padding: 12px 14px;
|
|
679
|
+
}
|
|
680
|
+
.sa-bundles__kpi-label {
|
|
681
|
+
font-size: 10px;
|
|
682
|
+
text-transform: uppercase;
|
|
683
|
+
letter-spacing: 0.5px;
|
|
684
|
+
color: #64748b;
|
|
685
|
+
}
|
|
686
|
+
.sa-bundles__kpi-value {
|
|
687
|
+
font-size: 26px;
|
|
688
|
+
font-weight: 700;
|
|
689
|
+
}
|
|
690
|
+
.sa-bundles__kpi-sub {
|
|
691
|
+
font-size: 11px;
|
|
692
|
+
color: #94a3b8;
|
|
693
|
+
}
|
|
694
|
+
.sa-bundles__filter-row {
|
|
695
|
+
display: flex;
|
|
696
|
+
align-items: center;
|
|
697
|
+
gap: 10px;
|
|
698
|
+
flex-wrap: wrap;
|
|
699
|
+
}
|
|
700
|
+
.sa-bundles__search {
|
|
701
|
+
max-width: 420px;
|
|
702
|
+
flex: 1 1 280px;
|
|
703
|
+
}
|
|
704
|
+
.sa-bundles__status-filter {
|
|
705
|
+
min-width: 220px;
|
|
706
|
+
}
|
|
707
|
+
.sa-bundles__list {
|
|
708
|
+
display: flex;
|
|
709
|
+
flex-direction: column;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
}
|
|
712
|
+
.sa-bd-card {
|
|
713
|
+
background: #fff;
|
|
714
|
+
border: 1px solid #e2e8f0;
|
|
715
|
+
border-radius: 10px;
|
|
716
|
+
overflow: hidden;
|
|
717
|
+
}
|
|
718
|
+
.sa-bd-card.open {
|
|
719
|
+
border-color: #3f6bff;
|
|
720
|
+
}
|
|
721
|
+
.sa-bd-card__head {
|
|
722
|
+
display: flex;
|
|
723
|
+
gap: 12px;
|
|
724
|
+
align-items: center;
|
|
725
|
+
padding: 12px 14px;
|
|
726
|
+
cursor: pointer;
|
|
727
|
+
}
|
|
728
|
+
.sa-bd-card__mark {
|
|
729
|
+
width: 34px;
|
|
730
|
+
height: 34px;
|
|
731
|
+
border-radius: 8px;
|
|
732
|
+
background: #eff6ff;
|
|
733
|
+
color: #1d4ed8;
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
flex-shrink: 0;
|
|
738
|
+
}
|
|
739
|
+
.sa-bd-card__titlewrap {
|
|
740
|
+
flex: 1;
|
|
741
|
+
min-width: 0;
|
|
742
|
+
}
|
|
743
|
+
.sa-bd-card__titlerow {
|
|
744
|
+
display: flex;
|
|
745
|
+
gap: 8px;
|
|
746
|
+
align-items: center;
|
|
747
|
+
}
|
|
748
|
+
.sa-bd-card__key {
|
|
749
|
+
font-weight: 700;
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
font-family: var(--sa-font-mono, ui-monospace, monospace);
|
|
752
|
+
}
|
|
753
|
+
.sa-bd-card__name {
|
|
754
|
+
font-size: 13px;
|
|
755
|
+
font-weight: 600;
|
|
756
|
+
margin-top: 2px;
|
|
757
|
+
}
|
|
758
|
+
.sa-bd-card__desc {
|
|
759
|
+
font-size: 12px;
|
|
760
|
+
color: #64748b;
|
|
761
|
+
}
|
|
762
|
+
.sa-bd-card__chev {
|
|
763
|
+
transition: transform 0.15s;
|
|
764
|
+
color: #94a3b8;
|
|
765
|
+
}
|
|
766
|
+
.sa-bd-card__chev.open {
|
|
767
|
+
transform: rotate(90deg);
|
|
768
|
+
}
|
|
769
|
+
.sa-bd-card__body {
|
|
770
|
+
border-top: 1px solid #e2e8f0;
|
|
771
|
+
padding: 14px;
|
|
772
|
+
background: #f8fafc;
|
|
773
|
+
}
|
|
774
|
+
.sa-bd-grid {
|
|
775
|
+
display: grid;
|
|
776
|
+
grid-template-columns: minmax(280px, 360px) 1fr;
|
|
777
|
+
gap: 18px;
|
|
778
|
+
}
|
|
779
|
+
@media (max-width: 980px) {
|
|
780
|
+
.sa-bd-grid {
|
|
781
|
+
grid-template-columns: 1fr;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
.sa-bd-col {
|
|
785
|
+
display: flex;
|
|
786
|
+
flex-direction: column;
|
|
787
|
+
gap: 8px;
|
|
788
|
+
}
|
|
789
|
+
.sa-bd-col--versions {
|
|
790
|
+
gap: 14px;
|
|
791
|
+
}
|
|
792
|
+
.sa-bd-version-actions {
|
|
793
|
+
display: flex;
|
|
794
|
+
justify-content: space-between;
|
|
795
|
+
gap: 12px;
|
|
796
|
+
margin-top: 4px;
|
|
797
|
+
}
|
|
798
|
+
.sa-bd-section-label {
|
|
799
|
+
font-size: 11px;
|
|
800
|
+
font-weight: 700;
|
|
801
|
+
text-transform: uppercase;
|
|
802
|
+
letter-spacing: 0.5px;
|
|
803
|
+
color: #475569;
|
|
804
|
+
display: flex;
|
|
805
|
+
justify-content: space-between;
|
|
806
|
+
}
|
|
807
|
+
.sa-bd-section-label--mt {
|
|
808
|
+
margin-top: 10px;
|
|
809
|
+
}
|
|
810
|
+
.sa-bd-section-count {
|
|
811
|
+
color: #94a3b8;
|
|
812
|
+
font-weight: 600;
|
|
813
|
+
}
|
|
814
|
+
.sa-bundles__form {
|
|
815
|
+
display: flex;
|
|
816
|
+
flex-direction: column;
|
|
817
|
+
gap: 10px;
|
|
818
|
+
}
|
|
819
|
+
.sa-bd-i18n-hint {
|
|
820
|
+
font-size: 11px;
|
|
821
|
+
color: #94a3b8;
|
|
822
|
+
}
|
|
823
|
+
.sa-bd-i18n-block {
|
|
824
|
+
display: flex;
|
|
825
|
+
flex-direction: column;
|
|
826
|
+
gap: 6px;
|
|
827
|
+
padding: 8px;
|
|
828
|
+
border: 1px solid #e2e8f0;
|
|
829
|
+
border-radius: 8px;
|
|
830
|
+
background: #fff;
|
|
831
|
+
}
|
|
832
|
+
.sa-bd-i18n-head {
|
|
833
|
+
display: flex;
|
|
834
|
+
gap: 8px;
|
|
835
|
+
align-items: center;
|
|
836
|
+
}
|
|
837
|
+
.sa-bd-i18n-code {
|
|
838
|
+
font-size: 10px;
|
|
839
|
+
font-weight: 700;
|
|
840
|
+
background: #f1f5f9;
|
|
841
|
+
padding: 2px 7px;
|
|
842
|
+
border-radius: 5px;
|
|
843
|
+
}
|
|
844
|
+
.sa-bd-i18n-fallback {
|
|
845
|
+
font-size: 10px;
|
|
846
|
+
color: #b45309;
|
|
847
|
+
}
|
|
848
|
+
.sa-bd-save {
|
|
849
|
+
margin-top: 8px;
|
|
850
|
+
align-self: flex-start;
|
|
851
|
+
}
|
|
852
|
+
.sa-bd-delete {
|
|
853
|
+
margin-top: 8px;
|
|
854
|
+
align-self: flex-start;
|
|
855
|
+
}
|
|
856
|
+
.sa-bd-version {
|
|
857
|
+
display: flex;
|
|
858
|
+
gap: 10px;
|
|
859
|
+
align-items: flex-start;
|
|
860
|
+
background: #fff;
|
|
861
|
+
border: 1px solid #e2e8f0;
|
|
862
|
+
border-radius: 8px;
|
|
863
|
+
padding: 10px;
|
|
864
|
+
}
|
|
865
|
+
.sa-bd-version__main {
|
|
866
|
+
flex: 1;
|
|
867
|
+
min-width: 0;
|
|
868
|
+
}
|
|
869
|
+
.sa-bd-version__titlerow {
|
|
870
|
+
display: flex;
|
|
871
|
+
gap: 8px;
|
|
872
|
+
align-items: center;
|
|
873
|
+
}
|
|
874
|
+
.sa-bd-version__sub {
|
|
875
|
+
font-size: 11px;
|
|
876
|
+
color: #64748b;
|
|
877
|
+
margin: 2px 0;
|
|
878
|
+
}
|
|
879
|
+
.sa-bd-version__feats {
|
|
880
|
+
display: flex;
|
|
881
|
+
flex-wrap: wrap;
|
|
882
|
+
gap: 4px;
|
|
883
|
+
}
|
|
884
|
+
.sa-bd-feat-chip {
|
|
885
|
+
font-size: 10px;
|
|
886
|
+
background: #f1f5f9;
|
|
887
|
+
padding: 2px 6px;
|
|
888
|
+
border-radius: 5px;
|
|
889
|
+
}
|
|
890
|
+
.sa-bd-version__act {
|
|
891
|
+
display: flex;
|
|
892
|
+
gap: 2px;
|
|
893
|
+
}
|
|
894
|
+
.sa-bd-empty-row {
|
|
895
|
+
padding: 20px;
|
|
896
|
+
text-align: center;
|
|
897
|
+
color: #94a3b8;
|
|
898
|
+
font-size: 12px;
|
|
899
|
+
border: 1px dashed #cbd5e1;
|
|
900
|
+
border-radius: 8px;
|
|
901
|
+
}
|
|
902
|
+
.sa-bundles__warnings-banner {
|
|
903
|
+
border-left: 4px solid #f2c037;
|
|
904
|
+
}
|
|
905
|
+
.sa-bundles__warnings-list {
|
|
906
|
+
margin: 8px 0 0;
|
|
907
|
+
padding-left: 20px;
|
|
908
|
+
font-size: 13px;
|
|
909
|
+
}
|
|
910
|
+
</style>
|