@saasicat/ui-vue 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,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>