@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,662 @@
1
+ <template>
2
+ <div class="sa-bts">
3
+ <header class="sa-bts__head">
4
+ <div>
5
+ <h1 class="sa-bts__title">BusinessTypes</h1>
6
+ <p class="sa-bts__sub">
7
+ {{ businessTypes.length }} BusinessType{{
8
+ businessTypes.length === 1 ? '' : 's'
9
+ }}
10
+ · Projekt {{ projectKey }}
11
+ </p>
12
+ </div>
13
+ <div class="sa-bts__head-actions">
14
+ <q-btn
15
+ unelevated
16
+ color="primary"
17
+ icon="add"
18
+ label="Neuer BusinessType"
19
+ @click="openCreateDialog"
20
+ />
21
+ <q-btn flat icon="refresh" :loading="loading" @click="load" />
22
+ </div>
23
+ </header>
24
+
25
+ <q-banner v-if="error" class="sa-bts__error" inline-actions rounded>
26
+ <template #avatar><q-icon name="warning" color="negative" /></template>
27
+ Fehler beim Laden: {{ error.message }}
28
+ </q-banner>
29
+
30
+ <q-banner
31
+ v-if="businessTypes.length === 0 && !loading && !error"
32
+ class="sa-bts__empty"
33
+ inline-actions
34
+ rounded
35
+ >
36
+ <template #avatar><q-icon name="info" color="info" /></template>
37
+ Noch keine BusinessTypes angelegt. BusinessTypes komponieren mehrere published
38
+ BundleVersions zu einer fachlichen Vertikale (z. B. Vereinstyp).
39
+ </q-banner>
40
+
41
+ <div v-if="businessTypes.length > 0" class="sa-bts__card">
42
+ <q-table
43
+ flat
44
+ :rows="businessTypes"
45
+ :columns="columns"
46
+ row-key="id"
47
+ :pagination="{ rowsPerPage: 0 }"
48
+ hide-pagination
49
+ @row-click="(_, row) => openDetail(row)"
50
+ >
51
+ <template #body-cell-businessTypeKey="{ row }">
52
+ <q-td>
53
+ <code class="sa-bts__key">{{ row.businessTypeKey }}</code>
54
+ </q-td>
55
+ </template>
56
+ <template #body-cell-actions="{ row }">
57
+ <q-td>
58
+ <q-btn
59
+ flat
60
+ dense
61
+ icon="edit"
62
+ color="primary"
63
+ @click.stop="openDetail(row)"
64
+ />
65
+ <q-btn
66
+ flat
67
+ dense
68
+ icon="delete"
69
+ color="negative"
70
+ @click.stop="confirmDelete(row)"
71
+ />
72
+ </q-td>
73
+ </template>
74
+ </q-table>
75
+ </div>
76
+
77
+ <!-- Create dialog -->
78
+ <q-dialog v-model="createOpen">
79
+ <q-card style="min-width: 480px; max-width: 96vw">
80
+ <q-card-section>
81
+ <div class="text-h6">Neuer BusinessType</div>
82
+ </q-card-section>
83
+ <q-card-section class="sa-bts__form">
84
+ <q-input
85
+ v-model="createForm.businessTypeKey"
86
+ outlined
87
+ dense
88
+ label="BusinessType-Key (SCREAMING_SNAKE_CASE)"
89
+ />
90
+ <q-input v-model="createForm.label" outlined dense label="Label" />
91
+ <q-input
92
+ v-model="createForm.description"
93
+ outlined
94
+ dense
95
+ type="textarea"
96
+ autogrow
97
+ label="Beschreibung"
98
+ />
99
+ <q-input v-model="createForm.icon" outlined dense label="Icon (optional)" />
100
+ <q-input
101
+ v-model.number="createForm.sortOrder"
102
+ outlined
103
+ dense
104
+ type="number"
105
+ label="Sortier-Reihenfolge"
106
+ />
107
+ </q-card-section>
108
+ <q-card-actions align="right">
109
+ <q-btn v-close-popup flat label="Abbrechen" />
110
+ <q-btn
111
+ unelevated
112
+ color="primary"
113
+ label="Anlegen"
114
+ :loading="createSubmitting"
115
+ :disable="!canSubmitCreate"
116
+ @click="submitCreate"
117
+ />
118
+ </q-card-actions>
119
+ </q-card>
120
+ </q-dialog>
121
+
122
+ <!-- Detail drawer -->
123
+ <q-drawer v-model="detailOpen" side="right" :width="560" bordered overlay>
124
+ <div v-if="detailType" class="sa-bts__drawer">
125
+ <header class="sa-bts__drawer-head">
126
+ <div>
127
+ <code class="sa-bts__key">{{ detailType.businessTypeKey }}</code>
128
+ <h2 class="sa-bts__drawer-title">{{ detailType.label }}</h2>
129
+ </div>
130
+ <q-btn flat dense icon="close" @click="detailOpen = false" />
131
+ </header>
132
+
133
+ <section class="sa-bts__form">
134
+ <q-input v-model="editForm.label" outlined dense label="Label" />
135
+ <q-input
136
+ v-model="editForm.description"
137
+ outlined
138
+ dense
139
+ type="textarea"
140
+ autogrow
141
+ label="Beschreibung"
142
+ />
143
+ <q-input v-model="editForm.icon" outlined dense label="Icon" />
144
+ <q-input
145
+ v-model.number="editForm.sortOrder"
146
+ outlined
147
+ dense
148
+ type="number"
149
+ label="Sortier-Reihenfolge"
150
+ />
151
+ <q-btn
152
+ unelevated
153
+ color="primary"
154
+ label="Speichern"
155
+ :loading="editSubmitting"
156
+ @click="submitEdit"
157
+ />
158
+ </section>
159
+
160
+ <q-separator class="q-my-md" />
161
+
162
+ <section>
163
+ <div class="sa-bts__drawer-subhead">
164
+ <strong>Versionen</strong>
165
+ <span class="text-caption text-grey-7">
166
+ {{ detailVersions.length }}
167
+ </span>
168
+ </div>
169
+ <q-banner
170
+ v-if="detailVersions.length === 0"
171
+ class="sa-bts__empty"
172
+ inline-actions
173
+ rounded
174
+ >
175
+ Noch keine Version. „Neuer Draft" anlegen.
176
+ </q-banner>
177
+ <q-list v-else bordered separator>
178
+ <q-item v-for="v in detailVersions" :key="v.id">
179
+ <q-item-section>
180
+ <q-item-label>v{{ v.version }}</q-item-label>
181
+ <q-item-label caption>
182
+ {{ v.bundles.length }} Bundle{{
183
+ v.bundles.length === 1 ? '' : 's'
184
+ }}
185
+ · {{ v.monthlyNet ?? 'Σ Bundle-Preise' }}
186
+ </q-item-label>
187
+ </q-item-section>
188
+ <q-item-section side>
189
+ <div class="sa-bts__version-actions">
190
+ <q-chip dense :color="versionStatusColor(v)" text-color="white">
191
+ {{ versionStatusLabel(v) }}
192
+ </q-chip>
193
+ <q-btn
194
+ v-if="v.publishedAt === null"
195
+ flat
196
+ dense
197
+ icon="edit"
198
+ color="primary"
199
+ @click="openEditDraft(v)"
200
+ />
201
+ <q-btn
202
+ v-if="v.publishedAt === null"
203
+ flat
204
+ dense
205
+ icon="rocket_launch"
206
+ color="positive"
207
+ @click="openPublish(v)"
208
+ />
209
+ </div>
210
+ </q-item-section>
211
+ </q-item>
212
+ </q-list>
213
+
214
+ <div v-if="!hasDraft && detailType" class="sa-bts__new-draft">
215
+ <q-btn
216
+ unelevated
217
+ color="primary"
218
+ icon="add"
219
+ label="Neuer Draft"
220
+ @click="openCreateDraft"
221
+ />
222
+ </div>
223
+ <q-banner v-else-if="hasDraft" class="sa-bts__hint" inline-actions rounded>
224
+ Hat bereits eine Draft-Version.
225
+ </q-banner>
226
+ </section>
227
+ </div>
228
+ </q-drawer>
229
+
230
+ <!-- Strict mode warnings -->
231
+ <q-banner
232
+ v-if="lastWarnings.length > 0"
233
+ class="sa-bts__warnings-banner"
234
+ inline-actions
235
+ rounded
236
+ >
237
+ <template #avatar><q-icon name="warning" color="warning" /></template>
238
+ <strong>{{ lastWarnings.length }} Strict-Mode-Warnung(en) bei letzter Operation</strong>
239
+ <ul class="sa-bts__warnings-list">
240
+ <li v-for="(w, i) in lastWarnings" :key="i">
241
+ <code>{{ w.code }}</code>
242
+ <template v-if="w.value">
243
+ · <code>{{ w.value }}</code></template
244
+ >
245
+ — {{ w.message }}
246
+ </li>
247
+ </ul>
248
+ <template #action>
249
+ <q-btn flat dense label="Schließen" @click="lastWarnings = []" />
250
+ </template>
251
+ </q-banner>
252
+
253
+ <!-- Editor + publish dialogs -->
254
+ <BusinessTypeVersionEditorDialog
255
+ v-if="detailType && editorOpen"
256
+ v-model="editorOpen"
257
+ :mode="editorMode"
258
+ :business-type-key="detailType.businessTypeKey"
259
+ :draft="editorDraft"
260
+ :version-number="editorVersionNumber"
261
+ :available-bundles="availableBundles"
262
+ :submit="onEditorSubmit"
263
+ @submitted="onEditorSubmitted"
264
+ />
265
+ <BusinessTypeVersionPublishDialog
266
+ v-if="detailType && publishOpen && publishDraft"
267
+ v-model="publishOpen"
268
+ :business-type-key="detailType.businessTypeKey"
269
+ :draft="publishDraft"
270
+ :previous="publishPrevious"
271
+ :warnings="lastWarnings"
272
+ :classify-diff="classifyDiff"
273
+ :submit="onPublishSubmit"
274
+ @submitted="onPublishSubmitted"
275
+ />
276
+ </div>
277
+ </template>
278
+
279
+ <script setup lang="ts">
280
+ import { computed, ref, watch } from 'vue';
281
+ import type {
282
+ BundleVersionRow,
283
+ BusinessTypeRow,
284
+ BusinessTypeVersionMutationResult,
285
+ BusinessTypeVersionRow,
286
+ CreateBusinessTypeData,
287
+ CreateBusinessTypeVersionDraftData,
288
+ StrictModeWarning,
289
+ UpdateBusinessTypeData,
290
+ UpdateBusinessTypeVersionDraftData,
291
+ VersionChange,
292
+ } from '@saasicat/types';
293
+
294
+ import BusinessTypeVersionEditorDialog from '../components/BusinessTypeVersionEditorDialog.vue';
295
+ import BusinessTypeVersionPublishDialog from '../components/BusinessTypeVersionPublishDialog.vue';
296
+
297
+ interface QTableColumn {
298
+ name: string;
299
+ label: string;
300
+ field: string | ((row: unknown) => unknown);
301
+ align?: 'left' | 'right' | 'center';
302
+ sortable?: boolean;
303
+ }
304
+
305
+ const props = defineProps<{
306
+ projectKey: string;
307
+ businessTypes: BusinessTypeRow[];
308
+ loading: boolean;
309
+ error: Error | null;
310
+ load: () => Promise<void>;
311
+ create: (data: CreateBusinessTypeData) => Promise<BusinessTypeRow>;
312
+ update: (businessTypeId: string, data: UpdateBusinessTypeData) => Promise<BusinessTypeRow>;
313
+ softDelete: (businessTypeId: string) => Promise<void>;
314
+ loadVersions: (businessTypeId: string) => Promise<BusinessTypeVersionRow[]>;
315
+ createDraft: (
316
+ businessTypeId: string,
317
+ data: Omit<CreateBusinessTypeVersionDraftData, 'businessTypeId'>,
318
+ ) => Promise<BusinessTypeVersionMutationResult>;
319
+ updateDraft: (
320
+ versionId: string,
321
+ data: UpdateBusinessTypeVersionDraftData,
322
+ ) => Promise<BusinessTypeVersionMutationResult>;
323
+ publish: (
324
+ versionId: string,
325
+ opts: { forceRegressive?: boolean },
326
+ ) => Promise<BusinessTypeVersionMutationResult>;
327
+ /** List of all available published BundleVersions in the project. */
328
+ availableBundles: BundleVersionRow[];
329
+ /** Pure-function diff classifier. */
330
+ classifyDiff: (
331
+ previous: BusinessTypeVersionRow,
332
+ draft: BusinessTypeVersionRow,
333
+ ) => { changes: VersionChange[]; nonRegressive: boolean };
334
+ }>();
335
+
336
+ const columns: QTableColumn[] = [
337
+ {
338
+ name: 'businessTypeKey',
339
+ label: 'Key',
340
+ field: 'businessTypeKey',
341
+ align: 'left',
342
+ sortable: true,
343
+ },
344
+ { name: 'label', label: 'Label', field: 'label', align: 'left', sortable: true },
345
+ {
346
+ name: 'description',
347
+ label: 'Beschreibung',
348
+ field: (row) => (row as BusinessTypeRow).description ?? '—',
349
+ align: 'left',
350
+ },
351
+ {
352
+ name: 'sortOrder',
353
+ label: 'Sort',
354
+ field: 'sortOrder',
355
+ align: 'right',
356
+ sortable: true,
357
+ },
358
+ { name: 'actions', label: '', field: () => '', align: 'right' },
359
+ ];
360
+
361
+ // ─── Create dialog ───
362
+ const createOpen = ref(false);
363
+ const createSubmitting = ref(false);
364
+ const createForm = ref<{
365
+ businessTypeKey: string;
366
+ label: string;
367
+ description: string;
368
+ icon: string;
369
+ sortOrder: number;
370
+ }>({
371
+ businessTypeKey: '',
372
+ label: '',
373
+ description: '',
374
+ icon: '',
375
+ sortOrder: 0,
376
+ });
377
+
378
+ const canSubmitCreate = computed(
379
+ () => createForm.value.businessTypeKey.length > 0 && createForm.value.label.length > 0,
380
+ );
381
+
382
+ function openCreateDialog(): void {
383
+ createForm.value = {
384
+ businessTypeKey: '',
385
+ label: '',
386
+ description: '',
387
+ icon: '',
388
+ sortOrder: 0,
389
+ };
390
+ createOpen.value = true;
391
+ }
392
+
393
+ async function submitCreate(): Promise<void> {
394
+ createSubmitting.value = true;
395
+ try {
396
+ await props.create({
397
+ projectKey: props.projectKey,
398
+ businessTypeKey: createForm.value.businessTypeKey,
399
+ label: createForm.value.label,
400
+ description: createForm.value.description || undefined,
401
+ icon: createForm.value.icon || undefined,
402
+ sortOrder: createForm.value.sortOrder,
403
+ });
404
+ createOpen.value = false;
405
+ } finally {
406
+ createSubmitting.value = false;
407
+ }
408
+ }
409
+
410
+ // ─── Detail drawer ───
411
+ const detailOpen = ref(false);
412
+ const detailType = ref<BusinessTypeRow | null>(null);
413
+ const detailVersions = ref<BusinessTypeVersionRow[]>([]);
414
+ const editForm = ref<UpdateBusinessTypeData>({
415
+ label: '',
416
+ description: '',
417
+ icon: '',
418
+ sortOrder: 0,
419
+ });
420
+ const editSubmitting = ref(false);
421
+
422
+ async function openDetail(bt: BusinessTypeRow): Promise<void> {
423
+ detailType.value = bt;
424
+ editForm.value = {
425
+ label: bt.label,
426
+ description: bt.description ?? '',
427
+ icon: bt.icon ?? '',
428
+ sortOrder: bt.sortOrder,
429
+ };
430
+ detailOpen.value = true;
431
+ detailVersions.value = await props.loadVersions(bt.id);
432
+ }
433
+
434
+ async function submitEdit(): Promise<void> {
435
+ if (!detailType.value) return;
436
+ editSubmitting.value = true;
437
+ try {
438
+ const updated = await props.update(detailType.value.id, {
439
+ label: editForm.value.label,
440
+ description: editForm.value.description || null,
441
+ icon: editForm.value.icon || null,
442
+ sortOrder: editForm.value.sortOrder,
443
+ });
444
+ detailType.value = updated;
445
+ } finally {
446
+ editSubmitting.value = false;
447
+ }
448
+ }
449
+
450
+ async function confirmDelete(bt: BusinessTypeRow): Promise<void> {
451
+ const ok = window.confirm(`BusinessType '${bt.businessTypeKey}' wirklich soft-deleten?`);
452
+ if (!ok) return;
453
+ await props.softDelete(bt.id);
454
+ if (detailType.value?.id === bt.id) {
455
+ detailOpen.value = false;
456
+ detailType.value = null;
457
+ }
458
+ }
459
+
460
+ watch(
461
+ () => props.businessTypes,
462
+ (next) => {
463
+ if (detailType.value && !next.some((b) => b.id === detailType.value!.id)) {
464
+ detailOpen.value = false;
465
+ detailType.value = null;
466
+ }
467
+ },
468
+ );
469
+
470
+ // ─── Strict mode warnings ───
471
+ const lastWarnings = ref<StrictModeWarning[]>([]);
472
+
473
+ // ─── Editor modal ───
474
+ const editorOpen = ref(false);
475
+ const editorMode = ref<'create' | 'edit'>('create');
476
+ const editorDraft = ref<BusinessTypeVersionRow | null>(null);
477
+
478
+ const hasDraft = computed(() => detailVersions.value.some((v) => v.publishedAt === null));
479
+
480
+ const editorVersionNumber = computed(() => {
481
+ if (editorMode.value === 'edit' && editorDraft.value) return editorDraft.value.version;
482
+ if (detailVersions.value.length === 0) return 1;
483
+ return Math.max(...detailVersions.value.map((v) => v.version)) + 1;
484
+ });
485
+
486
+ function openCreateDraft(): void {
487
+ if (hasDraft.value) return;
488
+ editorMode.value = 'create';
489
+ editorDraft.value = null;
490
+ editorOpen.value = true;
491
+ }
492
+
493
+ function openEditDraft(v: BusinessTypeVersionRow): void {
494
+ if (v.publishedAt !== null) return;
495
+ editorMode.value = 'edit';
496
+ editorDraft.value = v;
497
+ editorOpen.value = true;
498
+ }
499
+
500
+ async function onEditorSubmit(
501
+ data: CreateBusinessTypeVersionDraftData | UpdateBusinessTypeVersionDraftData,
502
+ ): Promise<BusinessTypeVersionMutationResult> {
503
+ if (editorMode.value === 'create' && detailType.value) {
504
+ return props.createDraft(detailType.value.id, data as CreateBusinessTypeVersionDraftData);
505
+ }
506
+ if (editorMode.value === 'edit' && editorDraft.value) {
507
+ return props.updateDraft(editorDraft.value.id, data as UpdateBusinessTypeVersionDraftData);
508
+ }
509
+ throw new Error('BusinessTypesPage: editor submit ohne Kontext');
510
+ }
511
+
512
+ async function onEditorSubmitted(result: BusinessTypeVersionMutationResult): Promise<void> {
513
+ lastWarnings.value = result.warnings;
514
+ if (detailType.value) {
515
+ detailVersions.value = await props.loadVersions(detailType.value.id);
516
+ }
517
+ }
518
+
519
+ // ─── Publish modal ───
520
+ const publishOpen = ref(false);
521
+ const publishDraft = ref<BusinessTypeVersionRow | null>(null);
522
+
523
+ const publishPrevious = computed<BusinessTypeVersionRow | null>(() => {
524
+ if (!publishDraft.value) return null;
525
+ return (
526
+ detailVersions.value
527
+ .filter((v) => v.publishedAt !== null && v.supersededAt === null)
528
+ .sort((a, b) => b.version - a.version)[0] ?? null
529
+ );
530
+ });
531
+
532
+ function openPublish(v: BusinessTypeVersionRow): void {
533
+ if (v.publishedAt !== null) return;
534
+ publishDraft.value = v;
535
+ publishOpen.value = true;
536
+ }
537
+
538
+ async function onPublishSubmit(opts: {
539
+ forceRegressive: boolean;
540
+ }): Promise<BusinessTypeVersionMutationResult> {
541
+ if (!publishDraft.value) {
542
+ throw new Error('BusinessTypesPage: publish submit ohne Draft');
543
+ }
544
+ return props.publish(publishDraft.value.id, opts);
545
+ }
546
+
547
+ async function onPublishSubmitted(result: BusinessTypeVersionMutationResult): Promise<void> {
548
+ lastWarnings.value = result.warnings;
549
+ publishDraft.value = null;
550
+ if (detailType.value) {
551
+ detailVersions.value = await props.loadVersions(detailType.value.id);
552
+ }
553
+ }
554
+
555
+ function versionStatusLabel(v: BusinessTypeVersionRow): string {
556
+ if (v.publishedAt === null) return 'Draft';
557
+ if (v.supersededAt !== null) return 'Superseded';
558
+ return v.marketed ? 'Live · marketed' : 'Live · intern';
559
+ }
560
+
561
+ function versionStatusColor(v: BusinessTypeVersionRow): string {
562
+ if (v.publishedAt === null) return 'warning';
563
+ if (v.supersededAt !== null) return 'grey';
564
+ return v.marketed ? 'positive' : 'info';
565
+ }
566
+ </script>
567
+
568
+ <style scoped>
569
+ .sa-bts {
570
+ padding: 16px;
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 16px;
574
+ }
575
+ .sa-bts__head {
576
+ display: flex;
577
+ justify-content: space-between;
578
+ align-items: flex-start;
579
+ gap: 16px;
580
+ }
581
+ .sa-bts__title {
582
+ font-size: 20px;
583
+ font-weight: 600;
584
+ margin: 0;
585
+ }
586
+ .sa-bts__sub {
587
+ margin: 4px 0 0;
588
+ color: var(--q-grey-7, #757575);
589
+ font-size: 12px;
590
+ }
591
+ .sa-bts__head-actions {
592
+ display: flex;
593
+ align-items: center;
594
+ gap: 12px;
595
+ }
596
+ .sa-bts__error {
597
+ border-left: 4px solid var(--q-negative, #c10015);
598
+ }
599
+ .sa-bts__empty {
600
+ border-left: 4px solid var(--q-info, #31ccec);
601
+ }
602
+ .sa-bts__card {
603
+ background: white;
604
+ border-radius: 8px;
605
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
606
+ }
607
+ .sa-bts__key {
608
+ background: rgba(0, 0, 0, 0.05);
609
+ padding: 2px 6px;
610
+ border-radius: 4px;
611
+ font-size: 12px;
612
+ font-weight: 500;
613
+ }
614
+ .sa-bts__form {
615
+ display: flex;
616
+ flex-direction: column;
617
+ gap: 12px;
618
+ }
619
+ .sa-bts__drawer {
620
+ padding: 16px;
621
+ display: flex;
622
+ flex-direction: column;
623
+ gap: 12px;
624
+ }
625
+ .sa-bts__drawer-head {
626
+ display: flex;
627
+ justify-content: space-between;
628
+ align-items: flex-start;
629
+ gap: 12px;
630
+ }
631
+ .sa-bts__drawer-title {
632
+ font-size: 18px;
633
+ font-weight: 600;
634
+ margin: 4px 0 0;
635
+ }
636
+ .sa-bts__drawer-subhead {
637
+ display: flex;
638
+ justify-content: space-between;
639
+ align-items: baseline;
640
+ margin-bottom: 8px;
641
+ }
642
+ .sa-bts__version-actions {
643
+ display: flex;
644
+ align-items: center;
645
+ gap: 4px;
646
+ }
647
+ .sa-bts__new-draft {
648
+ margin-top: 12px;
649
+ }
650
+ .sa-bts__hint {
651
+ border-left: 4px solid var(--q-info, #31ccec);
652
+ margin-top: 12px;
653
+ }
654
+ .sa-bts__warnings-banner {
655
+ border-left: 4px solid var(--q-warning, #f2c037);
656
+ }
657
+ .sa-bts__warnings-list {
658
+ margin: 8px 0 0;
659
+ padding-left: 20px;
660
+ font-size: 13px;
661
+ }
662
+ </style>