@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,171 @@
1
+ <template>
2
+ <div v-if="capabilities.length === 0" class="sa-caps__empty">
3
+ <q-icon name="warning" size="14px" />
4
+ Feature im Katalog ohne implementierende Capability — blockiert im blocking-Strict-Mode das
5
+ Plan-Publish.
6
+ </div>
7
+ <div v-else class="sa-caps">
8
+ <div
9
+ v-for="cap in capabilities"
10
+ :key="cap.capabilityKey"
11
+ class="sa-caps-row"
12
+ :class="{ dep: cap.codeStatus === 'deprecated', gone: cap.codeStatus === 'retired' }"
13
+ >
14
+ <span class="sa-caps-row__kind" :style="kindStyle(cap.kind)">{{ cap.kind }}</span>
15
+ <div class="sa-caps-row__main">
16
+ <div class="sa-caps-row__titlerow">
17
+ <code class="sa-caps-row__key">{{ cap.capabilityKey }}</code>
18
+ <span v-if="isNew(cap)" class="sa-caps-row__flag sa-caps-row__flag--new">
19
+ neu
20
+ </span>
21
+ <span
22
+ v-if="cap.codeStatus === 'experimental'"
23
+ class="sa-caps-row__flag sa-caps-row__flag--exp"
24
+ >
25
+ experimental
26
+ </span>
27
+ <span
28
+ v-if="cap.codeStatus === 'deprecated'"
29
+ class="sa-caps-row__flag sa-caps-row__flag--dep"
30
+ >
31
+ deprecated
32
+ <template v-if="cap.replacementKey">
33
+ → <code>{{ cap.replacementKey }}</code>
34
+ </template>
35
+ </span>
36
+ <span
37
+ v-if="cap.codeStatus === 'retired'"
38
+ class="sa-caps-row__flag sa-caps-row__flag--gone"
39
+ >
40
+ aus Code entfernt
41
+ </span>
42
+ </div>
43
+ <div class="sa-caps-row__meta">
44
+ <span v-if="cap.label">{{ cap.label }}</span>
45
+ <span v-if="declaredAtByKey[cap.capabilityKey]">
46
+ <span class="sa-muted">impl</span>
47
+ <code>{{ declaredAtByKey[cap.capabilityKey] }}</code>
48
+ </span>
49
+ <span v-if="cap.owner">
50
+ <span class="sa-muted">Owner</span>
51
+ <code>{{ cap.owner }}</code>
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import type { CapabilityCatalogEntryRow } from '@saasicat/types';
61
+ import { kindStyle } from './discovery-ui.js';
62
+
63
+ // Read-only Capability list: code facts from the scan (#20). Embedded in the
64
+ // master-data subtab of the feature card as well as in the page's orphan bucket.
65
+
66
+ const props = defineProps<{
67
+ capabilities: CapabilityCatalogEntryRow[];
68
+ declaredAtByKey: Record<string, string>;
69
+ /**
70
+ * Baseline for the "new" flag — Capabilities with a later `createdAt`
71
+ * count as new since the last feature approval (`approvedAt`).
72
+ */
73
+ newSince?: string | null;
74
+ }>();
75
+
76
+ function isNew(cap: CapabilityCatalogEntryRow): boolean {
77
+ return Boolean(props.newSince && cap.createdAt > props.newSince);
78
+ }
79
+ </script>
80
+
81
+ <style scoped>
82
+ .sa-caps {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 6px;
86
+ }
87
+ .sa-caps-row {
88
+ display: flex;
89
+ gap: 10px;
90
+ align-items: flex-start;
91
+ background: #fff;
92
+ border: 1px solid #e2e8f0;
93
+ border-radius: 8px;
94
+ padding: 8px 10px;
95
+ }
96
+ .sa-caps-row.dep {
97
+ border-color: #fecaca;
98
+ background: #fef2f2;
99
+ }
100
+ .sa-caps-row__kind {
101
+ font-size: 9px;
102
+ font-weight: 700;
103
+ text-transform: uppercase;
104
+ padding: 2px 6px;
105
+ border-radius: 5px;
106
+ border: 1px solid;
107
+ flex-shrink: 0;
108
+ margin-top: 1px;
109
+ }
110
+ .sa-caps-row__main {
111
+ flex: 1;
112
+ min-width: 0;
113
+ }
114
+ .sa-caps-row__titlerow {
115
+ display: flex;
116
+ gap: 6px;
117
+ align-items: center;
118
+ flex-wrap: wrap;
119
+ }
120
+ .sa-caps-row__key {
121
+ font-size: 11px;
122
+ font-weight: 700;
123
+ }
124
+ .sa-caps-row__flag {
125
+ font-size: 9px;
126
+ font-weight: 700;
127
+ padding: 1px 5px;
128
+ border-radius: 4px;
129
+ }
130
+ .sa-caps-row__flag--new {
131
+ background: #fef3c7;
132
+ color: #92400e;
133
+ }
134
+ .sa-caps-row__flag--exp {
135
+ background: #ede9fe;
136
+ color: #6d28d9;
137
+ }
138
+ .sa-caps-row__flag--dep {
139
+ background: #fee2e2;
140
+ color: #b91c1c;
141
+ }
142
+ .sa-caps-row__flag--gone {
143
+ background: #e2e8f0;
144
+ color: #64748b;
145
+ }
146
+ .sa-caps-row.gone {
147
+ opacity: 0.6;
148
+ }
149
+ .sa-caps-row__meta {
150
+ display: flex;
151
+ gap: 12px;
152
+ flex-wrap: wrap;
153
+ font-size: 11px;
154
+ color: #475569;
155
+ margin-top: 1px;
156
+ }
157
+ .sa-caps-row__meta code {
158
+ font-size: 10px;
159
+ }
160
+ .sa-caps__empty {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 6px;
164
+ font-size: 11px;
165
+ color: #b45309;
166
+ background: #fffbeb;
167
+ border: 1px dashed #fde68a;
168
+ border-radius: 8px;
169
+ padding: 10px 12px;
170
+ }
171
+ </style>
@@ -0,0 +1,410 @@
1
+ <template>
2
+ <div class="sa-fc" :class="{ expanded }">
3
+ <div class="sa-fc__head" @click="emit('toggle')">
4
+ <q-icon :name="iconValue || 'help_outline'" size="22px" class="sa-fc__icon" />
5
+ <div class="sa-fc__main">
6
+ <div class="sa-fc__titlerow">
7
+ <code class="sa-fc__key">{{ feature.featureKey }}</code>
8
+ <span class="sa-fc__label">{{ labelValue || feature.featureKey }}</span>
9
+ <span v-if="newCapsCount > 0" class="sa-fc__flag sa-fc__flag--new">
10
+ {{ newCapsCount }} neu
11
+ </span>
12
+ <span v-if="deprecatedCapsCount > 0" class="sa-fc__flag sa-fc__flag--dep">
13
+ {{ deprecatedCapsCount }} deprecated
14
+ </span>
15
+ <span
16
+ v-if="feature.successorKey"
17
+ class="sa-fc__flag sa-fc__flag--succ"
18
+ :title="`ersetzt durch ${feature.successorKey}`"
19
+ >
20
+ ersetzt durch {{ feature.successorKey }}
21
+ </span>
22
+ <span
23
+ v-if="feature.replaces.length"
24
+ class="sa-fc__flag sa-fc__flag--repl"
25
+ :title="`ersetzt: ${feature.replaces.join(', ')}`"
26
+ >
27
+ ersetzt: {{ feature.replaces.join(', ') }}
28
+ </span>
29
+ </div>
30
+ <div class="sa-fc__sub">
31
+ <template v-if="owners.length">
32
+ <span class="sa-muted">Owner</span>
33
+ {{ owners.join(', ') }}
34
+ <span class="sa-fc__dot">·</span>
35
+ </template>
36
+ {{ capabilities.length }}
37
+ Capabilit{{ capabilities.length === 1 ? 'y' : 'ies' }}
38
+ <template v-if="tierValue">
39
+ <span class="sa-fc__dot">·</span>
40
+ <span class="sa-fc__tier">{{ tierValue }}</span>
41
+ </template>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="sa-fc__coverage">
46
+ <span
47
+ v-for="lng in targetLocales"
48
+ :key="lng"
49
+ class="sa-cov-pill"
50
+ :class="coverageClass(coverage(lng))"
51
+ >
52
+ <span>{{ localeShort(lng) }}</span>
53
+ <span>{{ coveragePct(coverage(lng)) }}%</span>
54
+ </span>
55
+ </div>
56
+
57
+ <DiscoveryStatusControl
58
+ :status="feature.discoveryStatus"
59
+ @set-status="(target) => emit('review', feature.featureKey, target)"
60
+ />
61
+ <q-icon name="chevron_right" class="sa-fc__chev" :class="{ open: expanded }" />
62
+ </div>
63
+
64
+ <div v-if="expanded" class="sa-fc__body">
65
+ <div v-if="feature.discoveryStatus === 'outdated'" class="sa-fc__banner warn">
66
+ <q-icon name="warning" size="16px" />
67
+ <span>
68
+ Die Implementierung hat sich seit der letzten Freigabe geändert. Bitte
69
+ Stammdaten &amp; Capabilities prüfen und <b>erneut freigeben</b>.
70
+ </span>
71
+ </div>
72
+ <div v-if="feature.discoveryStatus === 'obsolete'" class="sa-fc__banner mute">
73
+ <q-icon name="info" size="16px" />
74
+ <span>
75
+ Dieses Feature ist abgekündigt. Es sollte in neuen Plänen nicht mehr verwendet
76
+ werden.
77
+ </span>
78
+ </div>
79
+
80
+ <q-tabs v-model="sub" dense align="left" class="sa-fc__subtabs">
81
+ <q-tab name="stamm" label="Stammdaten" />
82
+ <q-tab name="i18n" label="Übersetzungen" />
83
+ </q-tabs>
84
+
85
+ <div v-if="sub === 'stamm'" class="sa-fc__split">
86
+ <div class="sa-fc__split-col">
87
+ <div class="sa-fc__split-head">Einstellungen</div>
88
+ <div class="sa-fc__fields">
89
+ <div class="sa-fc-field">
90
+ <label class="sa-fc-field__cap">Icon (Quasar-Icon-Name)</label>
91
+ <q-input
92
+ dense
93
+ outlined
94
+ :model-value="iconValue"
95
+ placeholder="z. B. directions_car"
96
+ @update:model-value="(v) => onField('icon', String(v ?? ''))"
97
+ >
98
+ <template #prepend>
99
+ <q-icon :name="iconValue || 'help_outline'" />
100
+ </template>
101
+ </q-input>
102
+ </div>
103
+ <div class="sa-fc-field">
104
+ <label class="sa-fc-field__cap">Tier</label>
105
+ <q-select
106
+ dense
107
+ outlined
108
+ clearable
109
+ use-input
110
+ new-value-mode="add-unique"
111
+ :options="TIER_OPTIONS"
112
+ :model-value="tierValue || null"
113
+ @update:model-value="(v) => onField('tier', String(v ?? ''))"
114
+ />
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <div class="sa-fc__split-col">
119
+ <div class="sa-fc__split-head">
120
+ Code Capabilities
121
+ <span class="sa-fc__split-count">
122
+ {{ capabilities.length }} · read-only
123
+ </span>
124
+ </div>
125
+ <DiscoveryCapList
126
+ :capabilities="capabilities"
127
+ :declared-at-by-key="declaredAtByKey"
128
+ :new-since="feature.approvedAt"
129
+ />
130
+ </div>
131
+ </div>
132
+
133
+ <CatalogEntryTransPanel
134
+ v-else
135
+ :entry="transEntry"
136
+ :fields="['label', 'description']"
137
+ :active-locales="activeLocales"
138
+ @update:base="onTransBase"
139
+ @update:locale="
140
+ (locale, patch) => emit('feature-locale', feature.featureKey, locale, patch)
141
+ "
142
+ />
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ import { computed, reactive, ref } from 'vue';
149
+ import type {
150
+ CapabilityCatalogEntryRow,
151
+ CatalogEntryI18nFields,
152
+ DiscoveryStatus,
153
+ FeatureCatalogEntryRow,
154
+ UpdateCatalogEntryBaseData,
155
+ } from '@saasicat/types';
156
+ import CatalogEntryTransPanel from './CatalogEntryTransPanel.vue';
157
+ import DiscoveryCapList from './DiscoveryCapList.vue';
158
+ import DiscoveryStatusControl from './DiscoveryStatusControl.vue';
159
+ import {
160
+ coverageClass,
161
+ coveragePct,
162
+ DISCOVERY_DEFAULT_LOCALE,
163
+ entryCoverage,
164
+ localeShort,
165
+ type TransEntry,
166
+ } from './discovery-ui.js';
167
+
168
+ // Expandable feature card (#20, Sim `FeatureCard`): header with
169
+ // StatusControl (approval state machine) + i18n coverage, body with subtabs
170
+ // master data (settings + read-only code capabilities) and translations.
171
+
172
+ const props = defineProps<{
173
+ feature: FeatureCatalogEntryRow;
174
+ /** Capabilities of this feature (read-only code facts). */
175
+ capabilities: CapabilityCatalogEntryRow[];
176
+ /** Owner rollup from the capabilities (#14), most frequent first — computed by the page. */
177
+ owners: string[];
178
+ declaredAtByKey: Record<string, string>;
179
+ activeLocales: string[];
180
+ expanded: boolean;
181
+ }>();
182
+
183
+ const emit = defineEmits<{
184
+ toggle: [];
185
+ review: [key: string, target: DiscoveryStatus];
186
+ 'feature-base': [key: string, patch: UpdateCatalogEntryBaseData];
187
+ 'feature-locale': [key: string, locale: string, patch: CatalogEntryI18nFields];
188
+ }>();
189
+
190
+ const TIER_OPTIONS = ['CORE', 'ADVANCED', 'PRO', 'ENTERPRISE'];
191
+
192
+ const sub = ref<'stamm' | 'i18n'>('stamm');
193
+
194
+ // Local input buffer: once a field has been edited, the draft wins over the
195
+ // prop value. Persistence is debounced + the list is replaced from the
196
+ // PATCH response — without a buffer a late echo would reset typed characters
197
+ // (see CatalogEntryTransPanel).
198
+ const drafts = reactive<Record<string, string>>({});
199
+ type Field = 'icon' | 'tier' | 'label' | 'description';
200
+
201
+ function fieldValue(field: Field): string {
202
+ if (field in drafts) return drafts[field];
203
+ const v = props.feature[field];
204
+ return v == null ? '' : String(v);
205
+ }
206
+ const iconValue = computed(() => fieldValue('icon'));
207
+ const tierValue = computed(() => fieldValue('tier'));
208
+ const labelValue = computed(() => fieldValue('label'));
209
+ const descriptionValue = computed(() => fieldValue('description'));
210
+
211
+ function onField(field: Field, value: string): void {
212
+ drafts[field] = value;
213
+ emit('feature-base', props.feature.featureKey, { [field]: value });
214
+ }
215
+
216
+ /** Mirror base edits from the translation panel so the header + master data follow along. */
217
+ function onTransBase(patch: { label?: string; description?: string }): void {
218
+ for (const [field, value] of Object.entries(patch)) {
219
+ drafts[field as Field] = value ?? '';
220
+ }
221
+ emit('feature-base', props.feature.featureKey, patch);
222
+ }
223
+
224
+ // "New" relative to the last approval: capabilities that entered the catalog
225
+ // since `approvedAt`. Without an approval there is no baseline.
226
+ const newCapsCount = computed(() => {
227
+ const approvedAt = props.feature.approvedAt;
228
+ if (!approvedAt) return 0;
229
+ return props.capabilities.filter((c) => c.createdAt > approvedAt).length;
230
+ });
231
+ const deprecatedCapsCount = computed(
232
+ () => props.capabilities.filter((c) => c.codeStatus === 'deprecated').length,
233
+ );
234
+
235
+ const targetLocales = computed(() =>
236
+ props.activeLocales.filter((l) => l !== DISCOVERY_DEFAULT_LOCALE),
237
+ );
238
+
239
+ const transEntry = computed<TransEntry>(() => ({
240
+ key: props.feature.featureKey,
241
+ label: labelValue.value,
242
+ description: descriptionValue.value,
243
+ i18n: props.feature.i18n ?? {},
244
+ }));
245
+
246
+ function coverage(locale: string): number {
247
+ return entryCoverage(transEntry.value, locale, ['label', 'description']);
248
+ }
249
+ </script>
250
+
251
+ <style scoped>
252
+ .sa-fc {
253
+ border: 1px solid #e2e8f0;
254
+ border-radius: 10px;
255
+ background: #fff;
256
+ overflow: hidden;
257
+ }
258
+ .sa-fc.expanded {
259
+ border-color: #c7d2fe;
260
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
261
+ }
262
+ .sa-fc__head {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 12px;
266
+ padding: 10px 12px;
267
+ cursor: pointer;
268
+ }
269
+ .sa-fc__icon {
270
+ color: #475569;
271
+ flex-shrink: 0;
272
+ }
273
+ .sa-fc__main {
274
+ flex: 1;
275
+ min-width: 0;
276
+ }
277
+ .sa-fc__titlerow {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 8px;
281
+ flex-wrap: wrap;
282
+ }
283
+ .sa-fc__key {
284
+ font-size: 11px;
285
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
286
+ background: #f1f5f9;
287
+ color: #475569;
288
+ padding: 2px 6px;
289
+ border-radius: 5px;
290
+ white-space: nowrap;
291
+ }
292
+ .sa-fc__label {
293
+ font-size: 13px;
294
+ font-weight: 600;
295
+ color: #0f172a;
296
+ }
297
+ .sa-fc__flag {
298
+ font-size: 10px;
299
+ font-weight: 700;
300
+ padding: 1px 6px;
301
+ border-radius: 5px;
302
+ }
303
+ .sa-fc__flag--new {
304
+ background: #fef3c7;
305
+ color: #92400e;
306
+ }
307
+ .sa-fc__flag--dep {
308
+ background: #fee2e2;
309
+ color: #b91c1c;
310
+ }
311
+ .sa-fc__flag--succ {
312
+ background: #fef3c7;
313
+ color: #92400e;
314
+ }
315
+ .sa-fc__flag--repl {
316
+ background: #dbeafe;
317
+ color: #1e40af;
318
+ }
319
+ .sa-fc__sub {
320
+ font-size: 11px;
321
+ color: #64748b;
322
+ margin-top: 1px;
323
+ }
324
+ .sa-fc__dot {
325
+ margin: 0 4px;
326
+ color: #cbd5e1;
327
+ }
328
+ .sa-fc__tier {
329
+ font-weight: 700;
330
+ text-transform: uppercase;
331
+ letter-spacing: 0.3px;
332
+ color: #4f46e5;
333
+ }
334
+ .sa-fc__coverage {
335
+ display: flex;
336
+ gap: 6px;
337
+ flex-shrink: 0;
338
+ }
339
+ .sa-fc__chev {
340
+ color: #94a3b8;
341
+ transition: transform 0.15s;
342
+ }
343
+ .sa-fc__chev.open {
344
+ transform: rotate(90deg);
345
+ }
346
+ .sa-fc__body {
347
+ border-top: 1px solid #f1f5f9;
348
+ padding: 12px;
349
+ background: #f8fafc;
350
+ }
351
+ .sa-fc__banner {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ font-size: 12px;
356
+ border-radius: 8px;
357
+ padding: 8px 12px;
358
+ margin-bottom: 10px;
359
+ }
360
+ .sa-fc__banner.warn {
361
+ background: #fffbeb;
362
+ border: 1px solid #fde68a;
363
+ color: #92400e;
364
+ }
365
+ .sa-fc__banner.mute {
366
+ background: #f1f5f9;
367
+ border: 1px solid #e2e8f0;
368
+ color: #475569;
369
+ }
370
+ .sa-fc__subtabs {
371
+ margin-bottom: 12px;
372
+ border-bottom: 1px solid #e2e8f0;
373
+ }
374
+ .sa-fc__split {
375
+ display: grid;
376
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
377
+ gap: 14px;
378
+ }
379
+ .sa-fc__split-head {
380
+ font-size: 11px;
381
+ font-weight: 700;
382
+ text-transform: uppercase;
383
+ letter-spacing: 0.5px;
384
+ color: #94a3b8;
385
+ margin-bottom: 8px;
386
+ }
387
+ .sa-fc__split-count {
388
+ font-weight: 600;
389
+ text-transform: none;
390
+ letter-spacing: 0;
391
+ color: #cbd5e1;
392
+ margin-left: 6px;
393
+ }
394
+ .sa-fc__fields {
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 10px;
398
+ }
399
+ .sa-fc-field {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 3px;
403
+ }
404
+ .sa-fc-field__cap {
405
+ font-size: 11px;
406
+ font-weight: 600;
407
+ text-transform: uppercase;
408
+ color: #94a3b8;
409
+ }
410
+ </style>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <header class="sa-discovery__head">
3
+ <div>
4
+ <h1 class="sa-discovery__title">Discovery</h1>
5
+ <p class="sa-discovery__sub">
6
+ Capabilities, die der Code <b>aktuell</b> implementiert. Discovery ist Ist-Zustand —
7
+ die Freigabe für Produkt &amp; Marketing erfolgt im Review.
8
+ </p>
9
+ </div>
10
+ <div class="sa-discovery__head-actions">
11
+ <q-btn
12
+ unelevated
13
+ color="primary"
14
+ icon="bolt"
15
+ label="Discovery neu ausführen"
16
+ :loading="loading"
17
+ @click="emit('runDiscovery')"
18
+ />
19
+ </div>
20
+ </header>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ defineProps<{
25
+ loading: boolean;
26
+ }>();
27
+
28
+ const emit = defineEmits<{
29
+ (e: 'runDiscovery'): void;
30
+ }>();
31
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="sa-discovery__kpis">
3
+ <div class="sa-discovery__kpi">
4
+ <div class="sa-discovery__kpi-label">Features gesamt</div>
5
+ <div class="sa-discovery__kpi-value">{{ featuresCount }}</div>
6
+ <div class="sa-discovery__kpi-sub">
7
+ aggregiert aus {{ capabilitiesCount }} Capabilities
8
+ </div>
9
+ </div>
10
+ <div class="sa-discovery__kpi good">
11
+ <div class="sa-discovery__kpi-label">Freigegeben</div>
12
+ <div class="sa-discovery__kpi-value">{{ approvedCount }}</div>
13
+ <div class="sa-discovery__kpi-sub">planungsbereit</div>
14
+ </div>
15
+ <div class="sa-discovery__kpi" :class="{ warn: pendingCount > 0 }">
16
+ <div class="sa-discovery__kpi-label">Ausstehend</div>
17
+ <div class="sa-discovery__kpi-value">{{ pendingCount }}</div>
18
+ <div class="sa-discovery__kpi-sub">noch nicht freigegeben</div>
19
+ </div>
20
+ <div class="sa-discovery__kpi">
21
+ <div class="sa-discovery__kpi-label">Veraltet / Obsolet</div>
22
+ <div class="sa-discovery__kpi-value">{{ outdatedCount + obsoleteCount }}</div>
23
+ <div class="sa-discovery__kpi-sub">
24
+ {{ outdatedCount }} outdated · {{ obsoleteCount }} obsolete
25
+ </div>
26
+ </div>
27
+ <div class="sa-discovery__kpi" :class="{ bad: orphanCount > 0 }">
28
+ <div class="sa-discovery__kpi-label">Orphans</div>
29
+ <div class="sa-discovery__kpi-value">{{ orphanCount }}</div>
30
+ <div class="sa-discovery__kpi-sub">Capabilities ohne Feature</div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ // Feature-centric KPI bar (#20 Slice 1, sim layout): the approval
37
+ // counts refer to features, no longer to capability reviews.
38
+ defineProps<{
39
+ featuresCount: number;
40
+ capabilitiesCount: number;
41
+ approvedCount: number;
42
+ pendingCount: number;
43
+ outdatedCount: number;
44
+ obsoleteCount: number;
45
+ orphanCount: number;
46
+ }>();
47
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div class="sa-discovery__banner">
3
+ <div class="sa-discovery__banner-app">
4
+ <q-icon name="inventory_2" size="20px" />
5
+ <div>
6
+ <div class="sa-discovery__banner-name">{{ appLabel }}</div>
7
+ <div class="sa-discovery__banner-meta">
8
+ <code>{{ appKey }}</code> · v{{ appVersion }}
9
+ </div>
10
+ </div>
11
+ </div>
12
+ <div class="sa-discovery__banner-time">
13
+ <div class="sa-discovery__banner-time-lbl">Letzter Scan</div>
14
+ <div class="sa-discovery__banner-time-val">{{ scanLabel }}</div>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ defineProps<{
21
+ appLabel: string;
22
+ appKey: string;
23
+ appVersion: string;
24
+ scanLabel: string;
25
+ }>();
26
+ </script>