@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,193 @@
1
+ <template>
2
+ <div class="sa-bd-grid">
3
+ <div class="sa-bd-col">
4
+ <div class="sa-bd-section-label">Stammdaten</div>
5
+ <div class="sa-bundles__form">
6
+ <q-input
7
+ :model-value="editForm.label"
8
+ outlined
9
+ dense
10
+ label="Label"
11
+ @update:model-value="(value) => patchEditForm({ label: String(value ?? '') })"
12
+ />
13
+ <q-input
14
+ :model-value="editForm.description"
15
+ outlined
16
+ dense
17
+ type="textarea"
18
+ autogrow
19
+ label="Beschreibung"
20
+ @update:model-value="
21
+ (value) => patchEditForm({ description: String(value ?? '') })
22
+ "
23
+ />
24
+ <q-input
25
+ :model-value="editForm.icon"
26
+ outlined
27
+ dense
28
+ label="Icon"
29
+ @update:model-value="(value) => patchEditForm({ icon: String(value ?? '') })"
30
+ />
31
+ <q-input
32
+ :model-value="editForm.sortOrder"
33
+ outlined
34
+ dense
35
+ type="number"
36
+ label="Sortier-Reihenfolge"
37
+ @update:model-value="
38
+ (value) => patchEditForm({ sortOrder: Number(value ?? 0) })
39
+ "
40
+ />
41
+ </div>
42
+
43
+ <div class="sa-bd-section-label sa-bd-section-label--mt">
44
+ <span>Übersetzungen</span>
45
+ <span class="sa-bd-section-count">{{ translatableLocales.length }} Sprache(n)</span>
46
+ </div>
47
+ <div v-if="translatableLocales.length === 0" class="sa-bd-i18n-hint">
48
+ Keine weiteren Sprachen aktiv — werden im Marketing-Catalog aktiviert.
49
+ </div>
50
+ <div v-for="lng in translatableLocales" :key="lng" class="sa-bd-i18n-block">
51
+ <div class="sa-bd-i18n-head">
52
+ <span class="sa-bd-i18n-code">{{ lng.toUpperCase() }}</span>
53
+ <span
54
+ v-if="!i18nDraft[lng]?.label || !i18nDraft[lng]?.description"
55
+ class="sa-bd-i18n-fallback"
56
+ >
57
+ Fallback aus DE
58
+ </span>
59
+ </div>
60
+ <q-input
61
+ outlined
62
+ dense
63
+ :model-value="i18nDraft[lng]?.label || ''"
64
+ :placeholder="`Label (Fallback: „${editForm.label}“)`"
65
+ @update:model-value="
66
+ (value) => emit('setI18n', lng, 'label', String(value ?? ''))
67
+ "
68
+ />
69
+ <q-input
70
+ outlined
71
+ dense
72
+ type="textarea"
73
+ autogrow
74
+ :model-value="i18nDraft[lng]?.description || ''"
75
+ :placeholder="`Beschreibung (Fallback aus DE)`"
76
+ @update:model-value="
77
+ (value) => emit('setI18n', lng, 'description', String(value ?? ''))
78
+ "
79
+ />
80
+ </div>
81
+
82
+ <q-btn
83
+ class="sa-bd-save"
84
+ unelevated
85
+ color="primary"
86
+ label="Stammdaten & Übersetzungen speichern"
87
+ :loading="editSubmitting"
88
+ @click="emit('submitEdit')"
89
+ />
90
+ </div>
91
+
92
+ <div class="sa-bd-col sa-bd-col--versions">
93
+ <BundleVersionStrip
94
+ :versions="detailVersions"
95
+ :model-value="selectedVersionId"
96
+ @update:model-value="(id) => emit('selectVersion', bundle.id, id)"
97
+ @add-version="emit('addVersion', bundle.id)"
98
+ />
99
+
100
+ <BundleVersionInlineEditor
101
+ v-if="selectedVersion"
102
+ :key="selectedVersion.id"
103
+ :version="selectedVersion"
104
+ :available-features="availableFeatures"
105
+ :available-quotas="availableQuotas"
106
+ :plans="plans"
107
+ :live-plan-versions="livePlanVersions"
108
+ :feature-registry="featureRegistry"
109
+ :quota-registry="quotaRegistry"
110
+ :saving="inlineEditorSaving"
111
+ :save-error="inlineEditorError"
112
+ @save="(data) => emit('inlineSave', bundle.id, selectedVersion!.id, data)"
113
+ @discard="emit('discardVersion', bundle.id, selectedVersion!.id)"
114
+ />
115
+ <div v-else class="sa-bd-empty-row">
116
+ Noch keine Version. Lege eine neue Version an, um Features, Quotas &amp; Pricing zu
117
+ kuratieren.
118
+ </div>
119
+
120
+ <div class="sa-bd-version-actions">
121
+ <q-btn
122
+ v-if="selectedVersion && selectedVersion.publishedAt === null"
123
+ unelevated
124
+ color="positive"
125
+ icon="rocket_launch"
126
+ label="Diese Version publishen"
127
+ @click="emit('publishVersion', selectedVersion)"
128
+ />
129
+ <q-btn
130
+ class="sa-bd-delete"
131
+ flat
132
+ dense
133
+ color="negative"
134
+ icon="delete"
135
+ label="Bundle soft-deleten"
136
+ @click="emit('deleteBundle', bundle)"
137
+ />
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </template>
142
+
143
+ <script setup lang="ts">
144
+ import type {
145
+ BundleRow,
146
+ BundleVersionRow,
147
+ CatalogEntryI18n,
148
+ DiscoverySnapshot,
149
+ PlanRow,
150
+ PlanVersionRow,
151
+ UpdateBundleVersionDraftData,
152
+ } from '@saasicat/types';
153
+ import BundleVersionInlineEditor from '../../components/bundle-editor/BundleVersionInlineEditor.vue';
154
+ import BundleVersionStrip from '../../components/bundle-editor/BundleVersionStrip.vue';
155
+ import type { FeatureMeta } from '../../components/bundle-editor/BundleFeaturesEditor.vue';
156
+ import type { QuotaMeta } from '../../components/bundle-editor/catalog-i18n.js';
157
+ import type { BundleEditForm } from './types.js';
158
+
159
+ const props = defineProps<{
160
+ bundle: BundleRow;
161
+ editForm: BundleEditForm;
162
+ i18nDraft: CatalogEntryI18n;
163
+ translatableLocales: string[];
164
+ editSubmitting: boolean;
165
+ detailVersions: BundleVersionRow[];
166
+ selectedVersionId: string | null;
167
+ selectedVersion: BundleVersionRow | null;
168
+ availableFeatures: DiscoverySnapshot['features'];
169
+ availableQuotas: DiscoverySnapshot['quotas'];
170
+ plans: PlanRow[];
171
+ livePlanVersions: Record<string, PlanVersionRow | null>;
172
+ featureRegistry: Record<string, FeatureMeta>;
173
+ quotaRegistry: Record<string, QuotaMeta>;
174
+ inlineEditorSaving: boolean;
175
+ inlineEditorError: string | null;
176
+ }>();
177
+
178
+ const emit = defineEmits<{
179
+ 'update:editForm': [value: BundleEditForm];
180
+ setI18n: [locale: string, field: 'label' | 'description', value: string];
181
+ submitEdit: [];
182
+ selectVersion: [bundleId: string, versionId: string];
183
+ addVersion: [bundleId: string];
184
+ inlineSave: [bundleId: string, versionId: string, data: UpdateBundleVersionDraftData];
185
+ discardVersion: [bundleId: string, versionId: string];
186
+ publishVersion: [version: BundleVersionRow];
187
+ deleteBundle: [bundle: BundleRow];
188
+ }>();
189
+
190
+ function patchEditForm(patch: Partial<BundleEditForm>): void {
191
+ emit('update:editForm', { ...props.editForm, ...patch });
192
+ }
193
+ </script>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="sa-bundles__filter-row">
3
+ <q-input
4
+ :model-value="query"
5
+ dense
6
+ outlined
7
+ clearable
8
+ placeholder="Bundle-Key oder Label suchen …"
9
+ class="sa-bundles__search"
10
+ @update:model-value="emitQuery"
11
+ >
12
+ <template #prepend><q-icon name="search" /></template>
13
+ </q-input>
14
+ <q-select
15
+ :model-value="statusFilter"
16
+ dense
17
+ outlined
18
+ :options="statusFilterOptions"
19
+ emit-value
20
+ map-options
21
+ class="sa-bundles__status-filter"
22
+ @update:model-value="emitStatusFilter"
23
+ />
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import type { BundlesStatusFilter, BundlesStatusFilterOption } from './types.js';
29
+
30
+ defineProps<{
31
+ query: string;
32
+ statusFilter: BundlesStatusFilter;
33
+ statusFilterOptions: BundlesStatusFilterOption[];
34
+ }>();
35
+
36
+ const emit = defineEmits<{
37
+ 'update:query': [value: string];
38
+ 'update:statusFilter': [value: BundlesStatusFilter];
39
+ }>();
40
+
41
+ function emitQuery(value: string | number | null): void {
42
+ emit('update:query', String(value ?? ''));
43
+ }
44
+
45
+ function emitStatusFilter(value: unknown): void {
46
+ emit('update:statusFilter', (value ?? 'all') as BundlesStatusFilter);
47
+ }
48
+ </script>
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <header class="sa-bundles__head">
3
+ <div>
4
+ <h1 class="sa-bundles__title">Bundles</h1>
5
+ <p class="sa-bundles__sub">
6
+ Produktgruppen aus Features &amp; Quotas — verwendet in Plänen. Bundles werden im
7
+ SuperAdmin kuratiert.
8
+ </p>
9
+ </div>
10
+ <div class="sa-bundles__head-actions">
11
+ <q-select
12
+ v-if="locales.length > 1"
13
+ :model-value="displayLocale"
14
+ :options="localeOptions"
15
+ outlined
16
+ dense
17
+ emit-value
18
+ map-options
19
+ class="sa-bundles__locale"
20
+ label="Anzeige-Sprache"
21
+ @update:model-value="(value) => emit('update:displayLocale', String(value))"
22
+ >
23
+ <template #prepend><q-icon name="translate" size="18px" /></template>
24
+ </q-select>
25
+ <q-btn
26
+ unelevated
27
+ color="primary"
28
+ icon="add"
29
+ label="Neues Bundle"
30
+ @click="emit('create')"
31
+ />
32
+ <q-btn flat icon="refresh" :loading="loading" @click="emit('refresh')">
33
+ <q-tooltip>Liste neu laden</q-tooltip>
34
+ </q-btn>
35
+ </div>
36
+ </header>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed } from 'vue';
41
+
42
+ // The language selector controls which locale feature/quota labels are resolved
43
+ // on the create form and in the detail view (fallback locale → DE → key). The default is the
44
+ // default locale (DE); further options come from the active project locales.
45
+
46
+ const props = defineProps<{
47
+ loading: boolean;
48
+ displayLocale: string;
49
+ locales: string[];
50
+ }>();
51
+
52
+ const emit = defineEmits<{
53
+ create: [];
54
+ refresh: [];
55
+ 'update:displayLocale': [locale: string];
56
+ }>();
57
+
58
+ const localeOptions = computed(() =>
59
+ props.locales.map((locale) => ({ label: locale.toUpperCase(), value: locale })),
60
+ );
61
+ </script>
62
+
63
+ <style scoped>
64
+ .sa-bundles__locale {
65
+ min-width: 150px;
66
+ }
67
+ </style>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div class="sa-bundles__kpis">
3
+ <div class="sa-bundles__kpi">
4
+ <div class="sa-bundles__kpi-label">Bundles gesamt</div>
5
+ <div class="sa-bundles__kpi-value">{{ bundlesTotal }}</div>
6
+ <div class="sa-bundles__kpi-sub">
7
+ {{ liveCount }} live · {{ scheduledBundlesCount }} mit geplanter Version
8
+ </div>
9
+ </div>
10
+ <div class="sa-bundles__kpi">
11
+ <div class="sa-bundles__kpi-label">Geplante Versionen</div>
12
+ <div class="sa-bundles__kpi-value">{{ totalScheduledVersions }}</div>
13
+ <div class="sa-bundles__kpi-sub">zukünftig aktiv · noch nicht verkaufbar</div>
14
+ </div>
15
+ <div class="sa-bundles__kpi">
16
+ <div class="sa-bundles__kpi-label">Offene Drafts</div>
17
+ <div class="sa-bundles__kpi-value">{{ totalDraftVersions }}</div>
18
+ <div class="sa-bundles__kpi-sub">{{ draftBundlesCount }} Bundle(s) ohne Publish</div>
19
+ </div>
20
+ <div class="sa-bundles__kpi">
21
+ <div class="sa-bundles__kpi-label">Mit Übersetzung</div>
22
+ <div class="sa-bundles__kpi-value">{{ translatedCount }}</div>
23
+ <div class="sa-bundles__kpi-sub">{{ localesCount }} aktive Sprache(n) im Projekt</div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ defineProps<{
30
+ bundlesTotal: number;
31
+ liveCount: number;
32
+ scheduledBundlesCount: number;
33
+ totalScheduledVersions: number;
34
+ totalDraftVersions: number;
35
+ draftBundlesCount: number;
36
+ translatedCount: number;
37
+ localesCount: number;
38
+ }>();
39
+ </script>
@@ -0,0 +1,15 @@
1
+ import type { BundleAggregateStatus } from '../../components/bundle-editor/bundle-version-status.js';
2
+
3
+ export type BundlesStatusFilter = 'all' | BundleAggregateStatus;
4
+
5
+ export interface BundlesStatusFilterOption {
6
+ label: string;
7
+ value: BundlesStatusFilter;
8
+ }
9
+
10
+ export interface BundleEditForm {
11
+ label: string;
12
+ description: string;
13
+ icon: string;
14
+ sortOrder: number;
15
+ }
@@ -0,0 +1,246 @@
1
+ <template>
2
+ <div class="sa-trans">
3
+ <!-- Default locale: editable source -->
4
+ <div class="sa-trans-lang sa-trans-lang--source">
5
+ <div class="sa-trans-lang__head">
6
+ <span class="sa-trans-lang__code">{{ defaultLocale.toUpperCase() }}</span>
7
+ <span class="sa-trans-lang__name">{{ localeFull(defaultLocale) }}</span>
8
+ <span class="sa-trans-lang__badge">Source</span>
9
+ </div>
10
+ <div class="sa-trans-fields">
11
+ <div v-for="f in fields" :key="f" class="sa-trans-field">
12
+ <label class="sa-trans-field__cap">{{ i18nFieldLabel(f) }}</label>
13
+ <q-input
14
+ v-if="f !== 'unit'"
15
+ dense
16
+ outlined
17
+ :type="f === 'description' ? 'textarea' : 'text'"
18
+ :autogrow="f === 'description'"
19
+ :model-value="baseValue(f)"
20
+ @update:model-value="(v) => onBase(f, String(v ?? ''))"
21
+ />
22
+ <div v-else class="sa-trans-locked">
23
+ {{ entry.unit || '—' }}
24
+ <span class="sa-trans-locked__hint">aus Code</span>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Target languages -->
31
+ <div v-for="lng in targetLocales" :key="lng" class="sa-trans-lang">
32
+ <div class="sa-trans-lang__head">
33
+ <span class="sa-trans-lang__code">{{ localeShort(lng) }}</span>
34
+ <span class="sa-trans-lang__name">{{ localeFull(lng) }}</span>
35
+ <q-btn
36
+ flat
37
+ dense
38
+ no-caps
39
+ size="sm"
40
+ icon="content_copy"
41
+ label="Aus DE"
42
+ class="sa-trans-lang__copy"
43
+ @click="copyFromDefault(lng)"
44
+ >
45
+ <q-tooltip>Leere Felder aus der Default-Locale übernehmen</q-tooltip>
46
+ </q-btn>
47
+ </div>
48
+ <div class="sa-trans-fields">
49
+ <div v-for="f in fields" :key="f" class="sa-trans-field">
50
+ <label class="sa-trans-field__cap">{{ i18nFieldLabel(f) }}</label>
51
+ <q-input
52
+ dense
53
+ outlined
54
+ :type="f === 'description' ? 'textarea' : 'text'"
55
+ :autogrow="f === 'description'"
56
+ :model-value="localeValue(lng, f)"
57
+ :placeholder="baseValue(f) ? `Fallback: „${baseValue(f)}“` : '—'"
58
+ @update:model-value="(v) => onLocale(lng, f, String(v ?? ''))"
59
+ />
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div v-if="targetLocales.length === 0" class="sa-trans__empty">
65
+ Keine weiteren Sprachen aktiv — Sprachen werden im Marketing-Catalog aktiviert.
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, reactive } from 'vue';
72
+ import type { CatalogEntryI18nFields } from '@saasicat/types';
73
+ import {
74
+ DISCOVERY_DEFAULT_LOCALE,
75
+ i18nFieldLabel,
76
+ localeFull,
77
+ localeShort,
78
+ type I18nField,
79
+ type TransEntry,
80
+ } from './discovery-ui.js';
81
+
82
+ // Translation block of a single catalog entry (feature / quota) —
83
+ // embedded in the collapsible card (#20, Sim `TransPanel`). Default-
84
+ // locale fields are editable (source of truth), target languages carry
85
+ // overrides; empty fields fall back to the default locale in the consumer.
86
+
87
+ const props = withDefaults(
88
+ defineProps<{
89
+ entry: TransEntry;
90
+ /** Fields to translate; order = display order. */
91
+ fields: I18nField[];
92
+ /** All active locales incl. default. */
93
+ activeLocales: string[];
94
+ defaultLocale?: string;
95
+ }>(),
96
+ { defaultLocale: DISCOVERY_DEFAULT_LOCALE },
97
+ );
98
+
99
+ const emit = defineEmits<{
100
+ /** Default-locale base field changed (`label`/`description`). */
101
+ 'update:base': [patch: { label?: string; description?: string }];
102
+ /** Target-locale override changed. */
103
+ 'update:locale': [locale: string, patch: CatalogEntryI18nFields];
104
+ }>();
105
+
106
+ const targetLocales = computed(() => props.activeLocales.filter((l) => l !== props.defaultLocale));
107
+
108
+ // Local input buffer: once a field has been edited, the draft wins over
109
+ // the prop value. Persistence is debounced — without this buffer, a late-
110
+ // returning PATCH response would reset the field to a stale state and
111
+ // swallow the most recently typed characters.
112
+ const drafts = reactive<Record<string, string>>({});
113
+
114
+ function baseValue(f: I18nField): string {
115
+ if (f === 'unit') return props.entry.unit ?? '';
116
+ const dk = `b|${f}`;
117
+ if (dk in drafts) return drafts[dk];
118
+ return (f === 'label' ? props.entry.label : props.entry.description) ?? '';
119
+ }
120
+
121
+ function localeValue(locale: string, f: I18nField): string {
122
+ const dk = `l|${locale}|${f}`;
123
+ if (dk in drafts) return drafts[dk];
124
+ return props.entry.i18n?.[locale]?.[f] ?? '';
125
+ }
126
+
127
+ function onBase(f: I18nField, value: string): void {
128
+ if (f === 'unit') return;
129
+ drafts[`b|${f}`] = value;
130
+ emit('update:base', { [f]: value });
131
+ }
132
+
133
+ function onLocale(locale: string, f: I18nField, value: string): void {
134
+ drafts[`l|${locale}|${f}`] = value;
135
+ emit('update:locale', locale, { [f]: value });
136
+ }
137
+
138
+ /** Fills empty target fields from the default locale. */
139
+ function copyFromDefault(locale: string): void {
140
+ const patch: CatalogEntryI18nFields = {};
141
+ for (const f of props.fields) {
142
+ if (!localeValue(locale, f).trim()) {
143
+ const base = baseValue(f).trim();
144
+ if (base) patch[f] = base;
145
+ }
146
+ }
147
+ if (Object.keys(patch).length === 0) return;
148
+ for (const [f, value] of Object.entries(patch)) {
149
+ drafts[`l|${locale}|${f}`] = value ?? '';
150
+ }
151
+ emit('update:locale', locale, patch);
152
+ }
153
+ </script>
154
+
155
+ <style scoped>
156
+ .sa-trans {
157
+ display: grid;
158
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
159
+ gap: 12px;
160
+ }
161
+ .sa-trans-lang {
162
+ border: 1px solid #e2e8f0;
163
+ border-radius: 10px;
164
+ background: #fff;
165
+ padding: 10px 12px;
166
+ }
167
+ .sa-trans-lang--source {
168
+ background: #f8fafc;
169
+ border-color: #cbd5e1;
170
+ }
171
+ .sa-trans-lang__head {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 8px;
175
+ margin-bottom: 8px;
176
+ }
177
+ .sa-trans-lang__code {
178
+ font-size: 10px;
179
+ font-weight: 700;
180
+ background: #0f172a;
181
+ color: #fff;
182
+ padding: 2px 6px;
183
+ border-radius: 5px;
184
+ }
185
+ .sa-trans-lang__name {
186
+ font-size: 12px;
187
+ font-weight: 600;
188
+ color: #334155;
189
+ flex: 1;
190
+ }
191
+ .sa-trans-lang__badge {
192
+ font-size: 9px;
193
+ font-weight: 700;
194
+ text-transform: uppercase;
195
+ letter-spacing: 0.5px;
196
+ color: #475569;
197
+ background: #e2e8f0;
198
+ padding: 2px 6px;
199
+ border-radius: 5px;
200
+ }
201
+ .sa-trans-lang__copy {
202
+ color: #475569;
203
+ }
204
+ .sa-trans-fields {
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 8px;
208
+ }
209
+ .sa-trans-field {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 3px;
213
+ }
214
+ .sa-trans-field__cap {
215
+ font-size: 10px;
216
+ font-weight: 600;
217
+ text-transform: uppercase;
218
+ letter-spacing: 0.3px;
219
+ color: #94a3b8;
220
+ }
221
+ .sa-trans-locked {
222
+ font-size: 13px;
223
+ color: #334155;
224
+ background: #f1f5f9;
225
+ border: 1px dashed #cbd5e1;
226
+ border-radius: 6px;
227
+ padding: 6px 10px;
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 8px;
231
+ }
232
+ .sa-trans-locked__hint {
233
+ font-size: 10px;
234
+ color: #94a3b8;
235
+ text-transform: uppercase;
236
+ }
237
+ .sa-trans__empty {
238
+ grid-column: 1 / -1;
239
+ padding: 16px;
240
+ text-align: center;
241
+ font-size: 12px;
242
+ color: #94a3b8;
243
+ border: 1px dashed #cbd5e1;
244
+ border-radius: 10px;
245
+ }
246
+ </style>