@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,247 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sa-qc" :class="{ expanded, warn: !quota.usageProvider }">
|
|
3
|
+
<div class="sa-qc__head" @click="emit('toggle')">
|
|
4
|
+
<q-icon
|
|
5
|
+
:name="quota.usageProvider ? 'inventory_2' : 'error'"
|
|
6
|
+
:color="quota.usageProvider ? 'primary' : 'negative'"
|
|
7
|
+
size="20px"
|
|
8
|
+
class="sa-qc__icon"
|
|
9
|
+
/>
|
|
10
|
+
<div class="sa-qc__main">
|
|
11
|
+
<div class="sa-qc__titlerow">
|
|
12
|
+
<span class="sa-qc__label">{{ labelValue || quota.quotaKey }}</span>
|
|
13
|
+
<code class="sa-qc__key">{{ quota.quotaKey }}</code>
|
|
14
|
+
<span class="sa-chip">{{ quota.enforcementMode }}</span>
|
|
15
|
+
<span
|
|
16
|
+
v-if="quota.successorKey"
|
|
17
|
+
class="sa-qc__flag sa-qc__flag--succ"
|
|
18
|
+
:title="`ersetzt durch ${quota.successorKey}`"
|
|
19
|
+
>
|
|
20
|
+
ersetzt durch {{ quota.successorKey }}
|
|
21
|
+
</span>
|
|
22
|
+
<span
|
|
23
|
+
v-if="quota.replaces.length"
|
|
24
|
+
class="sa-qc__flag sa-qc__flag--repl"
|
|
25
|
+
:title="`ersetzt: ${quota.replaces.join(', ')}`"
|
|
26
|
+
>
|
|
27
|
+
ersetzt: {{ quota.replaces.join(', ') }}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="sa-qc__sub">
|
|
31
|
+
Einheit <code>{{ quota.unit }}</code> · UsageProvider
|
|
32
|
+
<code v-if="quota.usageProvider">{{ quota.usageProvider }}</code>
|
|
33
|
+
<span v-else class="sa-qc__missing">fehlt</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div v-if="!quota.usageProvider" class="sa-qc__warning">
|
|
36
|
+
Eine harte Quota ohne UsageProvider ist nicht deploy-fähig (Preflight, SPEC_V2
|
|
37
|
+
§6.3).
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="sa-qc__coverage">
|
|
42
|
+
<span
|
|
43
|
+
v-for="lng in targetLocales"
|
|
44
|
+
:key="lng"
|
|
45
|
+
class="sa-cov-pill"
|
|
46
|
+
:class="coverageClass(coverage(lng))"
|
|
47
|
+
>
|
|
48
|
+
<span>{{ localeShort(lng) }}</span>
|
|
49
|
+
<span>{{ coveragePct(coverage(lng)) }}%</span>
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<DiscoveryStatusControl
|
|
54
|
+
:status="quota.discoveryStatus"
|
|
55
|
+
@set-status="(target) => emit('review', quota.quotaKey, target)"
|
|
56
|
+
/>
|
|
57
|
+
<q-icon name="chevron_right" class="sa-qc__chev" :class="{ open: expanded }" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div v-if="expanded" class="sa-qc__body">
|
|
61
|
+
<div v-if="quota.discoveryStatus === 'outdated'" class="sa-qc__banner">
|
|
62
|
+
<q-icon name="warning" size="16px" />
|
|
63
|
+
<span>
|
|
64
|
+
Die code-abgeleiteten Quota-Fakten (Einheit/Enforcement/Provider) haben sich
|
|
65
|
+
seit der letzten Freigabe geändert — bitte prüfen und <b>erneut freigeben</b>.
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<CatalogEntryTransPanel
|
|
69
|
+
:entry="transEntry"
|
|
70
|
+
:fields="['label', 'unit', 'description']"
|
|
71
|
+
:active-locales="activeLocales"
|
|
72
|
+
@update:base="onTransBase"
|
|
73
|
+
@update:locale="
|
|
74
|
+
(locale, patch) => emit('quota-locale', quota.quotaKey, locale, patch)
|
|
75
|
+
"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { computed, reactive } from 'vue';
|
|
83
|
+
import type {
|
|
84
|
+
CatalogEntryI18nFields,
|
|
85
|
+
DiscoveryStatus,
|
|
86
|
+
QuotaCatalogEntryRow,
|
|
87
|
+
UpdateCatalogEntryBaseData,
|
|
88
|
+
} from '@saasicat/types';
|
|
89
|
+
import CatalogEntryTransPanel from './CatalogEntryTransPanel.vue';
|
|
90
|
+
import DiscoveryStatusControl from './DiscoveryStatusControl.vue';
|
|
91
|
+
import {
|
|
92
|
+
coverageClass,
|
|
93
|
+
coveragePct,
|
|
94
|
+
DISCOVERY_DEFAULT_LOCALE,
|
|
95
|
+
entryCoverage,
|
|
96
|
+
localeShort,
|
|
97
|
+
type TransEntry,
|
|
98
|
+
} from './discovery-ui.js';
|
|
99
|
+
|
|
100
|
+
// Expandable Quota card (#20 Slice 1, sim `QuotaRow`) — same lifecycle as
|
|
101
|
+
// Features. Body = translations (label/unit/description); `unit` is
|
|
102
|
+
// code-derived and only translatable per target locale.
|
|
103
|
+
|
|
104
|
+
const props = defineProps<{
|
|
105
|
+
quota: QuotaCatalogEntryRow;
|
|
106
|
+
activeLocales: string[];
|
|
107
|
+
expanded: boolean;
|
|
108
|
+
}>();
|
|
109
|
+
|
|
110
|
+
const emit = defineEmits<{
|
|
111
|
+
toggle: [];
|
|
112
|
+
review: [key: string, target: DiscoveryStatus];
|
|
113
|
+
'quota-base': [key: string, patch: UpdateCatalogEntryBaseData];
|
|
114
|
+
'quota-locale': [key: string, locale: string, patch: CatalogEntryI18nFields];
|
|
115
|
+
}>();
|
|
116
|
+
|
|
117
|
+
// Draft buffer for the header label — the fields themselves are buffered by
|
|
118
|
+
// the CatalogEntryTransPanel; here only so the title follows along while typing.
|
|
119
|
+
const drafts = reactive<{ label?: string }>({});
|
|
120
|
+
const labelValue = computed(() => drafts.label ?? props.quota.label ?? '');
|
|
121
|
+
|
|
122
|
+
/** Mirror base edits from the translation panel + bubble them up. */
|
|
123
|
+
function onTransBase(patch: { label?: string; description?: string }): void {
|
|
124
|
+
if (patch.label !== undefined) drafts.label = patch.label;
|
|
125
|
+
emit('quota-base', props.quota.quotaKey, patch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const targetLocales = computed(() =>
|
|
129
|
+
props.activeLocales.filter((l) => l !== DISCOVERY_DEFAULT_LOCALE),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const transEntry = computed<TransEntry>(() => ({
|
|
133
|
+
key: props.quota.quotaKey,
|
|
134
|
+
label: labelValue.value,
|
|
135
|
+
description: props.quota.description,
|
|
136
|
+
unit: props.quota.unit,
|
|
137
|
+
i18n: props.quota.i18n ?? {},
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
function coverage(locale: string): number {
|
|
141
|
+
return entryCoverage(transEntry.value, locale, ['label', 'unit', 'description']);
|
|
142
|
+
}
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<style scoped>
|
|
146
|
+
.sa-qc {
|
|
147
|
+
border: 1px solid #e2e8f0;
|
|
148
|
+
border-radius: 10px;
|
|
149
|
+
background: #fff;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
.sa-qc.warn {
|
|
153
|
+
border-color: #fecaca;
|
|
154
|
+
}
|
|
155
|
+
.sa-qc.expanded {
|
|
156
|
+
border-color: #c7d2fe;
|
|
157
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
|
158
|
+
}
|
|
159
|
+
.sa-qc__head {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 12px;
|
|
163
|
+
padding: 10px 12px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
.sa-qc.warn .sa-qc__head {
|
|
167
|
+
background: #fef2f2;
|
|
168
|
+
}
|
|
169
|
+
.sa-qc__icon {
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
}
|
|
172
|
+
.sa-qc__main {
|
|
173
|
+
flex: 1;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
}
|
|
176
|
+
.sa-qc__titlerow {
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: 8px;
|
|
179
|
+
align-items: center;
|
|
180
|
+
flex-wrap: wrap;
|
|
181
|
+
}
|
|
182
|
+
.sa-qc__label {
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
font-size: 13px;
|
|
185
|
+
color: #0f172a;
|
|
186
|
+
}
|
|
187
|
+
.sa-qc__key {
|
|
188
|
+
font-size: 11px;
|
|
189
|
+
}
|
|
190
|
+
.sa-qc__flag {
|
|
191
|
+
font-size: 10px;
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
padding: 1px 6px;
|
|
194
|
+
border-radius: 5px;
|
|
195
|
+
}
|
|
196
|
+
.sa-qc__flag--succ {
|
|
197
|
+
background: #fef3c7;
|
|
198
|
+
color: #92400e;
|
|
199
|
+
}
|
|
200
|
+
.sa-qc__flag--repl {
|
|
201
|
+
background: #dbeafe;
|
|
202
|
+
color: #1e40af;
|
|
203
|
+
}
|
|
204
|
+
.sa-qc__sub {
|
|
205
|
+
font-size: 11px;
|
|
206
|
+
color: #64748b;
|
|
207
|
+
margin-top: 2px;
|
|
208
|
+
}
|
|
209
|
+
.sa-qc__missing {
|
|
210
|
+
color: #b91c1c;
|
|
211
|
+
font-weight: 700;
|
|
212
|
+
}
|
|
213
|
+
.sa-qc__warning {
|
|
214
|
+
font-size: 11px;
|
|
215
|
+
color: #b91c1c;
|
|
216
|
+
margin-top: 4px;
|
|
217
|
+
}
|
|
218
|
+
.sa-qc__coverage {
|
|
219
|
+
display: flex;
|
|
220
|
+
gap: 6px;
|
|
221
|
+
flex-shrink: 0;
|
|
222
|
+
}
|
|
223
|
+
.sa-qc__chev {
|
|
224
|
+
color: #94a3b8;
|
|
225
|
+
transition: transform 0.15s;
|
|
226
|
+
}
|
|
227
|
+
.sa-qc__chev.open {
|
|
228
|
+
transform: rotate(90deg);
|
|
229
|
+
}
|
|
230
|
+
.sa-qc__body {
|
|
231
|
+
border-top: 1px solid #f1f5f9;
|
|
232
|
+
padding: 12px;
|
|
233
|
+
background: #f8fafc;
|
|
234
|
+
}
|
|
235
|
+
.sa-qc__banner {
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 8px;
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
border-radius: 8px;
|
|
241
|
+
padding: 8px 12px;
|
|
242
|
+
margin-bottom: 10px;
|
|
243
|
+
background: #fffbeb;
|
|
244
|
+
border: 1px solid #fde68a;
|
|
245
|
+
color: #92400e;
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sa-stc" @click.stop>
|
|
3
|
+
<span class="sa-review" :class="`sa-review--${status}`">
|
|
4
|
+
{{ STATUS_META[status].label }}
|
|
5
|
+
<q-tooltip>{{ STATUS_META[status].hint }}</q-tooltip>
|
|
6
|
+
</span>
|
|
7
|
+
<q-btn
|
|
8
|
+
dense
|
|
9
|
+
no-caps
|
|
10
|
+
size="sm"
|
|
11
|
+
:unelevated="primary.emphasized"
|
|
12
|
+
:outline="!primary.emphasized"
|
|
13
|
+
:color="primary.emphasized ? 'primary' : undefined"
|
|
14
|
+
:label="primary.label"
|
|
15
|
+
:loading="busy"
|
|
16
|
+
@click="emit('set-status', primary.to)"
|
|
17
|
+
>
|
|
18
|
+
<q-tooltip>{{ STATUS_META[primary.to].hint }}</q-tooltip>
|
|
19
|
+
</q-btn>
|
|
20
|
+
<q-btn v-if="menu.length" dense flat round size="sm" icon="more_horiz">
|
|
21
|
+
<q-menu auto-close anchor="bottom right" self="top right">
|
|
22
|
+
<q-list dense>
|
|
23
|
+
<q-item
|
|
24
|
+
v-for="m in menu"
|
|
25
|
+
:key="m.to"
|
|
26
|
+
clickable
|
|
27
|
+
@click="emit('set-status', m.to)"
|
|
28
|
+
>
|
|
29
|
+
<q-item-section :class="{ 'sa-stc__danger': m.danger }">
|
|
30
|
+
{{ m.label }}
|
|
31
|
+
</q-item-section>
|
|
32
|
+
</q-item>
|
|
33
|
+
</q-list>
|
|
34
|
+
</q-menu>
|
|
35
|
+
</q-btn>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed } from 'vue';
|
|
41
|
+
import type { DiscoveryStatus } from '@saasicat/types';
|
|
42
|
+
import { primaryReviewAction, reviewMenuActions, STATUS_META } from './discovery-ui.js';
|
|
43
|
+
|
|
44
|
+
// StatusControl (#20, design sim): status chip + contextual primary action
|
|
45
|
+
// + kebab menu for the remaining lifecycle transitions. Shared by the feature
|
|
46
|
+
// and quota card; the actual PATCH runs via the `set-status` event
|
|
47
|
+
// in the page (reviewFeature/reviewQuota).
|
|
48
|
+
|
|
49
|
+
const props = defineProps<{
|
|
50
|
+
status: DiscoveryStatus;
|
|
51
|
+
/** Review request in progress — disables the primary action. */
|
|
52
|
+
busy?: boolean;
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
'set-status': [target: DiscoveryStatus];
|
|
57
|
+
}>();
|
|
58
|
+
|
|
59
|
+
const primary = computed(() => primaryReviewAction(props.status));
|
|
60
|
+
const menu = computed(() => reviewMenuActions(props.status));
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.sa-stc {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
}
|
|
70
|
+
.sa-stc__danger {
|
|
71
|
+
color: #b91c1c;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Shared UI helpers for the discovery page (feature/quota cards, orphans,
|
|
2
|
+
// translation panel, StatusControl). In its own file because
|
|
3
|
+
// `<script setup>` allows no named exports and the cards share the same
|
|
4
|
+
// labels/colors/coverage calculations.
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CapabilityCodeStatus,
|
|
8
|
+
CatalogEntryI18n,
|
|
9
|
+
DiscoveryStatus,
|
|
10
|
+
} from '@saasicat/types';
|
|
11
|
+
|
|
12
|
+
/** Translatable fields of a catalog entry. */
|
|
13
|
+
export type I18nField = 'label' | 'description' | 'unit';
|
|
14
|
+
|
|
15
|
+
/** Normalized entry for the translation panel (feature / quota). */
|
|
16
|
+
export interface TransEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
description: string | null;
|
|
20
|
+
/** Quotas only — display unit, code-derived (default locale not editable). */
|
|
21
|
+
unit?: string;
|
|
22
|
+
i18n: CatalogEntryI18n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DISCOVERY_DEFAULT_LOCALE = 'de';
|
|
26
|
+
|
|
27
|
+
const LOCALE_NAMES: Record<string, { short: string; full: string }> = {
|
|
28
|
+
de: { short: 'DE', full: 'Deutsch' },
|
|
29
|
+
en: { short: 'EN', full: 'English' },
|
|
30
|
+
tr: { short: 'TR', full: 'Türkçe' },
|
|
31
|
+
fr: { short: 'FR', full: 'Français' },
|
|
32
|
+
it: { short: 'IT', full: 'Italiano' },
|
|
33
|
+
es: { short: 'ES', full: 'Español' },
|
|
34
|
+
nl: { short: 'NL', full: 'Nederlands' },
|
|
35
|
+
pl: { short: 'PL', full: 'Polski' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function localeShort(locale: string): string {
|
|
39
|
+
return LOCALE_NAMES[locale]?.short ?? locale.toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function localeFull(locale: string): string {
|
|
43
|
+
return LOCALE_NAMES[locale]?.full ?? locale;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function i18nFieldLabel(f: I18nField): string {
|
|
47
|
+
return f === 'label' ? 'Label' : f === 'description' ? 'Beschreibung' : 'Einheit';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Approval lifecycle (#20): status display + state machine ────────────────
|
|
51
|
+
|
|
52
|
+
export const STATUS_META: Record<DiscoveryStatus, { label: string; hint: string }> = {
|
|
53
|
+
pending: {
|
|
54
|
+
label: 'Pending',
|
|
55
|
+
hint: 'Im Code gefunden, noch nicht freigegeben — steht der Planung nicht zur Verfügung.',
|
|
56
|
+
},
|
|
57
|
+
approved: {
|
|
58
|
+
label: 'Approved',
|
|
59
|
+
hint: 'Für Pläne, Bundles & Marketing freigegeben.',
|
|
60
|
+
},
|
|
61
|
+
outdated: {
|
|
62
|
+
label: 'Outdated',
|
|
63
|
+
hint: 'Code hat sich seit der Freigabe geändert — bitte erneut prüfen und freigeben.',
|
|
64
|
+
},
|
|
65
|
+
obsolete: {
|
|
66
|
+
label: 'Obsolete',
|
|
67
|
+
hint: 'Abgekündigt — nicht mehr verwenden, in neuen Plänen ausblenden.',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export interface ReviewAction {
|
|
72
|
+
label: string;
|
|
73
|
+
to: DiscoveryStatus;
|
|
74
|
+
/** Primary style (approve/re-approve) vs. ghost (revoke/reactivate). */
|
|
75
|
+
emphasized: boolean;
|
|
76
|
+
danger?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Primary action per status (design sim `StatusControl`, #20). */
|
|
80
|
+
export function primaryReviewAction(status: DiscoveryStatus): ReviewAction {
|
|
81
|
+
switch (status) {
|
|
82
|
+
case 'pending':
|
|
83
|
+
return { label: 'Freigeben', to: 'approved', emphasized: true };
|
|
84
|
+
case 'approved':
|
|
85
|
+
return { label: 'Freigabe entziehen', to: 'pending', emphasized: false };
|
|
86
|
+
case 'outdated':
|
|
87
|
+
return { label: 'Erneut freigeben', to: 'approved', emphasized: true };
|
|
88
|
+
case 'obsolete':
|
|
89
|
+
return { label: 'Reaktivieren', to: 'pending', emphasized: false };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Kebab menu actions per status (design sim `StatusControl`, #20). */
|
|
94
|
+
export function reviewMenuActions(status: DiscoveryStatus): ReviewAction[] {
|
|
95
|
+
switch (status) {
|
|
96
|
+
case 'pending':
|
|
97
|
+
return [
|
|
98
|
+
{ label: 'Als obsolet markieren', to: 'obsolete', emphasized: false, danger: true },
|
|
99
|
+
];
|
|
100
|
+
case 'approved':
|
|
101
|
+
return [
|
|
102
|
+
{ label: 'Als veraltet markieren', to: 'outdated', emphasized: false },
|
|
103
|
+
{ label: 'Als obsolet markieren', to: 'obsolete', emphasized: false, danger: true },
|
|
104
|
+
];
|
|
105
|
+
case 'outdated':
|
|
106
|
+
return [
|
|
107
|
+
{ label: 'Freigabe entziehen', to: 'pending', emphasized: false },
|
|
108
|
+
{ label: 'Als obsolet markieren', to: 'obsolete', emphasized: false, danger: true },
|
|
109
|
+
];
|
|
110
|
+
case 'obsolete':
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Capability code status (read-only code facts, #20) ──────────────────────
|
|
116
|
+
|
|
117
|
+
const CODE_STATUS_LABELS: Record<CapabilityCodeStatus, string> = {
|
|
118
|
+
active: 'Aktiv',
|
|
119
|
+
experimental: 'Experimental',
|
|
120
|
+
deprecated: 'Deprecated',
|
|
121
|
+
retired: 'Retired',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function codeStatusLabel(status: CapabilityCodeStatus): string {
|
|
125
|
+
return CODE_STATUS_LABELS[status] ?? status;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const KIND_COLORS: Record<string, string> = {
|
|
129
|
+
endpoint: '#2563eb',
|
|
130
|
+
service: '#7c3aed',
|
|
131
|
+
job: '#0891b2',
|
|
132
|
+
event: '#f59e0b',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export function kindStyle(kind: string): Record<string, string> {
|
|
136
|
+
const c = KIND_COLORS[kind] ?? '#64748b';
|
|
137
|
+
return { background: `${c}1a`, color: c, borderColor: `${c}33` };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fraction 0..1 of filled translation fields of an entry in a given locale.
|
|
142
|
+
* Fields without a default value don't count (except `label` — that's mandatory).
|
|
143
|
+
*/
|
|
144
|
+
export function entryCoverage(entry: TransEntry, locale: string, fields: I18nField[]): number {
|
|
145
|
+
let total = 0;
|
|
146
|
+
let filled = 0;
|
|
147
|
+
for (const f of fields) {
|
|
148
|
+
const base =
|
|
149
|
+
f === 'unit'
|
|
150
|
+
? (entry.unit ?? '')
|
|
151
|
+
: ((f === 'label' ? entry.label : entry.description) ?? '');
|
|
152
|
+
if (f !== 'label' && !base.trim()) continue;
|
|
153
|
+
total += 1;
|
|
154
|
+
if ((entry.i18n?.[locale]?.[f] ?? '').trim()) filled += 1;
|
|
155
|
+
}
|
|
156
|
+
return total === 0 ? 1 : filled / total;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function coveragePct(ratio: number): number {
|
|
160
|
+
return Math.round(ratio * 100);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function coverageClass(ratio: number): string {
|
|
164
|
+
const p = coveragePct(ratio);
|
|
165
|
+
return p === 100 ? 'complete' : p >= 50 ? 'warn' : 'low';
|
|
166
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Types of the shared EmailHistoryPage (platform email history) — deliberately
|
|
2
|
+
// extracted from the `.vue` so consumers can import them as regular TS types
|
|
3
|
+
// (a named type import directly from a `.vue` is not resolved through the
|
|
4
|
+
// `*.vue` module shim, but it is from this `.ts`; re-exported via the package
|
|
5
|
+
// index).
|
|
6
|
+
|
|
7
|
+
export type EmailHistoryStatus = 'PENDING' | 'SENT' | 'FAILED' | 'BOUNCED';
|
|
8
|
+
|
|
9
|
+
/** List projection — deliberately without body (loaded only in the detail view). */
|
|
10
|
+
export interface EmailHistoryRow {
|
|
11
|
+
id: string;
|
|
12
|
+
fromEmail: string;
|
|
13
|
+
toEmail: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
status: EmailHistoryStatus;
|
|
16
|
+
sentAt?: string | null;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Complete entry including content, headers, SMTP response and errors. */
|
|
21
|
+
export interface EmailHistoryDetail extends EmailHistoryRow {
|
|
22
|
+
ccEmail?: string | null;
|
|
23
|
+
bccEmail?: string | null;
|
|
24
|
+
bodyHtml?: string | null;
|
|
25
|
+
bodyText?: string | null;
|
|
26
|
+
errorMessage?: string | null;
|
|
27
|
+
smtpResponse?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Search/filter/pagination input — field names as in the backend (QueryEmailLogDto). */
|
|
31
|
+
export interface EmailHistoryFilter {
|
|
32
|
+
search?: string;
|
|
33
|
+
status?: EmailHistoryStatus;
|
|
34
|
+
from?: string;
|
|
35
|
+
to?: string;
|
|
36
|
+
page?: number;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EmailHistoryListResult {
|
|
41
|
+
rows: EmailHistoryRow[];
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EmailHistoryResendResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
message?: string;
|
|
48
|
+
}
|