@saasicat/ui-vue 0.21.0 → 0.22.1

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 (132) hide show
  1. package/dist/{catalog-BY_t9prv.d.cts → catalog-DJcfm5N2.d.cts} +20 -7
  2. package/dist/{catalog-BY_t9prv.d.ts → catalog-DJcfm5N2.d.ts} +20 -7
  3. package/dist/{chunk-JN6HZGLV.js → chunk-F7NRM6KI.js} +54 -28
  4. package/dist/{chunk-7IFKVGZV.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-OTP3OSSW.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +132 -29
  7. package/dist/client/index.d.cts +53 -3
  8. package/dist/client/index.d.ts +53 -3
  9. package/dist/client/index.js +14 -2
  10. package/dist/index.cjs +319 -162
  11. package/dist/index.d.cts +30 -8
  12. package/dist/index.d.ts +30 -8
  13. package/dist/index.js +176 -133
  14. package/dist/quasar/index.cjs +76 -29
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +6 -2
  18. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  19. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  20. package/dist/{use-super-admin-i18n-c_Eqsu9u.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-DlJRGIVq.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
  22. package/package.json +18 -8
  23. package/src/client/i18n/messages/bundles.ts +0 -2
  24. package/src/client/i18n/messages/common.ts +22 -0
  25. package/src/client/i18n/messages/discovery.ts +2 -2
  26. package/src/client/i18n/messages/marketing.ts +2 -4
  27. package/src/client/i18n/messages/plan-detail.ts +0 -4
  28. package/src/client/i18n/messages/plans.ts +20 -8
  29. package/src/client/i18n/messages/tenants.ts +0 -2
  30. package/src/client/index.ts +1 -0
  31. package/src/client/manifest-loader.ts +1 -1
  32. package/src/client/resolve-plans.ts +144 -0
  33. package/src/components/BundleVersionPublishDialog.vue +6 -6
  34. package/src/components/KvBlock.vue +4 -4
  35. package/src/components/MarketingPromotionsTab.vue +34 -48
  36. package/src/components/MfaPromptDialog.vue +8 -2
  37. package/src/components/TenantActionConfirmDialog.vue +4 -4
  38. package/src/components/VersionDiffPreview.vue +7 -7
  39. package/src/components/admin-page/AdminFilters.vue +12 -0
  40. package/src/components/admin-page/AdminHero.vue +22 -5
  41. package/src/components/admin-page/AdminKpi.vue +84 -0
  42. package/src/components/admin-page/AdminPaginator.vue +195 -0
  43. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  44. package/src/components/admin-page/AdminSection.vue +31 -16
  45. package/src/components/admin-page/AdminStatistics.vue +40 -0
  46. package/src/components/admin-page/AdminTable.vue +175 -0
  47. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  48. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  49. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  50. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  51. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  52. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  53. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  54. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  55. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  56. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  57. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  58. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  59. package/src/components/plan/PlanGrid.vue +1 -1
  60. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  61. package/src/components/plan-detail/PlanDetail.vue +92 -302
  62. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  63. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  64. package/src/components/plan-list/PlanList.vue +72 -290
  65. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  66. package/src/components/plan-review/PlanReview.vue +191 -275
  67. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  70. package/src/index.ts +1 -0
  71. package/src/pages-standard/AdminLayout.vue +34 -10
  72. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  73. package/src/pages-standard/AuditPage.vue +53 -72
  74. package/src/pages-standard/BundlesPage.vue +128 -144
  75. package/src/pages-standard/DashboardPage.vue +43 -107
  76. package/src/pages-standard/DiscoveryPage.vue +158 -200
  77. package/src/pages-standard/EmailHistoryPage.vue +98 -88
  78. package/src/pages-standard/MarketingCatalogPage.vue +237 -267
  79. package/src/pages-standard/PilotsPage.vue +44 -60
  80. package/src/pages-standard/PlansPage.vue +230 -105
  81. package/src/pages-standard/PlatformEmailPage.vue +37 -40
  82. package/src/pages-standard/PromoCodeDetailPage.vue +16 -30
  83. package/src/pages-standard/PromoCodesPage.vue +86 -112
  84. package/src/pages-standard/SubscriptionsPage.vue +10 -17
  85. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  86. package/src/pages-standard/SuperAdminSetupWizard.vue +20 -17
  87. package/src/pages-standard/TenantDetailPage.vue +45 -41
  88. package/src/pages-standard/TenantsPage.vue +184 -264
  89. package/src/pages-standard/UsersPage.vue +69 -104
  90. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  91. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  92. package/src/pages-standard/bundles-page/BundlesToolbar.vue +6 -10
  93. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  94. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  95. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  96. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  97. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  98. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +1 -1
  99. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +2 -13
  100. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  101. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  102. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  103. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  104. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  105. package/src/pages-standard/plans-page/PlansPageToast.vue +1 -1
  106. package/src/pages-standard/sa-theme.css +481 -71
  107. package/src/pages-standard/tenant-detail/TenantUsers.vue +2 -8
  108. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  109. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  110. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  111. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  112. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  113. package/src/quasar/create-super-admin-app.ts +4 -0
  114. package/src/vue/entitlement-provider.ts +0 -1
  115. package/src/vue/feature-router-guard.ts +0 -1
  116. package/src/vue/super-admin-context.ts +106 -1
  117. package/src/vue/use-api-list.ts +2 -1
  118. package/src/vue/use-pagination.ts +63 -0
  119. package/src/vue/use-plans.ts +1 -2
  120. package/src/vue/use-sign-out.ts +90 -0
  121. package/src/vue/use-tenant-billing.ts +1 -1
  122. package/src/vue/use-tenant-manifest.ts +0 -1
  123. package/src/components/KpiCard.vue +0 -53
  124. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1027
  125. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  126. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -99
  127. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  128. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  129. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  130. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -249
  131. package/src/components/plan-cockpit/types.ts +0 -72
  132. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
@@ -2,40 +2,40 @@
2
2
  <AdminPage class="sa-pemail">
3
3
  <AdminHero :title="resolvedTitle" :subtitle="msg.provider.subtitle">
4
4
  <template #actions>
5
- <q-btn
5
+ <button
6
6
  v-if="rows.length === 0"
7
- unelevated
8
- color="primary"
9
- icon="add"
10
- :label="msg.sender"
7
+ class="sa-btn sa-btn--primary"
8
+ type="button"
11
9
  @click="openCreate"
12
- />
13
- <q-btn flat icon="refresh" :label="common.reload" @click="reload" />
10
+ >
11
+ <q-icon name="add" size="16px" />
12
+ <span>{{ msg.sender }}</span>
13
+ </button>
14
+ <AdminRefreshBtn :loading="loading" @refresh="reload" />
14
15
  </template>
15
16
  </AdminHero>
16
17
 
17
- <div class="sa-pemail__card">
18
- <q-table
19
- flat
20
- :rows="rows"
21
- :columns="columns"
22
- row-key="id"
23
- :loading="loading"
24
- :pagination="{ rowsPerPage: 0 }"
25
- hide-pagination
26
- >
27
- <template #body-cell-active="{ row }">
28
- <q-td>
29
- <q-badge
30
- :color="row.active ? 'positive' : 'grey'"
31
- :label="
32
- row.active ? msg.provider.statusActive : msg.provider.statusInactive
33
- "
34
- />
35
- </q-td>
36
- </template>
37
- <template #body-cell-actions="{ row }">
38
- <q-td>
18
+ <AdminBody>
19
+ <AdminSection class="sa-pemail__card">
20
+ <AdminTable
21
+ :rows="rows"
22
+ :columns="columns"
23
+ :loading="loading"
24
+ storage-key="platform-email"
25
+ >
26
+ <template #body-cell-active="{ row }">
27
+ <q-td>
28
+ <q-badge
29
+ :color="row.active ? 'positive' : 'grey'"
30
+ :label="
31
+ row.active
32
+ ? msg.provider.statusActive
33
+ : msg.provider.statusInactive
34
+ "
35
+ />
36
+ </q-td>
37
+ </template>
38
+ <template #row-actions="{ row }">
39
39
  <q-btn
40
40
  flat
41
41
  dense
@@ -60,10 +60,10 @@
60
60
  :title="common.delete"
61
61
  @click="onDelete(row)"
62
62
  />
63
- </q-td>
64
- </template>
65
- </q-table>
66
- </div>
63
+ </template>
64
+ </AdminTable>
65
+ </AdminSection>
66
+ </AdminBody>
67
67
 
68
68
  <q-dialog v-model="showForm">
69
69
  <q-card class="sa-pemail__dialog">
@@ -192,10 +192,14 @@
192
192
  </template>
193
193
 
194
194
  <script setup lang="ts">
195
+ import AdminTable from '../components/admin-page/AdminTable.vue';
195
196
  import { computed, reactive, ref } from 'vue';
196
197
  import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
197
198
  import { useQuasar } from 'quasar';
199
+ import AdminRefreshBtn from '../components/admin-page/AdminRefreshBtn.vue';
200
+ import AdminBody from '../components/admin-page/AdminBody.vue';
198
201
  import AdminHero from '../components/admin-page/AdminHero.vue';
202
+ import AdminSection from '../components/admin-page/AdminSection.vue';
199
203
  import AdminPage from '../components/admin-page/AdminPage.vue';
200
204
  import { useSuperAdminNotify } from '../quasar/notify.js';
201
205
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
@@ -264,7 +268,6 @@ const columns = computed(() => [
264
268
  align: 'left' as const,
265
269
  },
266
270
  { name: 'active', label: common.value.status, field: 'active', align: 'left' as const },
267
- { name: 'actions', label: '', field: 'id' as never, align: 'right' as 'left' },
268
271
  ]);
269
272
 
270
273
  const showForm = ref(false);
@@ -457,12 +460,6 @@ async function onTest(): Promise<void> {
457
460
  </script>
458
461
 
459
462
  <style scoped>
460
- .sa-pemail__card {
461
- background: #fff;
462
- border: 1px solid var(--sa-border, #e2e8f0);
463
- border-radius: 12px;
464
- overflow: hidden;
465
- }
466
463
  .sa-pemail__dialog {
467
464
  min-width: 420px;
468
465
  }
@@ -22,14 +22,15 @@
22
22
  <template v-else>—</template>
23
23
  </template>
24
24
  <template #actions>
25
- <q-btn
25
+ <button
26
26
  v-if="editSubmit && data"
27
- color="primary"
28
- unelevated
29
- icon="edit"
30
- :label="labels.edit"
27
+ class="sa-btn sa-btn--primary"
28
+ type="button"
31
29
  @click="openEdit"
32
- />
30
+ >
31
+ <q-icon name="edit" size="16px" />
32
+ <span>{{ labels.edit }}</span>
33
+ </button>
33
34
  <slot name="header-actions" />
34
35
  </template>
35
36
  </AdminHero>
@@ -41,13 +42,13 @@
41
42
  :empty-text="labels.empty"
42
43
  >
43
44
  <template v-if="data">
44
- <AdminSection :title="labels.config" class="sa-card sa-promo-detail__section">
45
+ <AdminSection :title="labels.config">
45
46
  <slot name="config" :promo="data.promo">
46
47
  <pre class="sa-promo-detail__kv">{{ resolveFormatPromo(data.promo) }}</pre>
47
48
  </slot>
48
49
  </AdminSection>
49
50
 
50
- <AdminSection :title="labels.stats" class="sa-card sa-promo-detail__section">
51
+ <AdminSection :title="labels.stats">
51
52
  <slot name="stats" :stats="data.stats">
52
53
  <pre class="sa-promo-detail__kv">{{
53
54
  JSON.stringify(data.stats, null, 2)
@@ -55,18 +56,12 @@
55
56
  </slot>
56
57
  </AdminSection>
57
58
 
58
- <AdminSection
59
- :title="`${labels.redemptions} (${data.redemptions.length})`"
60
- class="sa-card sa-promo-detail__section"
61
- >
59
+ <AdminSection :title="`${labels.redemptions} (${data.redemptions.length})`">
62
60
  <slot name="redemptions" :redemptions="data.redemptions">
63
- <q-table
64
- flat
65
- :rows="data.redemptions"
61
+ <AdminTable
62
+ :rows="redemptionRows"
66
63
  :columns="redemptionsColumns ?? defaultColumns"
67
- row-key="id"
68
- :pagination="{ rowsPerPage: 0 }"
69
- hide-pagination
64
+ storage-key="promo-code-redemptions"
70
65
  />
71
66
  </slot>
72
67
  </AdminSection>
@@ -87,6 +82,7 @@
87
82
  </template>
88
83
 
89
84
  <script setup lang="ts">
85
+ import AdminTable from '../components/admin-page/AdminTable.vue';
90
86
  import { computed, onMounted, ref } from 'vue';
91
87
  import AdminBody from '../components/admin-page/AdminBody.vue';
92
88
  import AdminHero from '../components/admin-page/AdminHero.vue';
@@ -281,6 +277,8 @@ const defaultColumns = computed<QTableColumn[]>(() => [
281
277
  align: 'left',
282
278
  },
283
279
  ]);
280
+
281
+ const redemptionRows = computed(() => data.value?.redemptions ?? []);
284
282
  </script>
285
283
 
286
284
  <style scoped>
@@ -298,18 +296,6 @@ const defaultColumns = computed<QTableColumn[]>(() => [
298
296
  flex-direction: column;
299
297
  gap: 14px;
300
298
  }
301
- .sa-promo-detail__section {
302
- padding: 16px 18px;
303
- }
304
- .sa-promo-detail__section-head {
305
- font-size: 11px;
306
- font-weight: 700;
307
- letter-spacing: 0.08em;
308
- color: var(--sa-muted);
309
- text-transform: uppercase;
310
- margin-bottom: 10px;
311
- font-family: var(--sa-font-head);
312
- }
313
299
  .sa-promo-detail__kv {
314
300
  background: #fafbfc;
315
301
  border: 1px solid var(--sa-border);
@@ -3,74 +3,68 @@
3
3
  <AdminHero :title="msg.title" :subtitle="subtitle">
4
4
  <template #actions>
5
5
  <slot name="head-actions">
6
- <q-btn
6
+ <button
7
7
  v-if="enableCreate"
8
- unelevated
9
- color="primary"
10
- icon="add"
11
- :label="resolvedCreateLabel"
8
+ class="sa-btn sa-btn--primary"
9
+ type="button"
12
10
  @click="showCreate = true"
13
- />
11
+ >
12
+ <q-icon name="add" size="16px" />
13
+ <span>{{ resolvedCreateLabel }}</span>
14
+ </button>
14
15
  </slot>
15
16
  </template>
16
17
  </AdminHero>
17
18
 
18
- <div class="sa-stats">
19
- <button
20
- v-for="tile in statTiles"
21
- :key="tile.id"
22
- class="sa-stat"
23
- :class="[
24
- tile.tone ? `sa-stat--${tile.tone}` : null,
25
- statusFilter === tile.id ? 'sa-stat--active' : null,
26
- ]"
27
- @click="onStatusTileClick(tile.id)"
28
- >
29
- <span class="sa-stat__num">{{ tile.count }}</span>
30
- <span class="sa-stat__label">{{ tile.label }}</span>
31
- <span v-if="tile.hint" class="sa-stat__hint">{{ tile.hint }}</span>
32
- </button>
33
- </div>
34
-
35
- <div class="sa-promo-codes__filter">
36
- <q-input
37
- v-model="filter.search"
38
- outlined
39
- dense
40
- :label="msg.list.searchLabel"
41
- clearable
42
- @update:model-value="reload"
43
- />
44
- <q-select
45
- v-model="filter.status"
46
- outlined
47
- dense
48
- emit-value
49
- map-options
50
- clearable
51
- :label="common.status"
52
- :options="statusOptions"
53
- @update:model-value="reload"
54
- />
55
- </div>
56
-
57
- <div class="sa-promo-codes__card">
58
- <q-table
59
- flat
60
- :rows="filteredRows"
61
- :columns="effectiveColumns"
62
- row-key="id"
63
- :pagination="{ rowsPerPage: 0 }"
64
- :loading="loading"
65
- hide-pagination
66
- >
67
- <template #body-cell-status="{ row }">
68
- <q-td>
69
- <q-badge :color="statusColor(row.status)" :label="row.status" />
70
- </q-td>
71
- </template>
72
- <template #body-cell-actions="{ row }">
73
- <q-td>
19
+ <AdminBody>
20
+ <AdminSection>
21
+ <AdminStatistics :label="msg.title">
22
+ <AdminKpi
23
+ v-for="tile in statTiles"
24
+ :key="tile.id"
25
+ :label="tile.label"
26
+ :value="tile.count"
27
+ :sub="tile.hint"
28
+ :tone="tile.tone"
29
+ :selected="statusFilter === tile.id"
30
+ :action="() => onStatusTileClick(tile.id)"
31
+ />
32
+ </AdminStatistics>
33
+ </AdminSection>
34
+
35
+ <AdminSection class="sa-promo-codes__card">
36
+ <AdminFilters class="q-mb-lg">
37
+ <q-input
38
+ v-model="filter.search"
39
+ outlined
40
+ dense
41
+ :label="msg.list.searchLabel"
42
+ clearable
43
+ />
44
+ <q-select
45
+ v-model="filter.status"
46
+ outlined
47
+ dense
48
+ emit-value
49
+ map-options
50
+ clearable
51
+ :label="common.status"
52
+ :options="statusOptions"
53
+ />
54
+ </AdminFilters>
55
+
56
+ <AdminTable
57
+ :rows="filteredRows"
58
+ :columns="effectiveColumns"
59
+ :loading="loading"
60
+ storage-key="promo-codes"
61
+ >
62
+ <template #body-cell-status="{ row }">
63
+ <q-td>
64
+ <q-badge :color="statusColor(row.status)" :label="row.status" />
65
+ </q-td>
66
+ </template>
67
+ <template #row-actions="{ row }">
74
68
  <slot name="row-actions" :row="row" :reload="reload">
75
69
  <q-btn
76
70
  v-for="action in visibleActions(row)"
@@ -83,10 +77,10 @@
83
77
  @click="action.handler(row)"
84
78
  />
85
79
  </slot>
86
- </q-td>
87
- </template>
88
- </q-table>
89
- </div>
80
+ </template>
81
+ </AdminTable>
82
+ </AdminSection>
83
+ </AdminBody>
90
84
 
91
85
  <PromoCodeCreateDialog
92
86
  v-if="enableCreate && submitCreate"
@@ -112,6 +106,7 @@
112
106
  // `<script setup>` (the whole setup section is wrapped in setup()).
113
107
  // Pure helpers + constants therefore live here in the regular `<script>` block.
114
108
 
109
+ import AdminTable from '../components/admin-page/AdminTable.vue';
115
110
  import type { PromoCodePlanOption } from '../components/dialogs/types.js';
116
111
 
117
112
  const PLAN_COLOR_PALETTE = ['#0ea5e9', '#10b981', '#f59e0b', '#7c3aed', '#dc2626', '#64748b'];
@@ -133,8 +128,13 @@ export function computePlanColors(
133
128
 
134
129
  <script setup lang="ts">
135
130
  import { computed, onMounted, reactive, ref } from 'vue';
131
+ import AdminBody from '../components/admin-page/AdminBody.vue';
132
+ import AdminFilters from '../components/admin-page/AdminFilters.vue';
136
133
  import AdminHero from '../components/admin-page/AdminHero.vue';
134
+ import AdminSection from '../components/admin-page/AdminSection.vue';
135
+ import AdminKpi from '../components/admin-page/AdminKpi.vue';
137
136
  import AdminPage from '../components/admin-page/AdminPage.vue';
137
+ import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
138
138
  import { useQuasar } from 'quasar';
139
139
  import { formatMessage } from '../client/i18n/format.js';
140
140
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
@@ -251,11 +251,25 @@ function classifyRow(row: PromoRow): Exclude<StatusFilter, 'all'> | null {
251
251
  return null;
252
252
  }
253
253
 
254
+ // The table narrows; the tiles above it deliberately do not — see statTiles.
254
255
  const filteredRows = computed(() => {
255
- if (statusFilter.value === 'all') return rows.value;
256
- return rows.value.filter((r) => classifyRow(r) === statusFilter.value);
256
+ // `clearable` emits null, not '' — see Quasar's use-field clearValue().
257
+ const q = (filter.search ?? '').trim().toLowerCase();
258
+ const status = filter.status?.toUpperCase() ?? null;
259
+
260
+ return rows.value.filter((row) => {
261
+ if (statusFilter.value !== 'all' && classifyRow(row) !== statusFilter.value) return false;
262
+ if (status && String(row.status ?? '').toUpperCase() !== status) return false;
263
+ if (!q) return true;
264
+ return [row.code, row.description, row.campaignTag]
265
+ .filter((v): v is string => typeof v === 'string')
266
+ .some((v) => v.toLowerCase().includes(q));
267
+ });
257
268
  });
258
269
 
270
+ // Absolute counts over everything the tenant has, deliberately independent of
271
+ // the filter below: these tiles are the status filter, so counting the filtered
272
+ // set would drop every unselected tile to zero the moment one is picked.
259
273
  const statTiles = computed<
260
274
  Array<{
261
275
  id: StatusFilter;
@@ -291,22 +305,8 @@ const statTiles = computed<
291
305
  ];
292
306
  });
293
307
 
294
- // Tile click: set the filter AND, if a server status match exists, pass it
295
- // through to the search so `loadPromos` can pre-filter server-side if needed.
296
308
  function onStatusTileClick(id: StatusFilter): void {
297
309
  statusFilter.value = id;
298
- const serverStatus =
299
- id === 'active'
300
- ? 'ACTIVE'
301
- : id === 'paused'
302
- ? 'PAUSED'
303
- : id === 'expired'
304
- ? 'EXPIRED'
305
- : null;
306
- if (serverStatus !== filter.status) {
307
- filter.status = serverStatus;
308
- reload();
309
- }
310
310
  }
311
311
 
312
312
  const baseColumns = computed(() => [
@@ -394,14 +394,6 @@ const mergedActions = computed<readonly PromoRowAction[]>(() => [
394
394
 
395
395
  const effectiveColumns = computed(() => {
396
396
  const cols = [...baseColumns.value];
397
- if (mergedActions.value.length > 0) {
398
- cols.push({
399
- name: 'actions',
400
- label: '',
401
- field: 'id' as never,
402
- align: 'right' as 'left',
403
- });
404
- }
405
397
  return cols;
406
398
  });
407
399
 
@@ -419,10 +411,9 @@ function statusColor(status: string): string {
419
411
  async function reload() {
420
412
  loading.value = true;
421
413
  try {
422
- rows.value = await props.loadPromos({
423
- search: filter.search || undefined,
424
- status: filter.status || undefined,
425
- });
414
+ // Unfiltered on purpose: the tiles count the tenant's full set, and
415
+ // the table narrows locally, so filtering costs no round trip.
416
+ rows.value = await props.loadPromos({});
426
417
  } catch (err) {
427
418
  rows.value = [];
428
419
  console.warn('[PromoCodesPage] loadPromos failed:', err);
@@ -559,21 +550,4 @@ function formatDate(iso: string | Date | null | undefined): string | null {
559
550
  }
560
551
  </script>
561
552
 
562
- <style scoped>
563
- .sa-promo-codes__filter {
564
- display: flex;
565
- gap: 10px;
566
- margin-bottom: 12px;
567
- flex-wrap: wrap;
568
- }
569
- .sa-promo-codes__filter > * {
570
- flex: 1;
571
- min-width: 200px;
572
- }
573
- .sa-promo-codes__card {
574
- background: #fff;
575
- border: 1px solid var(--sa-border, #e2e8f0);
576
- border-radius: 12px;
577
- overflow: hidden;
578
- }
579
- </style>
553
+ <style scoped></style>
@@ -6,25 +6,25 @@
6
6
  </template>
7
7
  </AdminHero>
8
8
 
9
- <div class="sa-subscriptions__body">
10
- <div class="sa-subscriptions__card">
11
- <q-table
12
- flat
9
+ <AdminBody>
10
+ <AdminSection class="sa-subscriptions__body">
11
+ <AdminTable
13
12
  :rows="rows"
14
13
  :columns="effectiveColumns"
15
- row-key="id"
16
- :pagination="{ rowsPerPage: 0 }"
17
14
  :loading="loading"
18
- hide-pagination
15
+ storage-key="subscriptions"
19
16
  />
20
- </div>
21
- </div>
17
+ </AdminSection>
18
+ </AdminBody>
22
19
  </AdminPage>
23
20
  </template>
24
21
 
25
22
  <script setup lang="ts">
23
+ import AdminTable from '../components/admin-page/AdminTable.vue';
26
24
  import { computed, onMounted, ref } from 'vue';
25
+ import AdminBody from '../components/admin-page/AdminBody.vue';
27
26
  import AdminHero from '../components/admin-page/AdminHero.vue';
27
+ import AdminSection from '../components/admin-page/AdminSection.vue';
28
28
  import AdminPage from '../components/admin-page/AdminPage.vue';
29
29
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
30
30
 
@@ -128,11 +128,4 @@ function formatDate(iso: string | null | undefined): string | null {
128
128
  }
129
129
  </script>
130
130
 
131
- <style scoped>
132
- .sa-subscriptions__card {
133
- background: #fff;
134
- border: 1px solid var(--sa-border, #e2e8f0);
135
- border-radius: 12px;
136
- overflow: hidden;
137
- }
138
- </style>
131
+ <style scoped></style>
@@ -24,7 +24,7 @@
24
24
  <h1 class="sa-login-title">{{ msg.login.signIn }}</h1>
25
25
  <p v-if="subtitle" class="sa-login-subtitle">{{ subtitle }}</p>
26
26
 
27
- <q-form @submit.prevent="handleSubmit" class="sa-login-form">
27
+ <q-form class="sa-login-form" @submit.prevent="handleSubmit">
28
28
  <q-input
29
29
  v-model="form.email"
30
30
  :label="msg.emailLabel"
@@ -111,7 +111,7 @@ interface Props {
111
111
  subtitle?: string;
112
112
  }
113
113
 
114
- const props = defineProps<Props>();
114
+ defineProps<Props>();
115
115
 
116
116
  const router = useRouter();
117
117
  const msg = useSaMessages('shell');
@@ -199,7 +199,7 @@ async function handleSubmit(): Promise<void> {
199
199
  on top of the centered card on short viewports. */
200
200
  margin-left: auto;
201
201
  align-self: flex-start;
202
- color: var(--sa-login-tag-color, #475569);
202
+ color: var(--sa-login-tag-color, var(--sa-muted-dark));
203
203
  }
204
204
  .sa-login-wrap {
205
205
  min-height: 100vh;
@@ -208,7 +208,7 @@ async function handleSubmit(): Promise<void> {
208
208
  align-items: center;
209
209
  justify-content: center;
210
210
  padding: 24px;
211
- background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
211
+ background: linear-gradient(180deg, var(--sa-heading) 0%, #1e293b 100%);
212
212
  }
213
213
  .sa-login-card {
214
214
  width: 420px;
@@ -234,7 +234,7 @@ async function handleSubmit(): Promise<void> {
234
234
  overflow: hidden;
235
235
  }
236
236
  .sa-login-logo--text {
237
- background: var(--sa-login-logo-bg, linear-gradient(135deg, #475569, #1e293b));
237
+ background: var(--sa-login-logo-bg, linear-gradient(135deg, var(--sa-muted-dark), #1e293b));
238
238
  color: var(--sa-login-logo-color, #fff);
239
239
  font-weight: 800;
240
240
  font-size: 18px;
@@ -248,12 +248,12 @@ async function handleSubmit(): Promise<void> {
248
248
  .sa-login-brand__name {
249
249
  font-weight: 800;
250
250
  font-size: 16px;
251
- color: #0f172a;
251
+ color: var(--sa-heading);
252
252
  line-height: 1.1;
253
253
  }
254
254
  .sa-login-brand__tag {
255
255
  font-size: 12px;
256
- color: var(--sa-login-tag-color, #475569);
256
+ color: var(--sa-login-tag-color, var(--sa-muted-dark));
257
257
  text-transform: uppercase;
258
258
  letter-spacing: 0.08em;
259
259
  }
@@ -261,12 +261,12 @@ async function handleSubmit(): Promise<void> {
261
261
  .sa-login-title {
262
262
  font-size: 22px;
263
263
  font-weight: 800;
264
- color: #0f172a;
264
+ color: var(--sa-heading);
265
265
  margin: 0 0 6px;
266
266
  }
267
267
  .sa-login-subtitle {
268
268
  font-size: 13px;
269
- color: #475569;
269
+ color: var(--sa-muted-dark);
270
270
  margin: 0 0 20px;
271
271
  line-height: 1.5;
272
272
  }
@@ -290,13 +290,13 @@ async function handleSubmit(): Promise<void> {
290
290
  .sa-login-hint {
291
291
  margin-top: 18px;
292
292
  padding: 10px 12px;
293
- background: #f1f5f9;
293
+ background: var(--sa-border-soft);
294
294
  border-radius: 8px;
295
295
  font-size: 12px;
296
- color: #475569;
296
+ color: var(--sa-muted-dark);
297
297
  }
298
298
  .sa-login-hint code {
299
- background: #e2e8f0;
299
+ background: var(--sa-border);
300
300
  padding: 1px 5px;
301
301
  border-radius: 4px;
302
302
  }