@saasicat/ui-vue 0.7.0 → 0.8.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/dist/{chunk-A3W2N26K.js → chunk-7N3CJTOA.js} +7 -227
- package/dist/{chunk-X5SQ5HMB.js → chunk-TBLXKSTR.js} +0 -137
- package/dist/{chunk-QZOSDCNP.js → chunk-TY2WYKME.js} +1 -1
- package/dist/client/index.cjs +112 -467
- package/dist/client/index.d.cts +4 -62
- package/dist/client/index.d.ts +4 -62
- package/dist/client/index.js +2 -6
- package/dist/index.cjs +204 -583
- package/dist/index.d.cts +10 -55
- package/dist/index.d.ts +10 -55
- package/dist/index.js +74 -101
- package/dist/{messages-7b0P8W75.d.ts → messages-BIYw32u1.d.cts} +0 -67
- package/dist/{messages-7b0P8W75.d.cts → messages-BIYw32u1.d.ts} +0 -67
- package/dist/quasar/index.cjs +0 -136
- package/dist/quasar/index.d.cts +2 -2
- package/dist/quasar/index.d.ts +2 -2
- package/dist/quasar/index.js +2 -2
- package/dist/{use-super-admin-i18n-B3v3-SWZ.d.ts → use-super-admin-i18n-CWNu_W3u.d.ts} +1 -1
- package/dist/{use-super-admin-i18n-DMktRVEt.d.cts → use-super-admin-i18n-DL_qI1BY.d.cts} +1 -1
- package/package.json +2 -2
- package/src/client/i18n/messages/nav.ts +0 -2
- package/src/client/i18n/messages/plan-versions.ts +0 -143
- package/src/client/index.ts +0 -2
- package/src/client/nav-builder.ts +9 -4
- package/src/index.ts +0 -1
- package/src/pages-standard/AdminLayout.vue +1 -1
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +1 -1
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +1 -1
- package/src/pages-standard/plan-versions/catalog-history.types.ts +37 -0
- package/src/pages-standard/plan-versions/format.ts +2 -38
- package/src/client/plan-versions-catalog.ts +0 -380
- package/src/pages-standard/PlanVersionsPage.vue +0 -220
- package/src/pages-standard/plan-versions/PlanVersionHeader.vue +0 -309
- package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +0 -48
- package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +0 -326
- package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +0 -65
- package/src/pages-standard/plan-versions/PlanVersionsList.vue +0 -315
- package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +0 -261
- package/src/pages-standard/plan-versions/types.ts +0 -33
- package/src/vue/use-plan-versions.ts +0 -83
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="sa-pv-kpi">
|
|
3
|
-
<div class="sa-pv-kpi__icon"><q-icon :name="icon" size="18px" /></div>
|
|
4
|
-
<div>
|
|
5
|
-
<div class="sa-pv-kpi__label">{{ label }}</div>
|
|
6
|
-
<div class="sa-pv-kpi__value">
|
|
7
|
-
{{ value }}
|
|
8
|
-
<span v-if="sub" class="sa-pv-kpi__sub">{{ sub }}</span>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
</template>
|
|
13
|
-
|
|
14
|
-
<script setup lang="ts">
|
|
15
|
-
defineProps<{
|
|
16
|
-
label: string;
|
|
17
|
-
value: number | string;
|
|
18
|
-
icon: string;
|
|
19
|
-
sub?: string;
|
|
20
|
-
}>();
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<style scoped>
|
|
24
|
-
.sa-pv-kpi {
|
|
25
|
-
background: #fff;
|
|
26
|
-
border: 1px solid var(--sa-border, #e2e8f0);
|
|
27
|
-
border-radius: 10px;
|
|
28
|
-
padding: 12px 14px;
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: center;
|
|
31
|
-
gap: 12px;
|
|
32
|
-
}
|
|
33
|
-
.sa-pv-kpi__icon {
|
|
34
|
-
width: 36px;
|
|
35
|
-
height: 36px;
|
|
36
|
-
border-radius: 9px;
|
|
37
|
-
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
|
|
38
|
-
display: flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
justify-content: center;
|
|
41
|
-
}
|
|
42
|
-
.sa-pv-kpi__icon :deep(.q-icon) {
|
|
43
|
-
color: var(--sa-primary, #3f6bff);
|
|
44
|
-
}
|
|
45
|
-
.sa-pv-kpi__label {
|
|
46
|
-
font-size: 10.5px;
|
|
47
|
-
font-weight: 700;
|
|
48
|
-
letter-spacing: 0.08em;
|
|
49
|
-
color: var(--sa-muted, #64748b);
|
|
50
|
-
text-transform: uppercase;
|
|
51
|
-
}
|
|
52
|
-
.sa-pv-kpi__value {
|
|
53
|
-
font-family: var(--sa-font-head, system-ui, sans-serif);
|
|
54
|
-
font-weight: 700;
|
|
55
|
-
font-size: 20px;
|
|
56
|
-
color: var(--sa-heading, #0f172a);
|
|
57
|
-
line-height: 1.1;
|
|
58
|
-
margin-top: 2px;
|
|
59
|
-
}
|
|
60
|
-
.sa-pv-kpi__sub {
|
|
61
|
-
font-size: 12px;
|
|
62
|
-
font-weight: 500;
|
|
63
|
-
color: var(--sa-muted, #64748b);
|
|
64
|
-
}
|
|
65
|
-
</style>
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="sa-pv-list">
|
|
3
|
-
<div class="sa-pv-list__kpis">
|
|
4
|
-
<PlanVersionsKpi
|
|
5
|
-
:label="msg.list.kpiPlans"
|
|
6
|
-
:value="snapshot.plans.length"
|
|
7
|
-
icon="inventory_2"
|
|
8
|
-
/>
|
|
9
|
-
<PlanVersionsKpi
|
|
10
|
-
:label="msg.list.kpiSelfService"
|
|
11
|
-
:value="marketedCount"
|
|
12
|
-
icon="storefront"
|
|
13
|
-
:sub="totalPlansLabel"
|
|
14
|
-
/>
|
|
15
|
-
<PlanVersionsKpi
|
|
16
|
-
:label="msg.list.kpiFeaturesTotal"
|
|
17
|
-
:value="totalFeatures"
|
|
18
|
-
icon="extension"
|
|
19
|
-
/>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div v-if="missingPlans.length > 0" class="sa-pv-list__missing">
|
|
23
|
-
<q-icon name="info" size="16px" />
|
|
24
|
-
<div>
|
|
25
|
-
<strong>{{ msg.list.missingPlansLabel }}</strong> {{ missingPlans.join(', ') }}
|
|
26
|
-
{{ msg.list.missingPlansHint }}
|
|
27
|
-
<template v-if="canEdit">
|
|
28
|
-
<button
|
|
29
|
-
v-for="planId in missingPlans"
|
|
30
|
-
:key="`new-${planId}`"
|
|
31
|
-
type="button"
|
|
32
|
-
class="sa-pv-list__missing-btn"
|
|
33
|
-
@click="emit('createPlanDraft', planId)"
|
|
34
|
-
>
|
|
35
|
-
{{ createDraftLabel(planId) }}
|
|
36
|
-
</button>
|
|
37
|
-
</template>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<div class="sa-pv-list__table">
|
|
42
|
-
<slot name="table-head">
|
|
43
|
-
<div
|
|
44
|
-
class="sa-pv-list__head sa-pv-list__row sa-pv-list__row--default"
|
|
45
|
-
:style="rowGridStyle"
|
|
46
|
-
>
|
|
47
|
-
<div>{{ msg.list.colPlan }}</div>
|
|
48
|
-
<div class="sa-pv-list__th--num">{{ msg.list.colPriceNet }}</div>
|
|
49
|
-
<div
|
|
50
|
-
v-for="col in quotaColumns"
|
|
51
|
-
:key="`hd-${col.key}`"
|
|
52
|
-
class="sa-pv-list__th--num"
|
|
53
|
-
>
|
|
54
|
-
{{ col.label }}
|
|
55
|
-
</div>
|
|
56
|
-
<div class="sa-pv-list__th--num">{{ msg.list.colFeatures }}</div>
|
|
57
|
-
</div>
|
|
58
|
-
</slot>
|
|
59
|
-
|
|
60
|
-
<template v-for="(plan, i) in snapshot.plans" :key="plan.planId">
|
|
61
|
-
<slot
|
|
62
|
-
name="row"
|
|
63
|
-
:plan="plan"
|
|
64
|
-
:is-open="openId === plan.planId"
|
|
65
|
-
:is-last="i === snapshot.plans.length - 1"
|
|
66
|
-
:can-edit="canEdit"
|
|
67
|
-
:toggle="() => onToggle(plan.planId)"
|
|
68
|
-
>
|
|
69
|
-
<div
|
|
70
|
-
class="sa-pv-list__default-row sa-pv-list__row sa-pv-list__row--default"
|
|
71
|
-
:style="rowGridStyle"
|
|
72
|
-
>
|
|
73
|
-
<div class="sa-pv-list__default-name">
|
|
74
|
-
<strong>{{ plan.planId }}</strong>
|
|
75
|
-
<span class="sa-pv-list__default-version">v{{ plan.version }}</span>
|
|
76
|
-
<span v-if="plan.isDraft" class="sa-pv-list__default-chip">{{
|
|
77
|
-
msg.status.draft
|
|
78
|
-
}}</span>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="sa-pv-list__th--num">
|
|
81
|
-
{{ formatEuro(plan.monthlyNet) }}
|
|
82
|
-
</div>
|
|
83
|
-
<div
|
|
84
|
-
v-for="col in quotaColumns"
|
|
85
|
-
:key="`val-${plan.planId}-${col.key}`"
|
|
86
|
-
class="sa-pv-list__th--num"
|
|
87
|
-
>
|
|
88
|
-
{{ formatQuotaValue(plan.quotas[col.key], col) }}
|
|
89
|
-
</div>
|
|
90
|
-
<div class="sa-pv-list__th--num">{{ plan.features.length }}</div>
|
|
91
|
-
</div>
|
|
92
|
-
</slot>
|
|
93
|
-
</template>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div v-if="snapshot.kind === 'drafts'" class="sa-pv-list__draft-hint">
|
|
97
|
-
<q-icon name="info" size="18px" />
|
|
98
|
-
<div>
|
|
99
|
-
<strong>{{ msg.list.draftHintLabel }}</strong> {{ msg.list.draftHintBefore }}
|
|
100
|
-
<em>{{ msg.list.draftHintEditButton }}</em> {{ msg.list.draftHintMiddle }}
|
|
101
|
-
<strong>{{ msg.list.draftHintPublishFlow }}</strong>
|
|
102
|
-
{{ msg.list.draftHintAfter }}
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</template>
|
|
107
|
-
|
|
108
|
-
<script setup lang="ts">
|
|
109
|
-
import { computed, ref } from 'vue';
|
|
110
|
-
import type { CatalogSnapshot } from '../../client/plan-versions-catalog.js';
|
|
111
|
-
import { formatMessage } from '../../client/i18n/format.js';
|
|
112
|
-
import { useSaMessages, useSuperAdminI18n } from '../../vue/use-super-admin-i18n.js';
|
|
113
|
-
import PlanVersionsKpi from './PlanVersionsKpi.vue';
|
|
114
|
-
import { fmtEuro } from './format.js';
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Configuration of a quota column (e.g. `users` → "Benutzer"). Optional
|
|
118
|
-
* `fractional: true` → 1 decimal place (storage GB). Optional `unit` →
|
|
119
|
-
* suffix like "GB" after the value. `-1` is rendered as ∞.
|
|
120
|
-
*/
|
|
121
|
-
export interface QuotaColumnConfig {
|
|
122
|
-
key: string;
|
|
123
|
-
label: string;
|
|
124
|
-
fractional?: boolean;
|
|
125
|
-
unit?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const props = defineProps<{
|
|
129
|
-
snapshot: CatalogSnapshot;
|
|
130
|
-
/**
|
|
131
|
-
* Optional: all expected plan IDs. When set, the list shows a
|
|
132
|
-
* warning with "create draft" buttons for missing plans.
|
|
133
|
-
*/
|
|
134
|
-
expectedPlanIds?: readonly string[];
|
|
135
|
-
/**
|
|
136
|
-
* Optional: additional quota columns between "Preis" and "Features".
|
|
137
|
-
* Example: `[{ key: 'users', label: 'Benutzer' },
|
|
138
|
-
* { key: 'members', label: 'Mitglieder' }, { key: 'storageGb',
|
|
139
|
-
* label: 'Speicher (GB)', fractional: true }, { key: 'resources',
|
|
140
|
-
* label: 'Plätze' }]`. Default: no additional columns
|
|
141
|
-
* (backwards-compatible).
|
|
142
|
-
*/
|
|
143
|
-
quotaColumns?: ReadonlyArray<QuotaColumnConfig>;
|
|
144
|
-
}>();
|
|
145
|
-
|
|
146
|
-
const emit = defineEmits<{
|
|
147
|
-
(e: 'editPlanDraft', draftId: string): void;
|
|
148
|
-
(e: 'createPlanDraft', planId: string): void;
|
|
149
|
-
}>();
|
|
150
|
-
|
|
151
|
-
const msg = useSaMessages('planVersions');
|
|
152
|
-
const { locale } = useSuperAdminI18n();
|
|
153
|
-
|
|
154
|
-
const openId = ref<string | null>(null);
|
|
155
|
-
const canEdit = computed(() => props.snapshot.kind === 'drafts');
|
|
156
|
-
|
|
157
|
-
const totalPlansLabel = computed(() =>
|
|
158
|
-
formatMessage(msg.value.list.kpiOfTotal, { count: props.snapshot.plans.length }),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const marketedCount = computed(() => props.snapshot.plans.filter((p) => p.marketed).length);
|
|
162
|
-
const totalFeatures = computed(() => new Set(props.snapshot.plans.flatMap((p) => p.features)).size);
|
|
163
|
-
|
|
164
|
-
const missingPlans = computed<string[]>(() => {
|
|
165
|
-
if (!props.expectedPlanIds) return [];
|
|
166
|
-
const present = new Set(props.snapshot.plans.map((p) => p.planId));
|
|
167
|
-
return props.expectedPlanIds.filter((id) => !present.has(id));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const quotaColumns = computed<ReadonlyArray<QuotaColumnConfig>>(() => props.quotaColumns ?? []);
|
|
171
|
-
|
|
172
|
-
const rowGridStyle = computed(() => {
|
|
173
|
-
if (quotaColumns.value.length === 0) {
|
|
174
|
-
return { gridTemplateColumns: '1.6fr 1fr 0.6fr' };
|
|
175
|
-
}
|
|
176
|
-
// Name (1.6fr), price (1fr), N×quota (0.7fr), features (0.5fr)
|
|
177
|
-
const quotaCols = Array(quotaColumns.value.length).fill('0.7fr').join(' ');
|
|
178
|
-
return { gridTemplateColumns: `1.6fr 1fr ${quotaCols} 0.5fr` };
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
function onToggle(id: string): void {
|
|
182
|
-
openId.value = openId.value === id ? null : id;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function createDraftLabel(planId: string): string {
|
|
186
|
-
return formatMessage(msg.value.list.createDraftFor, { planId });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function formatEuro(n: number): string {
|
|
190
|
-
return fmtEuro(n, locale.value);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function formatQuotaValue(value: number | undefined, col: QuotaColumnConfig): string {
|
|
194
|
-
if (value === undefined) return '–';
|
|
195
|
-
if (value === -1) return '∞';
|
|
196
|
-
const formatted = col.fractional ? value.toFixed(1) : Math.round(value).toString();
|
|
197
|
-
return col.unit ? `${formatted} ${col.unit}` : formatted;
|
|
198
|
-
}
|
|
199
|
-
</script>
|
|
200
|
-
|
|
201
|
-
<style scoped>
|
|
202
|
-
.sa-pv-list {
|
|
203
|
-
padding: 20px 28px;
|
|
204
|
-
}
|
|
205
|
-
.sa-pv-list__kpis {
|
|
206
|
-
display: grid;
|
|
207
|
-
grid-template-columns: repeat(4, 1fr);
|
|
208
|
-
gap: 12px;
|
|
209
|
-
margin-bottom: 18px;
|
|
210
|
-
}
|
|
211
|
-
.sa-pv-list__missing {
|
|
212
|
-
display: flex;
|
|
213
|
-
align-items: flex-start;
|
|
214
|
-
gap: 10px;
|
|
215
|
-
background: var(--sa-warning-soft, rgba(245, 158, 11, 0.1));
|
|
216
|
-
border: 1px solid var(--sa-amber-border, rgba(245, 158, 11, 0.3));
|
|
217
|
-
border-radius: 10px;
|
|
218
|
-
padding: 12px 16px;
|
|
219
|
-
margin-bottom: 14px;
|
|
220
|
-
font-size: 12.5px;
|
|
221
|
-
color: #7c2d12;
|
|
222
|
-
}
|
|
223
|
-
.sa-pv-list__missing :deep(.q-icon) {
|
|
224
|
-
color: var(--sa-warning, #b45309);
|
|
225
|
-
}
|
|
226
|
-
.sa-pv-list__missing-btn {
|
|
227
|
-
background: #fff;
|
|
228
|
-
border: 1px solid var(--sa-amber-border, rgba(245, 158, 11, 0.3));
|
|
229
|
-
color: var(--sa-warning, #b45309);
|
|
230
|
-
border-radius: 6px;
|
|
231
|
-
padding: 3px 8px;
|
|
232
|
-
font-size: 11.5px;
|
|
233
|
-
font-weight: 600;
|
|
234
|
-
cursor: pointer;
|
|
235
|
-
margin-left: 6px;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
.sa-pv-list__table {
|
|
239
|
-
background: #fff;
|
|
240
|
-
border: 1px solid var(--sa-border, #e2e8f0);
|
|
241
|
-
border-radius: 12px;
|
|
242
|
-
overflow: hidden;
|
|
243
|
-
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
244
|
-
}
|
|
245
|
-
.sa-pv-list__row {
|
|
246
|
-
display: grid;
|
|
247
|
-
align-items: center;
|
|
248
|
-
gap: 12px;
|
|
249
|
-
}
|
|
250
|
-
.sa-pv-list__row--default {
|
|
251
|
-
grid-template-columns: 1.6fr 1fr 0.6fr;
|
|
252
|
-
padding: 10px 14px;
|
|
253
|
-
}
|
|
254
|
-
.sa-pv-list__head {
|
|
255
|
-
background: #fafbfc;
|
|
256
|
-
border-bottom: 1px solid var(--sa-border, #e2e8f0);
|
|
257
|
-
font-size: 10.5px;
|
|
258
|
-
font-weight: 700;
|
|
259
|
-
letter-spacing: 0.08em;
|
|
260
|
-
color: var(--sa-muted, #64748b);
|
|
261
|
-
text-transform: uppercase;
|
|
262
|
-
}
|
|
263
|
-
.sa-pv-list__th--num {
|
|
264
|
-
text-align: right;
|
|
265
|
-
font-family: var(--sa-font-mono, ui-monospace, monospace);
|
|
266
|
-
font-size: 13px;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.sa-pv-list__default-row {
|
|
270
|
-
border-bottom: 1px solid var(--sa-border-soft, #f1f5f9);
|
|
271
|
-
}
|
|
272
|
-
.sa-pv-list__default-row:last-child {
|
|
273
|
-
border-bottom: none;
|
|
274
|
-
}
|
|
275
|
-
.sa-pv-list__default-name {
|
|
276
|
-
display: flex;
|
|
277
|
-
align-items: center;
|
|
278
|
-
gap: 8px;
|
|
279
|
-
}
|
|
280
|
-
.sa-pv-list__default-version {
|
|
281
|
-
font-family: var(--sa-font-mono, ui-monospace, monospace);
|
|
282
|
-
font-size: 11px;
|
|
283
|
-
font-weight: 600;
|
|
284
|
-
color: var(--sa-muted, #64748b);
|
|
285
|
-
background: #f1f5f9;
|
|
286
|
-
padding: 2px 6px;
|
|
287
|
-
border-radius: 4px;
|
|
288
|
-
}
|
|
289
|
-
.sa-pv-list__default-chip {
|
|
290
|
-
font-size: 9.5px;
|
|
291
|
-
font-weight: 700;
|
|
292
|
-
letter-spacing: 0.06em;
|
|
293
|
-
padding: 2px 6px;
|
|
294
|
-
border-radius: 4px;
|
|
295
|
-
background: var(--sa-warning-soft, rgba(245, 158, 11, 0.1));
|
|
296
|
-
color: var(--sa-warning, #b45309);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.sa-pv-list__draft-hint {
|
|
300
|
-
margin-top: 14px;
|
|
301
|
-
background: var(--sa-warning-soft, rgba(245, 158, 11, 0.1));
|
|
302
|
-
border: 1px solid var(--sa-amber-border, rgba(245, 158, 11, 0.3));
|
|
303
|
-
border-radius: 10px;
|
|
304
|
-
padding: 12px 16px;
|
|
305
|
-
display: flex;
|
|
306
|
-
align-items: flex-start;
|
|
307
|
-
gap: 12px;
|
|
308
|
-
font-size: 12.5px;
|
|
309
|
-
color: #7c2d12;
|
|
310
|
-
line-height: 1.5;
|
|
311
|
-
}
|
|
312
|
-
.sa-pv-list__draft-hint :deep(.q-icon) {
|
|
313
|
-
color: var(--sa-warning, #b45309);
|
|
314
|
-
}
|
|
315
|
-
</style>
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="sa-pv-matrix">
|
|
3
|
-
<div class="sa-pv-matrix__card">
|
|
4
|
-
<div class="sa-pv-matrix__head" :style="gridStyle">
|
|
5
|
-
<div class="sa-pv-matrix__th">Feature</div>
|
|
6
|
-
<div v-for="plan in snapshot.plans" :key="plan.planId" class="sa-pv-matrix__plan">
|
|
7
|
-
<div class="sa-pv-matrix__plan-name">{{ plan.planId }}</div>
|
|
8
|
-
<div class="sa-pv-matrix__plan-price">
|
|
9
|
-
{{ fmtEuro(plan.monthlyNet) }}/Mo · v{{ plan.version }}
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<template v-for="grp in groupsWithItems" :key="grp.name">
|
|
15
|
-
<div v-if="grp.name" class="sa-pv-matrix__group">{{ grp.name }}</div>
|
|
16
|
-
<div
|
|
17
|
-
v-for="f in grp.items"
|
|
18
|
-
:key="f.key"
|
|
19
|
-
class="sa-pv-matrix__row"
|
|
20
|
-
:style="gridStyle"
|
|
21
|
-
>
|
|
22
|
-
<div class="sa-pv-matrix__feat-cell">
|
|
23
|
-
<div class="sa-pv-matrix__feat-icon">
|
|
24
|
-
<q-icon :name="f.icon" size="14px" />
|
|
25
|
-
</div>
|
|
26
|
-
<div>
|
|
27
|
-
<div class="sa-pv-matrix__feat-label">
|
|
28
|
-
{{ f.label }}
|
|
29
|
-
<span v-if="f.plannedOnly" class="sa-pv-matrix__planned"
|
|
30
|
-
>PLANNED</span
|
|
31
|
-
>
|
|
32
|
-
</div>
|
|
33
|
-
<div class="sa-pv-matrix__feat-key">{{ f.key }}</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
<div
|
|
37
|
-
v-for="plan in snapshot.plans"
|
|
38
|
-
:key="plan.planId"
|
|
39
|
-
class="sa-pv-matrix__cell"
|
|
40
|
-
:class="{ 'sa-pv-matrix__cell--off': !plan.features.includes(f.key) }"
|
|
41
|
-
>
|
|
42
|
-
<span v-if="plan.features.includes(f.key)" class="sa-pv-matrix__check">
|
|
43
|
-
<q-icon name="check" size="14px" />
|
|
44
|
-
</span>
|
|
45
|
-
<span v-else class="sa-pv-matrix__dash" aria-hidden="true" />
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
</template>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
</template>
|
|
52
|
-
|
|
53
|
-
<script setup lang="ts">
|
|
54
|
-
import { computed } from 'vue';
|
|
55
|
-
import type { CatalogSnapshot } from '../../client/plan-versions-catalog.js';
|
|
56
|
-
import { fmtEuro } from './format.js';
|
|
57
|
-
|
|
58
|
-
// Generic feature matrix. Apps pass `featureRegistry` for label/icon/
|
|
59
|
-
// plannedOnly hint and optionally `featureGroups` for the UX grouping of
|
|
60
|
-
// the rows (e.g. Core, Billing, Data, …).
|
|
61
|
-
//
|
|
62
|
-
// Without `featureGroups` the matrix renders a single group without a header.
|
|
63
|
-
|
|
64
|
-
interface FeatureMeta {
|
|
65
|
-
label?: string;
|
|
66
|
-
icon?: string;
|
|
67
|
-
plannedOnly?: boolean;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface FeatureGroupsConfig {
|
|
71
|
-
/** Order of the group headers. */
|
|
72
|
-
order: readonly string[];
|
|
73
|
-
/** Map featureKey → group name. Unassigned features land in "Sonstige". */
|
|
74
|
-
byKey: Record<string, string>;
|
|
75
|
-
/** Default bucket for features without a `byKey` entry. Default: 'Sonstige'. */
|
|
76
|
-
fallbackGroup?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const props = defineProps<{
|
|
80
|
-
snapshot: CatalogSnapshot;
|
|
81
|
-
featureRegistry: Record<string, FeatureMeta>;
|
|
82
|
-
featureGroups?: FeatureGroupsConfig;
|
|
83
|
-
}>();
|
|
84
|
-
|
|
85
|
-
const gridStyle = computed(() => ({
|
|
86
|
-
gridTemplateColumns: `minmax(280px, 1.5fr) repeat(${props.snapshot.plans.length}, minmax(110px, 1fr))`,
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
interface MatrixFeature {
|
|
90
|
-
key: string;
|
|
91
|
-
label: string;
|
|
92
|
-
icon: string;
|
|
93
|
-
plannedOnly: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const groupsWithItems = computed<Array<{ name: string | null; items: MatrixFeature[] }>>(() => {
|
|
97
|
-
const allItems: MatrixFeature[] = Object.entries(props.featureRegistry).map(([key, meta]) => ({
|
|
98
|
-
key,
|
|
99
|
-
label: meta.label ?? key,
|
|
100
|
-
icon: meta.icon ?? 'extension',
|
|
101
|
-
plannedOnly: meta.plannedOnly === true,
|
|
102
|
-
}));
|
|
103
|
-
|
|
104
|
-
if (!props.featureGroups) {
|
|
105
|
-
return allItems.length > 0 ? [{ name: null, items: allItems }] : [];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fallback = props.featureGroups.fallbackGroup ?? 'Sonstige';
|
|
109
|
-
const byGroup = new Map<string, MatrixFeature[]>();
|
|
110
|
-
for (const item of allItems) {
|
|
111
|
-
const group = props.featureGroups.byKey[item.key] ?? fallback;
|
|
112
|
-
const list = byGroup.get(group) ?? [];
|
|
113
|
-
list.push(item);
|
|
114
|
-
byGroup.set(group, list);
|
|
115
|
-
}
|
|
116
|
-
const orderWithFallback = [
|
|
117
|
-
...props.featureGroups.order,
|
|
118
|
-
...(byGroup.has(fallback) && !props.featureGroups.order.includes(fallback)
|
|
119
|
-
? [fallback]
|
|
120
|
-
: []),
|
|
121
|
-
];
|
|
122
|
-
return orderWithFallback.flatMap((g) => {
|
|
123
|
-
const items = byGroup.get(g);
|
|
124
|
-
return items && items.length > 0 ? [{ name: g, items }] : [];
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
</script>
|
|
128
|
-
|
|
129
|
-
<style scoped>
|
|
130
|
-
.sa-pv-matrix {
|
|
131
|
-
padding: 20px 28px;
|
|
132
|
-
}
|
|
133
|
-
.sa-pv-matrix__card {
|
|
134
|
-
background: #fff;
|
|
135
|
-
border: 1px solid var(--sa-border, #e2e8f0);
|
|
136
|
-
border-radius: 12px;
|
|
137
|
-
overflow: hidden;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.sa-pv-matrix__head {
|
|
141
|
-
position: sticky;
|
|
142
|
-
top: 0;
|
|
143
|
-
z-index: 2;
|
|
144
|
-
display: grid;
|
|
145
|
-
background: #fafbfc;
|
|
146
|
-
border-bottom: 1px solid var(--sa-border, #e2e8f0);
|
|
147
|
-
}
|
|
148
|
-
.sa-pv-matrix__th {
|
|
149
|
-
padding: 12px 16px;
|
|
150
|
-
font-size: 11px;
|
|
151
|
-
font-weight: 700;
|
|
152
|
-
letter-spacing: 0.08em;
|
|
153
|
-
color: var(--sa-muted, #64748b);
|
|
154
|
-
text-transform: uppercase;
|
|
155
|
-
border-right: 1px solid var(--sa-border, #e2e8f0);
|
|
156
|
-
}
|
|
157
|
-
.sa-pv-matrix__plan {
|
|
158
|
-
padding: 12px 14px;
|
|
159
|
-
text-align: center;
|
|
160
|
-
border-right: 1px solid var(--sa-border, #e2e8f0);
|
|
161
|
-
}
|
|
162
|
-
.sa-pv-matrix__plan-name {
|
|
163
|
-
font-size: 12.5px;
|
|
164
|
-
font-weight: 700;
|
|
165
|
-
color: var(--sa-heading, #0f172a);
|
|
166
|
-
font-family: var(--sa-font-head, system-ui, sans-serif);
|
|
167
|
-
}
|
|
168
|
-
.sa-pv-matrix__plan-price {
|
|
169
|
-
font-size: 10.5px;
|
|
170
|
-
color: var(--sa-muted, #64748b);
|
|
171
|
-
margin-top: 1px;
|
|
172
|
-
font-family: var(--sa-font-mono, ui-monospace, monospace);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.sa-pv-matrix__group {
|
|
176
|
-
padding: 8px 16px;
|
|
177
|
-
background: var(--sa-border-soft, #f1f5f9);
|
|
178
|
-
font-size: 10.5px;
|
|
179
|
-
font-weight: 700;
|
|
180
|
-
letter-spacing: 0.08em;
|
|
181
|
-
color: var(--sa-muted, #64748b);
|
|
182
|
-
text-transform: uppercase;
|
|
183
|
-
border-bottom: 1px solid var(--sa-border, #e2e8f0);
|
|
184
|
-
}
|
|
185
|
-
.sa-pv-matrix__row {
|
|
186
|
-
display: grid;
|
|
187
|
-
border-bottom: 1px solid var(--sa-border-soft, #f1f5f9);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.sa-pv-matrix__feat-cell {
|
|
191
|
-
padding: 10px 16px;
|
|
192
|
-
display: flex;
|
|
193
|
-
align-items: center;
|
|
194
|
-
gap: 10px;
|
|
195
|
-
border-right: 1px solid var(--sa-border-soft, #f1f5f9);
|
|
196
|
-
}
|
|
197
|
-
.sa-pv-matrix__feat-icon {
|
|
198
|
-
width: 24px;
|
|
199
|
-
height: 24px;
|
|
200
|
-
border-radius: 6px;
|
|
201
|
-
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
|
|
202
|
-
display: flex;
|
|
203
|
-
align-items: center;
|
|
204
|
-
justify-content: center;
|
|
205
|
-
flex-shrink: 0;
|
|
206
|
-
}
|
|
207
|
-
.sa-pv-matrix__feat-icon :deep(.q-icon) {
|
|
208
|
-
color: var(--sa-primary, #3f6bff);
|
|
209
|
-
}
|
|
210
|
-
.sa-pv-matrix__feat-label {
|
|
211
|
-
font-size: 12.5px;
|
|
212
|
-
font-weight: 600;
|
|
213
|
-
color: var(--sa-heading, #0f172a);
|
|
214
|
-
display: flex;
|
|
215
|
-
align-items: center;
|
|
216
|
-
gap: 5px;
|
|
217
|
-
}
|
|
218
|
-
.sa-pv-matrix__planned {
|
|
219
|
-
font-size: 9px;
|
|
220
|
-
font-weight: 700;
|
|
221
|
-
letter-spacing: 0.06em;
|
|
222
|
-
background: #fef3c7;
|
|
223
|
-
color: #92400e;
|
|
224
|
-
padding: 1px 5px;
|
|
225
|
-
border-radius: 3px;
|
|
226
|
-
}
|
|
227
|
-
.sa-pv-matrix__feat-key {
|
|
228
|
-
font-size: 10.5px;
|
|
229
|
-
color: var(--sa-muted, #64748b);
|
|
230
|
-
margin-top: 1px;
|
|
231
|
-
font-family: var(--sa-font-mono, ui-monospace, monospace);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.sa-pv-matrix__cell {
|
|
235
|
-
display: flex;
|
|
236
|
-
align-items: center;
|
|
237
|
-
justify-content: center;
|
|
238
|
-
border-right: 1px solid var(--sa-border-soft, #f1f5f9);
|
|
239
|
-
}
|
|
240
|
-
.sa-pv-matrix__cell--off {
|
|
241
|
-
background: #fafbfc;
|
|
242
|
-
}
|
|
243
|
-
.sa-pv-matrix__check {
|
|
244
|
-
width: 22px;
|
|
245
|
-
height: 22px;
|
|
246
|
-
border-radius: 50%;
|
|
247
|
-
background: var(--sa-positive-soft, rgba(4, 120, 87, 0.1));
|
|
248
|
-
display: flex;
|
|
249
|
-
align-items: center;
|
|
250
|
-
justify-content: center;
|
|
251
|
-
}
|
|
252
|
-
.sa-pv-matrix__check :deep(.q-icon) {
|
|
253
|
-
color: var(--sa-positive, #047857);
|
|
254
|
-
}
|
|
255
|
-
.sa-pv-matrix__dash {
|
|
256
|
-
width: 22px;
|
|
257
|
-
height: 22px;
|
|
258
|
-
border-radius: 50%;
|
|
259
|
-
border: 1px dashed var(--sa-border, #e2e8f0);
|
|
260
|
-
}
|
|
261
|
-
</style>
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Shared types of the plan version views.
|
|
2
|
-
//
|
|
3
|
-
// Phase 2c: ported from a consumer admin;
|
|
4
|
-
// source type generic over the platform version rows.
|
|
5
|
-
|
|
6
|
-
import type { PlanVersionRow } from '@saasicat/types';
|
|
7
|
-
|
|
8
|
-
export type PlanVersionViewMode = 'list' | 'matrix' | 'audit';
|
|
9
|
-
|
|
10
|
-
export type DraftKind = 'plan';
|
|
11
|
-
|
|
12
|
-
export interface DraftEntry {
|
|
13
|
-
kind: DraftKind;
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
detail: string;
|
|
17
|
-
source: PlanVersionRow;
|
|
18
|
-
baseId: string | null;
|
|
19
|
-
hasChangeNote: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ValidationCheck {
|
|
23
|
-
ok: boolean;
|
|
24
|
-
label: string;
|
|
25
|
-
detail: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PublishResult {
|
|
29
|
-
id: string;
|
|
30
|
-
label: string;
|
|
31
|
-
ok: boolean;
|
|
32
|
-
error?: string;
|
|
33
|
-
}
|