@saasicat/ui-vue 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/{catalog-JXdKdIqG.d.cts → catalog-DJcfm5N2.d.cts} +21 -7
  2. package/dist/{catalog-JXdKdIqG.d.ts → catalog-DJcfm5N2.d.ts} +21 -7
  3. package/dist/{chunk-DPTIOQXX.js → chunk-F7NRM6KI.js} +56 -28
  4. package/dist/{chunk-RVG6LQHL.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-ORX3ZG47.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +134 -29
  7. package/dist/client/index.d.cts +53 -3
  8. package/dist/client/index.d.ts +53 -3
  9. package/dist/client/index.js +14 -2
  10. package/dist/index.cjs +321 -162
  11. package/dist/index.d.cts +30 -8
  12. package/dist/index.d.ts +30 -8
  13. package/dist/index.js +176 -133
  14. package/dist/quasar/index.cjs +78 -29
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +6 -2
  18. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  19. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  20. package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
  22. package/package.json +18 -8
  23. package/src/client/i18n/messages/bundles.ts +0 -2
  24. package/src/client/i18n/messages/common.ts +24 -0
  25. package/src/client/i18n/messages/discovery.ts +2 -2
  26. package/src/client/i18n/messages/marketing.ts +2 -4
  27. package/src/client/i18n/messages/plan-detail.ts +0 -4
  28. package/src/client/i18n/messages/plans.ts +20 -8
  29. package/src/client/i18n/messages/tenants.ts +0 -2
  30. package/src/client/index.ts +1 -0
  31. package/src/client/manifest-loader.ts +1 -1
  32. package/src/client/resolve-plans.ts +144 -0
  33. package/src/components/BundleVersionPublishDialog.vue +6 -6
  34. package/src/components/KvBlock.vue +4 -4
  35. package/src/components/MarketingPromotionsTab.vue +34 -48
  36. package/src/components/MfaPromptDialog.vue +8 -2
  37. package/src/components/TenantActionConfirmDialog.vue +4 -4
  38. package/src/components/VersionDiffPreview.vue +7 -7
  39. package/src/components/admin-page/AdminBody.vue +40 -0
  40. package/src/components/admin-page/AdminFilters.vue +12 -0
  41. package/src/components/admin-page/AdminHero.vue +49 -0
  42. package/src/components/admin-page/AdminKpi.vue +84 -0
  43. package/src/components/admin-page/AdminPage.vue +16 -0
  44. package/src/components/admin-page/AdminPaginator.vue +195 -0
  45. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  46. package/src/components/admin-page/AdminSection.vue +53 -0
  47. package/src/components/admin-page/AdminStatistics.vue +40 -0
  48. package/src/components/admin-page/AdminTable.vue +175 -0
  49. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  50. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  51. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  52. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  53. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  54. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  55. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  56. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  57. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  58. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  59. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  60. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  61. package/src/components/plan/PlanGrid.vue +1 -1
  62. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  63. package/src/components/plan-detail/PlanAuditLog.vue +7 -8
  64. package/src/components/plan-detail/PlanDetail.vue +95 -292
  65. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  66. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  67. package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
  68. package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
  69. package/src/components/plan-list/PlanList.vue +206 -457
  70. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  71. package/src/components/plan-review/PlanReview.vue +191 -275
  72. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  73. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  74. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  75. package/src/index.ts +1 -0
  76. package/src/pages-standard/AdminLayout.vue +38 -12
  77. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  78. package/src/pages-standard/AuditPage.vue +65 -109
  79. package/src/pages-standard/BundlesPage.vue +146 -179
  80. package/src/pages-standard/DashboardPage.vue +59 -154
  81. package/src/pages-standard/DiscoveryPage.vue +172 -227
  82. package/src/pages-standard/EmailHistoryPage.vue +118 -159
  83. package/src/pages-standard/MarketingCatalogPage.vue +399 -440
  84. package/src/pages-standard/PilotsPage.vue +69 -117
  85. package/src/pages-standard/PlansPage.vue +240 -112
  86. package/src/pages-standard/PlatformEmailPage.vue +56 -91
  87. package/src/pages-standard/PromoCodeDetailPage.vue +49 -74
  88. package/src/pages-standard/PromoCodesPage.vue +95 -128
  89. package/src/pages-standard/SubscriptionsPage.vue +19 -52
  90. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  91. package/src/pages-standard/SuperAdminSetupWizard.vue +22 -18
  92. package/src/pages-standard/TenantDetailPage.vue +100 -191
  93. package/src/pages-standard/TenantsPage.vue +190 -295
  94. package/src/pages-standard/UsersPage.vue +89 -157
  95. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  96. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  97. package/src/pages-standard/bundles-page/BundlesToolbar.vue +63 -0
  98. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  99. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  100. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  101. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  102. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  103. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
  104. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +43 -62
  105. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
  106. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
  107. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  108. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  109. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  110. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  111. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  112. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  113. package/src/pages-standard/sa-theme.css +552 -86
  114. package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
  115. package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
  116. package/src/pages-standard/tenant-detail/TenantUsers.vue +15 -0
  117. package/src/pages-standard/tenant-detail/types.ts +38 -0
  118. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  119. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  120. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  121. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  122. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  123. package/src/quasar/create-super-admin-app.ts +4 -0
  124. package/src/vue/entitlement-provider.ts +0 -1
  125. package/src/vue/feature-router-guard.ts +0 -1
  126. package/src/vue/super-admin-context.ts +106 -1
  127. package/src/vue/use-api-list.ts +2 -1
  128. package/src/vue/use-mfa-prompt.ts +72 -0
  129. package/src/vue/use-pagination.ts +63 -0
  130. package/src/vue/use-plans.ts +1 -2
  131. package/src/vue/use-sign-out.ts +90 -0
  132. package/src/vue/use-tenant-billing.ts +1 -1
  133. package/src/vue/use-tenant-manifest.ts +0 -1
  134. package/src/components/KpiCard.vue +0 -53
  135. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1012
  136. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  137. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -100
  138. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  139. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  140. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  141. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -251
  142. package/src/components/plan-cockpit/types.ts +0 -72
  143. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
  144. package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
  145. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
@@ -1,1012 +0,0 @@
1
- <template>
2
- <div class="pc">
3
- <PlanCockpitHeader
4
- :plan="plan"
5
- :accent-bg="accentBg"
6
- :accent-fg="accentFg"
7
- :has-open-draft="hasOpenDraft"
8
- @back="$emit('back')"
9
- @clone-plan="$emit('clonePlan')"
10
- @view-catalog="$emit('viewCatalog')"
11
- @create-draft="$emit('createDraft')"
12
- />
13
-
14
- <PlanCockpitKpis
15
- :live-version="liveVersion"
16
- :tenant-impact-total="tenantImpactTotal"
17
- :versions-count="versions.length"
18
- :published-count="publishedCount"
19
- :draft-count="draftCount"
20
- :draft-version="draftVersion"
21
- />
22
-
23
- <!-- Two columns -->
24
- <div class="pc-grid">
25
- <PlanCockpitVersions
26
- :versions="versions"
27
- :sorted-versions="sortedVersions"
28
- :timeline-segments="timelineSegments"
29
- :timeline-axis="timelineAxis"
30
- :has-open-draft="hasOpenDraft"
31
- :next-draft-version="nextDraftVersion"
32
- :impact-by-version="impactByVersion"
33
- :draft-version="draftVersion"
34
- @create-draft="$emit('createDraft')"
35
- @publish="$emit('publish', $event)"
36
- @edit-draft="$emit('editDraft', $event)"
37
- @view-diff="forwardViewDiff"
38
- />
39
-
40
- <!-- RIGHT — Diff + Impact -->
41
- <div class="pc-right-col">
42
- <PlanCockpitDiffPanel :diff-pair="diffPair" :diff="diff" :diff-rows="diffRows" />
43
-
44
- <PlanCockpitImpactPanel
45
- :tenant-impact="tenantImpact"
46
- :tenant-impact-total="tenantImpactTotal"
47
- />
48
- </div>
49
- </div>
50
-
51
- <PlanCockpitAuditLog :audit-rows="auditRows" :loading-audit="loadingAudit" />
52
- </div>
53
- </template>
54
-
55
- <script setup lang="ts">
56
- import { computed } from 'vue';
57
- import type { PlanRow, PlanVersionRow } from '@saasicat/types';
58
- import { formatMessage } from '../../client/i18n/format.js';
59
- import { formatCurrency } from '../../client/i18n/currency.js';
60
- import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
61
- import PlanCockpitAuditLog from './PlanCockpitAuditLog.vue';
62
- import PlanCockpitDiffPanel from './PlanCockpitDiffPanel.vue';
63
- import PlanCockpitHeader from './PlanCockpitHeader.vue';
64
- import PlanCockpitImpactPanel from './PlanCockpitImpactPanel.vue';
65
- import PlanCockpitKpis from './PlanCockpitKpis.vue';
66
- import PlanCockpitVersions from './PlanCockpitVersions.vue';
67
- import type {
68
- AuditRow,
69
- DiffRow,
70
- DiffSummary,
71
- DiscoveryQuota,
72
- FeatureMeta,
73
- PlanVersionPair,
74
- TenantImpact,
75
- TimelineSegment,
76
- } from './types';
77
-
78
- // PlanCockpit — V3 drill-in for a single plan: version timeline,
79
- // diff draft→live, tenant impact, audit log. Data comes from the consumer
80
- // (PlansPage). The diff is computed locally from two PlanVersionRows.
81
-
82
- const props = withDefaults(
83
- defineProps<{
84
- plan: PlanRow;
85
- versions: PlanVersionRow[];
86
- /** Accent color for the plan header. */
87
- accent?: string;
88
- /** Optional: tenant impact data for the publish preview. */
89
- tenantImpact?: TenantImpact | null;
90
- /** Tenant count per version (for the "Impact" column in the versions table). */
91
- impactByVersion?: Record<number, number>;
92
- /** Audit log entries for this plan. */
93
- auditRows?: AuditRow[];
94
- loadingAudit?: boolean;
95
- /** Discovery quotas (for diff labels). */
96
- availableQuotas?: DiscoveryQuota[];
97
- featureRegistry?: Record<string, FeatureMeta>;
98
- }>(),
99
- {
100
- accent: '#2563eb',
101
- tenantImpact: null,
102
- impactByVersion: () => ({}),
103
- auditRows: () => [],
104
- loadingAudit: false,
105
- availableQuotas: () => [],
106
- featureRegistry: () => ({}),
107
- },
108
- );
109
-
110
- const emit = defineEmits<{
111
- (e: 'back'): void;
112
- (e: 'createDraft'): void;
113
- (e: 'editDraft', version: PlanVersionRow): void;
114
- (e: 'publish', version: PlanVersionRow): void;
115
- (e: 'clonePlan'): void;
116
- (e: 'viewCatalog'): void;
117
- (e: 'viewDiff', from: PlanVersionRow, to: PlanVersionRow): void;
118
- }>();
119
-
120
- const msg = useSaMessages('planDetail');
121
- const { locale } = useSuperAdminI18n();
122
-
123
- function forwardViewDiff(from: PlanVersionRow, to: PlanVersionRow): void {
124
- emit('viewDiff', from, to);
125
- }
126
-
127
- const accentBg = computed(() => hexToTint(props.accent, 0.15));
128
- const accentFg = computed(() => props.accent);
129
-
130
- function hexToTint(hex: string, opacity: number): string {
131
- if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '#dbeafe';
132
- const r = parseInt(hex.slice(1, 3), 16);
133
- const g = parseInt(hex.slice(3, 5), 16);
134
- const b = parseInt(hex.slice(5, 7), 16);
135
- return `rgba(${r}, ${g}, ${b}, ${opacity})`;
136
- }
137
-
138
- const sortedVersions = computed<PlanVersionRow[]>(() =>
139
- [...props.versions].sort((a, b) => b.version - a.version),
140
- );
141
-
142
- const liveVersion = computed<PlanVersionRow | null>(
143
- () => props.versions.find((v) => v.publishedAt !== null && v.supersededAt === null) ?? null,
144
- );
145
-
146
- const draftVersion = computed<PlanVersionRow | null>(
147
- () => props.versions.find((v) => v.publishedAt === null) ?? null,
148
- );
149
-
150
- const hasOpenDraft = computed(() => draftVersion.value !== null);
151
-
152
- const publishedCount = computed(() => props.versions.filter((v) => v.publishedAt !== null).length);
153
- const draftCount = computed(() => props.versions.filter((v) => v.publishedAt === null).length);
154
-
155
- const nextDraftVersion = computed(() => {
156
- const max = props.versions.reduce((m, v) => Math.max(m, v.version), 0);
157
- return max + 1;
158
- });
159
-
160
- const tenantImpactTotal = computed(() => {
161
- if (props.tenantImpact) {
162
- return props.tenantImpact.auto + props.tenantImpact.review + props.tenantImpact.conflict;
163
- }
164
- return Object.values(props.impactByVersion).reduce((s, n) => s + n, 0);
165
- });
166
-
167
- // ── Timeline ────────────────────────────────────────────────────────
168
- const timelineSegments = computed<TimelineSegment[]>(() => {
169
- return [...props.versions]
170
- .sort((a, b) => a.version - b.version)
171
- .map((v) => ({
172
- key: v.id,
173
- version: v.version,
174
- status:
175
- v.publishedAt === null ? 'draft' : v.supersededAt !== null ? 'supersed' : 'live',
176
- flex: 1,
177
- }));
178
- });
179
-
180
- const timelineAxis = computed(() => {
181
- const labels: string[] = [];
182
- for (const v of [...props.versions].sort((a, b) => a.version - b.version)) {
183
- if (v.validFrom) labels.push(v.validFrom.slice(0, 7));
184
- }
185
- labels.push(msg.value.versions.timelineNow);
186
- return labels;
187
- });
188
-
189
- // ── Formatting ──────────────────────────────────────────────────────
190
- function formatMoney(raw: string | number): string {
191
- const num = typeof raw === 'string' ? Number(raw) : raw;
192
- if (!Number.isFinite(num)) return String(raw);
193
- return formatCurrency(num, locale.value);
194
- }
195
-
196
- // ── Diff ────────────────────────────────────────────────────────────
197
- const diffPair = computed<PlanVersionPair | null>(() => {
198
- const to = draftVersion.value ?? sortedVersions.value[0] ?? null;
199
- if (!to) return null;
200
- const from = sortedVersions.value.find((v) => v.version < to.version && v.publishedAt !== null);
201
- if (!from || from.id === to.id) return null;
202
- return { from, to };
203
- });
204
-
205
- function quotasOf(v: PlanVersionRow): Record<string, number> {
206
- if (v.quotas && Object.keys(v.quotas).length > 0) return v.quotas;
207
- const legacy: Record<string, number> = {};
208
- if (typeof v.maxUsers === 'number') legacy.users = v.maxUsers;
209
- if (typeof v.maxVehicles === 'number') legacy.vehicles = v.maxVehicles;
210
- if (typeof v.maxStorageGb === 'number') legacy.storageGb = v.maxStorageGb;
211
- return legacy;
212
- }
213
-
214
- function featureLabel(key: string): string {
215
- return props.featureRegistry[key]?.label ?? key;
216
- }
217
- function quotaLabel(key: string): string {
218
- return props.availableQuotas.find((q) => q.quotaKey === key)?.label || key;
219
- }
220
- function quotaUnit(key: string): string {
221
- return props.availableQuotas.find((q) => q.quotaKey === key)?.unit || '';
222
- }
223
-
224
- const diff = computed<DiffSummary>(() => {
225
- if (!diffPair.value) {
226
- return {
227
- addedFeatures: [] as string[],
228
- removedFeatures: [] as string[],
229
- addedQuotas: [] as Array<{ key: string; value: number }>,
230
- removedQuotas: [] as Array<{ key: string; value: number }>,
231
- changedQuotas: [] as Array<{ key: string; from: number; to: number }>,
232
- priceChanged: false,
233
- priceFrom: '',
234
- priceTo: '',
235
- };
236
- }
237
- const { from, to } = diffPair.value;
238
- const addedFeatures = to.features.filter((f) => !from.features.includes(f));
239
- const removedFeatures = from.features.filter((f) => !to.features.includes(f));
240
- const fromQ = quotasOf(from);
241
- const toQ = quotasOf(to);
242
- const addedQuotas: Array<{ key: string; value: number }> = [];
243
- const removedQuotas: Array<{ key: string; value: number }> = [];
244
- const changedQuotas: Array<{ key: string; from: number; to: number }> = [];
245
- const keys = new Set([...Object.keys(fromQ), ...Object.keys(toQ)]);
246
- for (const k of keys) {
247
- const fv = fromQ[k];
248
- const tv = toQ[k];
249
- if (fv === undefined && tv !== undefined) addedQuotas.push({ key: k, value: tv });
250
- else if (fv !== undefined && tv === undefined) removedQuotas.push({ key: k, value: fv });
251
- else if (fv !== undefined && tv !== undefined && fv !== tv) {
252
- changedQuotas.push({ key: k, from: fv, to: tv });
253
- }
254
- }
255
- const priceChanged = from.monthlyNet !== to.monthlyNet || from.yearlyNet !== to.yearlyNet;
256
- return {
257
- addedFeatures,
258
- removedFeatures,
259
- addedQuotas,
260
- removedQuotas,
261
- changedQuotas,
262
- priceChanged,
263
- priceFrom: formatPricePair(from),
264
- priceTo: formatPricePair(to),
265
- };
266
- });
267
-
268
- function formatPricePair(version: PlanVersionRow): string {
269
- return formatMessage(msg.value.diff.pricePair, {
270
- monthly: formatMoney(version.monthlyNet),
271
- yearly: formatMoney(version.yearlyNet),
272
- });
273
- }
274
-
275
- const STYLES = {
276
- added: { bg: '#ecfdf5', border: '#a7f3d0', color: '#047857', sign: '+' },
277
- removed: { bg: '#fef2f2', border: '#fecaca', color: '#b91c1c', sign: '−' },
278
- changed: { bg: '#fffbeb', border: '#fde68a', color: '#b45309', sign: '~' },
279
- } as const;
280
-
281
- const diffRows = computed<DiffRow[]>(() => {
282
- const texts = msg.value.diff;
283
- const out: DiffRow[] = [];
284
- for (const f of diff.value.addedFeatures) {
285
- out.push({
286
- id: `add-f-${f}`,
287
- section: texts.sectionLabelFeatures,
288
- label: featureLabel(f),
289
- sub: f,
290
- tag: texts.tagNew,
291
- ...STYLES.added,
292
- });
293
- }
294
- for (const f of diff.value.removedFeatures) {
295
- out.push({
296
- id: `rem-f-${f}`,
297
- section: texts.sectionLabelFeatures,
298
- label: featureLabel(f),
299
- sub: f,
300
- tag: texts.tagRemoved,
301
- ...STYLES.removed,
302
- });
303
- }
304
- for (const q of diff.value.changedQuotas) {
305
- const unit = quotaUnit(q.key);
306
- out.push({
307
- id: `chg-q-${q.key}`,
308
- section: texts.sectionLabelQuotas,
309
- label: quotaLabel(q.key),
310
- sub: q.key,
311
- from: `${q.from}${unit ? ' ' + unit : ''}`,
312
- to: `${q.to}${unit ? ' ' + unit : ''}`,
313
- tag: texts.tagChanged,
314
- ...STYLES.changed,
315
- });
316
- }
317
- for (const q of diff.value.addedQuotas) {
318
- const unit = quotaUnit(q.key);
319
- out.push({
320
- id: `add-q-${q.key}`,
321
- section: texts.sectionLabelQuotas,
322
- label: quotaLabel(q.key),
323
- sub: q.key,
324
- to: `${q.value}${unit ? ' ' + unit : ''}`,
325
- tag: texts.tagNew,
326
- ...STYLES.added,
327
- });
328
- }
329
- for (const q of diff.value.removedQuotas) {
330
- const unit = quotaUnit(q.key);
331
- out.push({
332
- id: `rem-q-${q.key}`,
333
- section: texts.sectionLabelQuotas,
334
- label: quotaLabel(q.key),
335
- sub: q.key,
336
- from: `${q.value}${unit ? ' ' + unit : ''}`,
337
- tag: texts.tagRemoved,
338
- ...STYLES.removed,
339
- });
340
- }
341
- if (diff.value.priceChanged) {
342
- out.push({
343
- id: 'chg-price',
344
- section: texts.sectionLabelPrice,
345
- label: texts.priceYearMonthLabel,
346
- from: diff.value.priceFrom,
347
- to: diff.value.priceTo,
348
- tag: texts.tagChanged,
349
- ...STYLES.changed,
350
- });
351
- }
352
- return out;
353
- });
354
- </script>
355
-
356
- <style>
357
- .pc {
358
- --pc-bg: #f6f7f9;
359
- --pc-surface: #ffffff;
360
- --pc-surface-2: #f8fafc;
361
- --pc-border: #e5e7eb;
362
- --pc-border-strong: #d1d5db;
363
- --pc-text: #0f172a;
364
- --pc-text-2: #475569;
365
- --pc-text-3: #94a3b8;
366
- --pc-primary: #2563eb;
367
- --pc-live: #10b981;
368
- --pc-live-bg: #ecfdf5;
369
- --pc-draft: #f59e0b;
370
- --pc-draft-bg: #fffbeb;
371
- --pc-supersed: #94a3b8;
372
- --pc-supersed-bg: #f1f5f9;
373
- --pc-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
374
- --pc-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
375
-
376
- padding: 18px 26px;
377
- background: var(--pc-bg);
378
- color: var(--pc-text);
379
- font-family: var(--pc-font-sans);
380
- min-height: 100%;
381
- box-sizing: border-box;
382
- }
383
- .pc * {
384
- box-sizing: border-box;
385
- }
386
- .pc-mono {
387
- font: 500 11px var(--pc-font-mono);
388
- }
389
- .pc-mono--xs {
390
- font-size: 10.5px;
391
- color: #64748b;
392
- }
393
-
394
- /* Header */
395
- .pc-header {
396
- display: flex;
397
- align-items: center;
398
- gap: 16px;
399
- margin-bottom: 18px;
400
- }
401
- .pc-header-left {
402
- display: flex;
403
- align-items: center;
404
- gap: 14px;
405
- flex: 1;
406
- }
407
- .pc-header-titles {
408
- min-width: 0;
409
- }
410
- .pc-header-right {
411
- display: flex;
412
- gap: 8px;
413
- }
414
- .pc-backbar {
415
- margin-bottom: 12px;
416
- }
417
- .pc-back {
418
- display: inline-flex;
419
- align-items: center;
420
- gap: 6px;
421
- padding: 6px 12px 6px 8px;
422
- border-radius: 7px;
423
- background: transparent;
424
- border: 1px solid transparent;
425
- color: var(--pc-text-2);
426
- font: 500 13px var(--pc-font-sans);
427
- cursor: pointer;
428
- transition: background 0.12s;
429
- }
430
- .pc-back:hover {
431
- background: rgba(15, 23, 42, 0.05);
432
- }
433
- .pc-back-ico {
434
- display: inline-flex;
435
- transform: rotate(180deg);
436
- }
437
- .pc-bigchip {
438
- padding: 6px 12px;
439
- border-radius: 7px;
440
- font: 700 12px var(--pc-font-mono);
441
- letter-spacing: 0.06em;
442
- }
443
- .pc-h-title {
444
- font-size: 24px;
445
- font-weight: 700;
446
- letter-spacing: -0.02em;
447
- margin: 0;
448
- }
449
- .pc-h-sub {
450
- font-size: 12.5px;
451
- color: var(--pc-text-2);
452
- margin: 3px 0 0;
453
- }
454
-
455
- /* Buttons + chips */
456
- .pc-btn {
457
- display: inline-flex;
458
- align-items: center;
459
- gap: 7px;
460
- padding: 8px 14px;
461
- border-radius: 7px;
462
- font: 500 13px var(--pc-font-sans);
463
- cursor: pointer;
464
- border: 1px solid var(--pc-border-strong);
465
- background: #fff;
466
- color: var(--pc-text);
467
- transition:
468
- background 0.12s,
469
- border-color 0.12s;
470
- }
471
- .pc-btn:hover:not(:disabled) {
472
- background: var(--pc-surface-2);
473
- }
474
- .pc-btn:disabled {
475
- cursor: not-allowed;
476
- opacity: 0.5;
477
- }
478
- .pc-btn--primary {
479
- background: var(--pc-primary);
480
- border-color: var(--pc-primary);
481
- color: #fff;
482
- }
483
- .pc-btn--primary:hover:not(:disabled) {
484
- background: #1d4ed8;
485
- }
486
- .pc-btn--sm {
487
- padding: 5px 9px;
488
- font-size: 12px;
489
- gap: 5px;
490
- }
491
-
492
- .pc-chip {
493
- display: inline-flex;
494
- align-items: center;
495
- gap: 5px;
496
- padding: 2px 8px;
497
- border-radius: 999px;
498
- font-size: 11px;
499
- font-weight: 600;
500
- background: var(--pc-surface-2);
501
- color: var(--pc-text-2);
502
- border: 1px solid var(--pc-border);
503
- }
504
- .pc-chip--mid {
505
- vertical-align: middle;
506
- margin-left: 6px;
507
- }
508
- .pc-chip--live {
509
- background: var(--pc-live-bg);
510
- color: #047857;
511
- border-color: #a7f3d0;
512
- }
513
- .pc-chip--draft {
514
- background: var(--pc-draft-bg);
515
- color: #b45309;
516
- border-color: #fde68a;
517
- }
518
- .pc-chip--supersed {
519
- background: var(--pc-supersed-bg);
520
- color: var(--pc-text-2);
521
- border-color: #cbd5e1;
522
- }
523
- .pc-chip--conflict {
524
- background: #fef2f2;
525
- color: #b91c1c;
526
- border-color: #fecaca;
527
- }
528
- .pc-chip--dot::before {
529
- content: '';
530
- width: 6px;
531
- height: 6px;
532
- border-radius: 50%;
533
- background: currentColor;
534
- }
535
-
536
- /* KPI strip */
537
- .pc-kpis {
538
- display: grid;
539
- grid-template-columns: repeat(4, 1fr);
540
- gap: 12px;
541
- margin-bottom: 18px;
542
- }
543
- .pc-kpi {
544
- background: #fff;
545
- border: 1px solid var(--pc-border);
546
- border-radius: 10px;
547
- padding: 14px 16px;
548
- }
549
- .pc-kpi--draft {
550
- background: linear-gradient(180deg, var(--pc-draft-bg) 0%, #fff 100%);
551
- border-color: #fde68a;
552
- }
553
- .pc-kpi-label {
554
- font-size: 11px;
555
- text-transform: uppercase;
556
- letter-spacing: 0.08em;
557
- color: #64748b;
558
- font-weight: 600;
559
- }
560
- .pc-kpi-val {
561
- font-size: 24px;
562
- font-weight: 700;
563
- letter-spacing: -0.02em;
564
- margin-top: 4px;
565
- }
566
- .pc-kpi-sub {
567
- font-size: 11.5px;
568
- color: var(--pc-text-3);
569
- margin-top: 2px;
570
- }
571
-
572
- /* Grid */
573
- .pc-grid {
574
- display: grid;
575
- grid-template-columns: 1.45fr 1fr;
576
- gap: 14px;
577
- }
578
- .pc-right-col {
579
- display: flex;
580
- flex-direction: column;
581
- gap: 14px;
582
- }
583
- .pc-card {
584
- background: #fff;
585
- border: 1px solid var(--pc-border);
586
- border-radius: 10px;
587
- overflow: hidden;
588
- }
589
- .pc-card-head {
590
- padding: 14px 18px;
591
- border-bottom: 1px solid var(--pc-border);
592
- display: flex;
593
- align-items: center;
594
- gap: 10px;
595
- }
596
- .pc-card-head--audit {
597
- border-bottom: 0;
598
- padding-bottom: 8px;
599
- }
600
- .pc-card-head-text {
601
- flex: 1;
602
- min-width: 0;
603
- }
604
- .pc-card-title {
605
- font-size: 14px;
606
- font-weight: 700;
607
- letter-spacing: -0.01em;
608
- }
609
- .pc-card-sub {
610
- font-size: 11.5px;
611
- color: #64748b;
612
- margin-top: 2px;
613
- }
614
- .pc-pillrow {
615
- display: flex;
616
- gap: 4px;
617
- }
618
- .pc-pill {
619
- font-size: 10.5px;
620
- font-weight: 600;
621
- padding: 2px 8px;
622
- border-radius: 4px;
623
- }
624
-
625
- /* Timeline */
626
- .pc-vtimeline {
627
- padding: 16px 18px 6px;
628
- }
629
- .pc-vtl-track {
630
- display: flex;
631
- gap: 2px;
632
- height: 26px;
633
- border-radius: 6px;
634
- overflow: hidden;
635
- background: #f1f5f9;
636
- }
637
- .pc-vtl-seg {
638
- display: grid;
639
- place-items: center;
640
- font: 600 10.5px var(--pc-font-mono);
641
- letter-spacing: 0.06em;
642
- }
643
- .pc-vtl-supersed {
644
- background: #e2e8f0;
645
- color: var(--pc-text-2);
646
- }
647
- .pc-vtl-live {
648
- background: var(--pc-live);
649
- color: #fff;
650
- }
651
- .pc-vtl-draft {
652
- background: repeating-linear-gradient(45deg, #fcd34d, #fcd34d 4px, #fde68a 4px, #fde68a 8px);
653
- color: #78350f;
654
- }
655
- .pc-vtl-axis {
656
- display: flex;
657
- justify-content: space-between;
658
- margin-top: 6px;
659
- font: 500 10px var(--pc-font-mono);
660
- color: var(--pc-text-3);
661
- }
662
-
663
- /* Version row — flexible columns so the action buttons (Publish /
664
- Edit) aren't clipped on a narrow card. Actions =
665
- `auto` (content-width), the rest flexes. */
666
- .pc-vrow {
667
- display: grid;
668
- grid-template-columns:
669
- 92px
670
- minmax(112px, 1fr)
671
- minmax(82px, 0.8fr)
672
- 56px
673
- minmax(90px, 1.3fr)
674
- auto;
675
- align-items: center;
676
- gap: 10px;
677
- padding: 11px 18px;
678
- border-top: 1px solid #f1f5f9;
679
- }
680
- .pc-vrow--head {
681
- background: #fbfbfd;
682
- padding: 8px 18px;
683
- }
684
- .pc-vrow--active {
685
- background: var(--pc-draft-bg);
686
- }
687
- .pc-vrow--active:hover {
688
- background: #fef3c7;
689
- }
690
- .pc-vrow:hover:not(.pc-vrow--head):not(.pc-vrow--active) {
691
- background: #fcfcfd;
692
- }
693
- .pc-vrow-headlabel {
694
- font-size: 10px;
695
- text-transform: uppercase;
696
- letter-spacing: 0.08em;
697
- color: var(--pc-text-3);
698
- }
699
- .pc-vrow-version {
700
- display: flex;
701
- align-items: center;
702
- gap: 8px;
703
- }
704
- .pc-vrow-vlabel {
705
- font: 700 14px var(--pc-font-mono);
706
- color: var(--pc-text);
707
- }
708
- .pc-vrow-cell {
709
- min-width: 0;
710
- }
711
- .pc-vrow-cell--right {
712
- text-align: right;
713
- }
714
- .pc-vrow-validity {
715
- font-size: 12.5px;
716
- font-weight: 500;
717
- color: var(--pc-text);
718
- }
719
- .pc-vrow-arrow {
720
- color: var(--pc-text-3);
721
- margin: 0 4px;
722
- }
723
- .pc-vrow-inf {
724
- color: var(--pc-live);
725
- font-weight: 700;
726
- }
727
- .pc-vrow-sub {
728
- font-size: 10.5px;
729
- color: var(--pc-text-3);
730
- margin-top: 2px;
731
- }
732
- .pc-vrow-price {
733
- font-size: 13px;
734
- font-weight: 700;
735
- color: var(--pc-text);
736
- }
737
- .pc-vrow-price-unit {
738
- font-weight: 500;
739
- color: var(--pc-text-3);
740
- font-size: 11px;
741
- }
742
- .pc-vrow-impact {
743
- font-size: 13px;
744
- font-weight: 700;
745
- color: var(--pc-text);
746
- }
747
- .pc-vrow-impact--zero {
748
- color: #cbd5e1;
749
- }
750
- .pc-vrow-note {
751
- font-size: 12px;
752
- color: var(--pc-text-2);
753
- min-width: 0;
754
- overflow: hidden;
755
- text-overflow: ellipsis;
756
- white-space: nowrap;
757
- }
758
- .pc-vrow-actions {
759
- display: flex;
760
- gap: 6px;
761
- justify-content: flex-end;
762
- flex-wrap: wrap;
763
- }
764
- .pc-vrow-actions .pc-btn {
765
- white-space: nowrap;
766
- }
767
-
768
- /* Diff */
769
- .pc-diff-body {
770
- padding: 12px 16px 16px;
771
- display: flex;
772
- flex-direction: column;
773
- gap: 6px;
774
- }
775
- .pc-diff-row {
776
- display: flex;
777
- align-items: center;
778
- gap: 10px;
779
- padding: 9px 11px;
780
- border: 1px solid;
781
- border-radius: 7px;
782
- }
783
- .pc-diff-sign {
784
- width: 20px;
785
- height: 20px;
786
- border-radius: 5px;
787
- color: #fff;
788
- font-weight: 800;
789
- font-size: 13px;
790
- display: grid;
791
- place-items: center;
792
- flex: 0 0 auto;
793
- }
794
- .pc-diff-main {
795
- flex: 1;
796
- min-width: 0;
797
- }
798
- .pc-diff-headline {
799
- display: flex;
800
- align-items: baseline;
801
- gap: 8px;
802
- flex-wrap: wrap;
803
- }
804
- .pc-diff-section {
805
- font-size: 10.5px;
806
- text-transform: uppercase;
807
- letter-spacing: 0.08em;
808
- font-weight: 700;
809
- }
810
- .pc-diff-label {
811
- font-size: 13px;
812
- font-weight: 600;
813
- color: var(--pc-text);
814
- }
815
- .pc-diff-change {
816
- display: flex;
817
- align-items: center;
818
- gap: 8px;
819
- margin-top: 3px;
820
- font-size: 12px;
821
- }
822
- .pc-diff-from {
823
- text-decoration: line-through;
824
- color: var(--pc-text-3);
825
- }
826
- .pc-diff-to {
827
- color: var(--pc-text);
828
- font-weight: 600;
829
- }
830
- .pc-diff-arrow {
831
- display: inline-flex;
832
- }
833
- .pc-diff-tag {
834
- font-size: 10px;
835
- }
836
-
837
- /* Impact */
838
- .pc-impact-strip {
839
- display: flex;
840
- align-items: center;
841
- gap: 20px;
842
- padding: 16px 18px;
843
- border-bottom: 1px solid #f1f5f9;
844
- }
845
- .pc-impact-num {
846
- font-size: 44px;
847
- font-weight: 700;
848
- letter-spacing: -0.03em;
849
- color: var(--pc-text);
850
- line-height: 1;
851
- }
852
- .pc-impact-bars {
853
- flex: 1;
854
- display: flex;
855
- flex-direction: column;
856
- gap: 6px;
857
- }
858
- .pc-impact-bar {
859
- display: flex;
860
- align-items: center;
861
- gap: 10px;
862
- height: 22px;
863
- position: relative;
864
- }
865
- .pc-impact-bar-fill {
866
- height: 8px;
867
- border-radius: 999px;
868
- min-width: 16px;
869
- }
870
- .pc-impact-bar-label {
871
- font-size: 11.5px;
872
- color: var(--pc-text-2);
873
- }
874
- .pc-impact-tenants {
875
- padding: 12px 18px 16px;
876
- display: flex;
877
- flex-direction: column;
878
- gap: 4px;
879
- }
880
- .pc-impact-tenant {
881
- display: flex;
882
- align-items: center;
883
- gap: 10px;
884
- padding: 7px 9px;
885
- border-radius: 6px;
886
- }
887
- .pc-impact-tenant:hover {
888
- background: var(--pc-surface-2);
889
- }
890
- .pc-impact-avatar {
891
- width: 28px;
892
- height: 28px;
893
- border-radius: 6px;
894
- background: #f1f5f9;
895
- color: var(--pc-text-2);
896
- display: grid;
897
- place-items: center;
898
- font-size: 11px;
899
- font-weight: 700;
900
- flex: 0 0 auto;
901
- }
902
- .pc-impact-body {
903
- flex: 1;
904
- min-width: 0;
905
- }
906
- .pc-impact-name {
907
- font-size: 13px;
908
- font-weight: 600;
909
- color: var(--pc-text);
910
- overflow: hidden;
911
- text-overflow: ellipsis;
912
- white-space: nowrap;
913
- }
914
- .pc-impact-sub {
915
- font: 500 10.5px var(--pc-font-mono);
916
- color: var(--pc-text-3);
917
- }
918
-
919
- /* Audit */
920
- .pc-audit-card {
921
- margin-top: 14px;
922
- }
923
- .pc-audit {
924
- padding: 4px 18px 16px;
925
- }
926
- .pc-audit-row {
927
- display: flex;
928
- align-items: center;
929
- gap: 14px;
930
- padding: 9px 0;
931
- border-top: 1px solid #f1f5f9;
932
- }
933
- .pc-audit-row:first-child {
934
- border-top: 0;
935
- }
936
- .pc-audit-dot {
937
- width: 7px;
938
- height: 7px;
939
- border-radius: 50%;
940
- flex: 0 0 auto;
941
- }
942
- .pc-audit-draft {
943
- background: #f59e0b;
944
- }
945
- .pc-audit-add {
946
- background: #10b981;
947
- }
948
- .pc-audit-change {
949
- background: #2563eb;
950
- }
951
- .pc-audit-publish {
952
- background: #8b5cf6;
953
- }
954
- .pc-audit-remove {
955
- background: #ef4444;
956
- }
957
- .pc-audit-when {
958
- font-size: 11.5px;
959
- color: #64748b;
960
- min-width: 130px;
961
- }
962
- .pc-audit-avatar {
963
- width: 24px;
964
- height: 24px;
965
- border-radius: 50%;
966
- background: var(--pc-text);
967
- color: #fff;
968
- display: grid;
969
- place-items: center;
970
- font: 700 11px var(--pc-font-sans);
971
- flex: 0 0 auto;
972
- }
973
- .pc-audit-who {
974
- font-size: 12px;
975
- color: var(--pc-text-2);
976
- min-width: 64px;
977
- }
978
- .pc-audit-what {
979
- font-size: 13px;
980
- color: var(--pc-text);
981
- flex: 1;
982
- }
983
-
984
- /* Empty */
985
- .pc-empty {
986
- padding: 22px 18px;
987
- text-align: center;
988
- color: var(--pc-text-3);
989
- font-style: italic;
990
- font-size: 13px;
991
- }
992
- .pc-empty--inline {
993
- padding: 12px;
994
- }
995
-
996
- @media (max-width: 1100px) {
997
- .pc-grid {
998
- grid-template-columns: 1fr;
999
- }
1000
- .pc-kpis {
1001
- grid-template-columns: repeat(2, 1fr);
1002
- }
1003
- .pc-vrow {
1004
- grid-template-columns: 88px 1fr 1fr 52px minmax(80px, 1.2fr);
1005
- gap: 8px;
1006
- }
1007
- .pc-vrow-actions {
1008
- grid-column: 1 / -1;
1009
- justify-content: flex-start;
1010
- }
1011
- }
1012
- </style>