@saasicat/ui-vue 0.2.0

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