@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,72 @@
1
+ import type { PlanVersionRow } from '@saasicat/types';
2
+
3
+ export interface AuditRow {
4
+ id: string;
5
+ createdAt: string;
6
+ action: string;
7
+ entity: string;
8
+ entityId: string;
9
+ changes: Record<string, unknown> | null;
10
+ user?: { email: string; firstName?: string; lastName?: string } | null;
11
+ userEmail?: string | null;
12
+ }
13
+
14
+ export interface ImpactTenant {
15
+ name: string;
16
+ plan: string;
17
+ state: 'auto' | 'review' | 'conflict';
18
+ }
19
+
20
+ export interface TenantImpact {
21
+ auto: number;
22
+ review: number;
23
+ conflict: number;
24
+ examples?: ImpactTenant[];
25
+ }
26
+
27
+ export interface DiscoveryQuota {
28
+ quotaKey: string;
29
+ label?: string | null;
30
+ unit?: string | null;
31
+ }
32
+
33
+ export interface FeatureMeta {
34
+ label?: string;
35
+ }
36
+
37
+ export interface TimelineSegment {
38
+ key: string;
39
+ version: number;
40
+ status: 'draft' | 'supersed' | 'live';
41
+ flex: number;
42
+ }
43
+
44
+ export interface PlanVersionPair {
45
+ from: PlanVersionRow;
46
+ to: PlanVersionRow;
47
+ }
48
+
49
+ export interface DiffSummary {
50
+ addedFeatures: string[];
51
+ removedFeatures: string[];
52
+ addedQuotas: Array<{ key: string; value: number }>;
53
+ removedQuotas: Array<{ key: string; value: number }>;
54
+ changedQuotas: Array<{ key: string; from: number; to: number }>;
55
+ priceChanged: boolean;
56
+ priceFrom: string;
57
+ priceTo: string;
58
+ }
59
+
60
+ export interface DiffRow {
61
+ id: string;
62
+ section: string;
63
+ label: string;
64
+ sub?: string;
65
+ from?: string;
66
+ to?: string;
67
+ bg: string;
68
+ border: string;
69
+ color: string;
70
+ sign: string;
71
+ tag: string;
72
+ }
@@ -0,0 +1,508 @@
1
+ <template>
2
+ <q-dialog
3
+ :model-value="modelValue"
4
+ @update:model-value="$emit('update:modelValue', $event)"
5
+ persistent
6
+ :no-shake="true"
7
+ >
8
+ <div class="pcd-modal" role="dialog" aria-labelledby="pcd-title">
9
+ <div class="pcd-head">
10
+ <div class="pcd-head-text">
11
+ <div id="pcd-title" class="pcd-title">Neuen Plan anlegen</div>
12
+ <div class="pcd-sub">
13
+ Plan-Stamm anlegen. Im nächsten Schritt weist du Features, Quotas und
14
+ Bundles zu.
15
+ </div>
16
+ </div>
17
+ <button class="pcd-close" type="button" aria-label="Schließen" @click="onCancel">
18
+ <svg
19
+ width="14"
20
+ height="14"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="2"
25
+ >
26
+ <path d="M18 6L6 18M6 6l12 12" />
27
+ </svg>
28
+ </button>
29
+ </div>
30
+
31
+ <div class="pcd-body">
32
+ <div class="pcd-row pcd-row--2col">
33
+ <div class="pcd-field">
34
+ <div class="pcd-field-label">
35
+ Plan-Key
36
+ <span class="pcd-kbd">UNIQUE</span>
37
+ </div>
38
+ <input
39
+ ref="keyInput"
40
+ class="pcd-input"
41
+ :class="{ 'pcd-input--error': keyError }"
42
+ placeholder="z. B. SCALE"
43
+ :value="form.planKey"
44
+ @input="onPlanKeyInput"
45
+ />
46
+ <div v-if="keyError" class="pcd-hint pcd-hint--error">{{ keyError }}</div>
47
+ <div v-else class="pcd-hint">
48
+ Großbuchstaben + Unterstriche · API-stabil, nicht änderbar nach Publish
49
+ </div>
50
+ </div>
51
+ <div class="pcd-field">
52
+ <div class="pcd-field-label">Anzeigename</div>
53
+ <input class="pcd-input" placeholder="z. B. Scale" v-model="form.label" />
54
+ <div class="pcd-hint">Wie der Plan im Catalog erscheint</div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="pcd-field">
59
+ <div class="pcd-field-label">Beschreibung</div>
60
+ <textarea
61
+ class="pcd-input pcd-input--textarea"
62
+ rows="2"
63
+ placeholder="Was bekommt der Kunde mit diesem Plan?"
64
+ v-model="form.description"
65
+ />
66
+ </div>
67
+
68
+ <div class="pcd-field">
69
+ <div class="pcd-field-label">Basis für die erste Version</div>
70
+ <div class="pcd-choice-grid">
71
+ <button
72
+ v-for="opt in choiceOptions"
73
+ :key="opt.key"
74
+ type="button"
75
+ :class="[
76
+ 'pcd-choice',
77
+ { 'pcd-choice--selected': form.basis === opt.key },
78
+ ]"
79
+ @click="form.basis = opt.key"
80
+ >
81
+ <div class="pcd-choice-title">{{ opt.title }}</div>
82
+ <div class="pcd-choice-sub">{{ opt.subtitle }}</div>
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="pcd-foot">
89
+ <button class="pcd-btn pcd-btn--ghost" type="button" @click="onCancel">
90
+ Abbrechen
91
+ </button>
92
+ <button
93
+ class="pcd-btn pcd-btn--primary"
94
+ type="button"
95
+ :disabled="!canSubmit || submitting"
96
+ @click="onSubmit"
97
+ >
98
+ <span>{{ submitting ? 'Wird angelegt…' : 'Weiter · Komponenten' }}</span>
99
+ <span class="pcd-ico" aria-hidden="true">
100
+ <svg
101
+ width="14"
102
+ height="14"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ >
108
+ <path d="M5 12h14M13 5l7 7-7 7" />
109
+ </svg>
110
+ </span>
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </q-dialog>
115
+ </template>
116
+
117
+ <script setup lang="ts">
118
+ import { computed, reactive, ref, watch } from 'vue';
119
+
120
+ // PlanCreateDialog — step 1 of the "new plan" flow from the plan
121
+ // simulation. Collects the plan master data (key, label, description) plus the
122
+ // basis for the first draft version (empty or a clone of an existing plan).
123
+ // On "Weiter" the dialog emits the complete submit payload; the
124
+ // hosting page (PlansPage) creates the plan and opens the V2 editor
125
+ // with the cloned initial form.
126
+
127
+ export interface TemplateOption {
128
+ /** "empty" or plan key. */
129
+ key: string;
130
+ label: string;
131
+ features: string[];
132
+ quotas: Record<string, number>;
133
+ bundles: string[];
134
+ }
135
+
136
+ export interface PlanCreateSubmit {
137
+ planKey: string;
138
+ label: string;
139
+ description: string;
140
+ basis: string;
141
+ initialFeatures: string[];
142
+ initialQuotas: Record<string, number>;
143
+ initialBundles: string[];
144
+ }
145
+
146
+ const props = withDefaults(
147
+ defineProps<{
148
+ modelValue: boolean;
149
+ /** Available clone templates (except "empty"). Default: empty (only "Leerer Plan"). */
150
+ availableTemplates?: TemplateOption[];
151
+ /** Plan keys that already exist — for uniqueness validation. */
152
+ existingPlanKeys?: string[];
153
+ /** Optionally preselected basis (e.g. when started from a "Klon" button). */
154
+ defaultBasis?: string;
155
+ submitting?: boolean;
156
+ }>(),
157
+ {
158
+ availableTemplates: () => [],
159
+ existingPlanKeys: () => [],
160
+ defaultBasis: 'empty',
161
+ submitting: false,
162
+ },
163
+ );
164
+
165
+ const emit = defineEmits<{
166
+ (e: 'update:modelValue', value: boolean): void;
167
+ (e: 'submit', payload: PlanCreateSubmit): void;
168
+ (e: 'cancel'): void;
169
+ }>();
170
+
171
+ const EMPTY_BASIS: TemplateOption = {
172
+ key: 'empty',
173
+ label: 'Leerer Plan',
174
+ features: [],
175
+ quotas: {},
176
+ bundles: [],
177
+ };
178
+
179
+ const form = reactive({
180
+ planKey: '',
181
+ label: '',
182
+ description: '',
183
+ basis: 'empty',
184
+ });
185
+
186
+ const keyInput = ref<HTMLInputElement | null>(null);
187
+
188
+ // Open → reset state + focus.
189
+ watch(
190
+ () => props.modelValue,
191
+ (open) => {
192
+ if (!open) return;
193
+ form.planKey = '';
194
+ form.label = '';
195
+ form.description = '';
196
+ form.basis = props.defaultBasis;
197
+ setTimeout(() => keyInput.value?.focus(), 50);
198
+ },
199
+ );
200
+
201
+ function onPlanKeyInput(e: Event): void {
202
+ const raw = (e.target as HTMLInputElement).value;
203
+ form.planKey = raw.toUpperCase().replace(/[^A-Z0-9_]/g, '');
204
+ }
205
+
206
+ const keyError = computed<string | null>(() => {
207
+ if (!form.planKey) return null;
208
+ if (!/^[A-Z][A-Z0-9_]*$/.test(form.planKey)) {
209
+ return 'Muss mit Buchstabe beginnen, nur A–Z, 0–9, _.';
210
+ }
211
+ if (props.existingPlanKeys.includes(form.planKey)) {
212
+ return `„${form.planKey}" existiert bereits.`;
213
+ }
214
+ return null;
215
+ });
216
+
217
+ const canSubmit = computed(
218
+ () => form.planKey.length > 0 && form.label.trim().length > 0 && keyError.value === null,
219
+ );
220
+
221
+ const choiceOptions = computed(() => {
222
+ const options = [
223
+ {
224
+ key: 'empty',
225
+ title: 'Leerer Plan',
226
+ subtitle: 'Im Editor alles selbst zusammenstellen',
227
+ },
228
+ ...props.availableTemplates.map((t) => ({
229
+ key: t.key,
230
+ title: `Klon von ${t.label}`,
231
+ subtitle: countsLabel(t),
232
+ })),
233
+ ];
234
+ return options;
235
+ });
236
+
237
+ function countsLabel(t: TemplateOption): string {
238
+ const parts = [
239
+ `${t.features.length} Features`,
240
+ `${Object.keys(t.quotas).length} Quotas`,
241
+ `${t.bundles.length} Bundles`,
242
+ ];
243
+ return parts.join(' · ');
244
+ }
245
+
246
+ function selectedTemplate(): TemplateOption {
247
+ if (form.basis === 'empty') return EMPTY_BASIS;
248
+ return props.availableTemplates.find((t) => t.key === form.basis) ?? EMPTY_BASIS;
249
+ }
250
+
251
+ function onSubmit(): void {
252
+ if (!canSubmit.value || props.submitting) return;
253
+ const tpl = selectedTemplate();
254
+ emit('submit', {
255
+ planKey: form.planKey,
256
+ label: form.label.trim(),
257
+ description: form.description.trim(),
258
+ basis: form.basis,
259
+ initialFeatures: [...tpl.features],
260
+ initialQuotas: { ...tpl.quotas },
261
+ initialBundles: [...tpl.bundles],
262
+ });
263
+ }
264
+
265
+ function onCancel(): void {
266
+ emit('cancel');
267
+ emit('update:modelValue', false);
268
+ }
269
+ </script>
270
+
271
+ <style scoped>
272
+ .pcd-modal {
273
+ --pcd-bg: #ffffff;
274
+ --pcd-border: #e5e7eb;
275
+ --pcd-border-strong: #d1d5db;
276
+ --pcd-text: #0f172a;
277
+ --pcd-text-2: #475569;
278
+ --pcd-text-3: #94a3b8;
279
+ --pcd-primary: #2563eb;
280
+ --pcd-primary-700: #1d4ed8;
281
+ --pcd-danger: #ef4444;
282
+ --pcd-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
283
+ --pcd-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
284
+
285
+ background: var(--pcd-bg);
286
+ width: 640px;
287
+ max-width: 92vw;
288
+ border-radius: 14px;
289
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.3);
290
+ overflow: hidden;
291
+ font-family: var(--pcd-font-sans);
292
+ color: var(--pcd-text);
293
+ }
294
+ .pcd-modal * {
295
+ box-sizing: border-box;
296
+ }
297
+
298
+ .pcd-head {
299
+ padding: 18px 22px;
300
+ border-bottom: 1px solid var(--pcd-border);
301
+ display: flex;
302
+ align-items: flex-start;
303
+ gap: 12px;
304
+ }
305
+ .pcd-head-text {
306
+ flex: 1;
307
+ min-width: 0;
308
+ }
309
+ .pcd-title {
310
+ font-size: 16px;
311
+ font-weight: 700;
312
+ letter-spacing: -0.01em;
313
+ }
314
+ .pcd-sub {
315
+ font-size: 12px;
316
+ color: #64748b;
317
+ margin-top: 3px;
318
+ }
319
+ .pcd-close {
320
+ width: 28px;
321
+ height: 28px;
322
+ border-radius: 6px;
323
+ background: transparent;
324
+ border: 0;
325
+ color: var(--pcd-text-3);
326
+ cursor: pointer;
327
+ display: grid;
328
+ place-items: center;
329
+ }
330
+ .pcd-close:hover {
331
+ background: rgba(15, 23, 42, 0.05);
332
+ color: var(--pcd-text);
333
+ }
334
+
335
+ .pcd-body {
336
+ padding: 20px 22px;
337
+ max-height: calc(90vh - 200px);
338
+ overflow-y: auto;
339
+ }
340
+ .pcd-row {
341
+ margin-bottom: 16px;
342
+ }
343
+ .pcd-row--2col {
344
+ display: grid;
345
+ grid-template-columns: 1fr 1fr;
346
+ gap: 14px;
347
+ margin-bottom: 16px;
348
+ }
349
+ .pcd-field {
350
+ margin-bottom: 16px;
351
+ }
352
+ .pcd-row--2col .pcd-field {
353
+ margin-bottom: 0;
354
+ }
355
+ .pcd-row .pcd-field:last-child,
356
+ .pcd-body > .pcd-field:last-child {
357
+ margin-bottom: 0;
358
+ }
359
+ .pcd-field-label {
360
+ font-size: 12px;
361
+ font-weight: 600;
362
+ color: #334155;
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 6px;
366
+ margin-bottom: 6px;
367
+ }
368
+ .pcd-kbd {
369
+ font: 600 10.5px var(--pcd-font-mono);
370
+ background: #f1f5f9;
371
+ color: #475569;
372
+ padding: 2px 6px;
373
+ border-radius: 4px;
374
+ border: 1px solid #e2e8f0;
375
+ letter-spacing: 0.04em;
376
+ }
377
+ .pcd-input {
378
+ width: 100%;
379
+ padding: 9px 12px;
380
+ background: #fff;
381
+ border: 1px solid var(--pcd-border);
382
+ border-radius: 7px;
383
+ font: 13.5px var(--pcd-font-sans);
384
+ color: var(--pcd-text);
385
+ outline: none;
386
+ transition:
387
+ border-color 0.12s,
388
+ box-shadow 0.12s;
389
+ }
390
+ .pcd-input--textarea {
391
+ resize: vertical;
392
+ min-height: 56px;
393
+ line-height: 1.5;
394
+ }
395
+ .pcd-input:focus {
396
+ border-color: var(--pcd-primary);
397
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
398
+ }
399
+ .pcd-input--error {
400
+ border-color: var(--pcd-danger);
401
+ }
402
+ .pcd-input--error:focus {
403
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
404
+ }
405
+ .pcd-hint {
406
+ font-size: 11px;
407
+ color: var(--pcd-text-3);
408
+ margin-top: 4px;
409
+ }
410
+ .pcd-hint--error {
411
+ color: var(--pcd-danger);
412
+ }
413
+
414
+ .pcd-choice-grid {
415
+ display: grid;
416
+ grid-template-columns: 1fr 1fr;
417
+ gap: 8px;
418
+ }
419
+ .pcd-choice {
420
+ border: 1.5px solid var(--pcd-border);
421
+ border-radius: 9px;
422
+ padding: 12px 14px;
423
+ cursor: pointer;
424
+ background: #fff;
425
+ text-align: left;
426
+ transition:
427
+ border-color 0.12s,
428
+ background 0.12s,
429
+ box-shadow 0.12s;
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: 3px;
433
+ font-family: inherit;
434
+ }
435
+ .pcd-choice:hover {
436
+ background: #f8fafc;
437
+ }
438
+ .pcd-choice--selected {
439
+ border-color: var(--pcd-primary);
440
+ background: #f0f6ff;
441
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.08);
442
+ }
443
+ .pcd-choice-title {
444
+ font-size: 13px;
445
+ font-weight: 600;
446
+ color: var(--pcd-text);
447
+ }
448
+ .pcd-choice-sub {
449
+ font-size: 11.5px;
450
+ color: #64748b;
451
+ }
452
+
453
+ .pcd-foot {
454
+ padding: 14px 22px;
455
+ background: #fbfbfd;
456
+ border-top: 1px solid var(--pcd-border);
457
+ display: flex;
458
+ justify-content: flex-end;
459
+ gap: 8px;
460
+ }
461
+ .pcd-btn {
462
+ display: inline-flex;
463
+ align-items: center;
464
+ gap: 7px;
465
+ padding: 9px 16px;
466
+ border-radius: 7px;
467
+ font: 500 13px var(--pcd-font-sans);
468
+ cursor: pointer;
469
+ border: 1px solid var(--pcd-border-strong);
470
+ background: #fff;
471
+ color: var(--pcd-text);
472
+ transition:
473
+ background 0.12s,
474
+ border-color 0.12s;
475
+ }
476
+ .pcd-btn:hover:not(:disabled) {
477
+ background: #f8fafc;
478
+ }
479
+ .pcd-btn:disabled {
480
+ cursor: not-allowed;
481
+ opacity: 0.5;
482
+ }
483
+ .pcd-btn--ghost {
484
+ border-color: transparent;
485
+ background: transparent;
486
+ }
487
+ .pcd-btn--ghost:hover {
488
+ background: rgba(15, 23, 42, 0.05);
489
+ }
490
+ .pcd-btn--primary {
491
+ background: var(--pcd-primary);
492
+ border-color: var(--pcd-primary);
493
+ color: #fff;
494
+ }
495
+ .pcd-btn--primary:hover:not(:disabled) {
496
+ background: var(--pcd-primary-700);
497
+ }
498
+ .pcd-ico {
499
+ display: inline-flex;
500
+ }
501
+
502
+ @media (max-width: 540px) {
503
+ .pcd-row--2col,
504
+ .pcd-choice-grid {
505
+ grid-template-columns: 1fr;
506
+ }
507
+ }
508
+ </style>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <section class="pd-panel pd-audit">
3
+ <div class="pd-panel-head">
4
+ <div style="min-width: 0">
5
+ <h3 class="pd-panel-title">Audit-Log</h3>
6
+ <div class="pd-panel-sub">Letzte Änderungen an diesem Plan</div>
7
+ </div>
8
+ </div>
9
+ <div class="pd-audit-body">
10
+ <div v-if="loadingAudit" class="pd-diff-empty">Lade Audit-Log…</div>
11
+ <div v-for="a in auditRows" :key="a.id" class="pd-audit-row">
12
+ <span :class="['pd-audit-dot', `pd-audit-${auditKind(a.action)}`]" />
13
+ <span class="pd-audit-when">{{ formatAuditDate(a.createdAt) }}</span>
14
+ <span class="pd-audit-avatar">{{ auditInitial(a) }}</span>
15
+ <span class="pd-audit-who">{{ auditWho(a) }}</span>
16
+ <span class="pd-audit-what">{{ a.action }}</span>
17
+ </div>
18
+ </div>
19
+ </section>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import type { AuditRow } from './types.js';
24
+
25
+ defineProps<{
26
+ auditRows: AuditRow[];
27
+ loadingAudit: boolean;
28
+ }>();
29
+
30
+ function auditKind(action: string): string {
31
+ const a = action.toLowerCase();
32
+ if (a.includes('publish')) return 'publish';
33
+ if (a.includes('create') && a.includes('draft')) return 'draft';
34
+ if (a.includes('create')) return 'add';
35
+ if (a.includes('delete')) return 'remove';
36
+ return 'change';
37
+ }
38
+
39
+ function auditWho(a: AuditRow): string {
40
+ if (a.user?.firstName || a.user?.lastName) {
41
+ return `${a.user.firstName ?? ''} ${a.user.lastName ?? ''}`.trim();
42
+ }
43
+ return a.user?.email ?? a.userEmail ?? 'system';
44
+ }
45
+
46
+ function auditInitial(a: AuditRow): string {
47
+ return auditWho(a)[0]?.toUpperCase() ?? '?';
48
+ }
49
+
50
+ function formatAuditDate(iso: string): string {
51
+ const d = new Date(iso);
52
+ if (Number.isNaN(d.getTime())) return iso;
53
+ const isToday = d.toDateString() === new Date().toDateString();
54
+ const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
55
+ return isToday
56
+ ? `heute · ${time}`
57
+ : `${d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' })} · ${time}`;
58
+ }
59
+ </script>