@7365admin1/layer-common 3.2.8-staging.202 → 3.2.8-staging.204
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.
- package/components/ClientDetailForm.vue +85 -263
- package/components/ClientMain.vue +145 -68
- package/components/ClientSubscriptionForm.vue +527 -0
- package/composables/useSubscription.ts +40 -0
- package/package.json +1 -1
- package/utils/client-subscription.test.ts +145 -0
- package/utils/client-subscription.ts +157 -0
- package/utils/subscription-form.test.ts +284 -0
- package/utils/subscription-form.ts +265 -0
|
@@ -37,19 +37,6 @@
|
|
|
37
37
|
</svg>
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
-
<div class="filter-field">
|
|
41
|
-
<label>Filter by plan type</label>
|
|
42
|
-
<select v-model="filterPlan" @change="handleFilterChange">
|
|
43
|
-
<option value="">All</option>
|
|
44
|
-
<option v-for="app in APP_LIST" :key="app.value" :value="app.value">
|
|
45
|
-
{{ app.title }}
|
|
46
|
-
</option>
|
|
47
|
-
</select>
|
|
48
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
49
|
-
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
50
|
-
</svg>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
40
|
<div class="filter-field">
|
|
54
41
|
<label>Filter by billing cycle</label>
|
|
55
42
|
<select v-model="filterBilling" @change="handleFilterChange">
|
|
@@ -69,6 +56,11 @@
|
|
|
69
56
|
<div class="table-card">
|
|
70
57
|
|
|
71
58
|
<!-- Toolbar row -->
|
|
59
|
+
<div v-if="savedNote" class="saved-note" role="status">
|
|
60
|
+
{{ savedNote }}
|
|
61
|
+
<button class="saved-note-close" @click="savedNote = ''">×</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
72
64
|
<div class="toolbar">
|
|
73
65
|
<div class="toolbar-left">
|
|
74
66
|
<button class="refresh-btn" :class="{ spinning: loading }" @click="handleRefresh">
|
|
@@ -111,10 +103,10 @@
|
|
|
111
103
|
<th>Organization Name</th>
|
|
112
104
|
<th>Email</th>
|
|
113
105
|
<th>Sites</th>
|
|
114
|
-
<th>
|
|
106
|
+
<th>Subscription</th>
|
|
115
107
|
<th>Billing Cycle</th>
|
|
116
|
-
<th>
|
|
117
|
-
<th>
|
|
108
|
+
<th>Start Date</th>
|
|
109
|
+
<th>End Date</th>
|
|
118
110
|
<th>Status</th>
|
|
119
111
|
<th>Actions</th>
|
|
120
112
|
</tr>
|
|
@@ -129,28 +121,46 @@
|
|
|
129
121
|
<td>
|
|
130
122
|
<span class="sites-badge">{{ item.sites }}</span>
|
|
131
123
|
</td>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</span>
|
|
143
|
-
</td>
|
|
144
|
-
<td>
|
|
145
|
-
<span class="date-cell">
|
|
146
|
-
<svg viewBox="0 0 16 16" fill="none" width="13" height="13">
|
|
147
|
-
<rect x="1" y="3" width="14" height="12" rx="2" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
148
|
-
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
149
|
-
<path d="M5 1v3M11 1v3" stroke="#9CA3AF" stroke-width="1.3" stroke-linecap="round"/>
|
|
150
|
-
</svg>
|
|
151
|
-
{{ item.subscriptionEnd }}
|
|
152
|
-
</span>
|
|
124
|
+
<!--
|
|
125
|
+
A client nobody has set up yet used to print four dashes here,
|
|
126
|
+
which reads as a broken screen. It is one ordinary state, so it
|
|
127
|
+
is said once, in words.
|
|
128
|
+
-->
|
|
129
|
+
<td v-if="item.sub.state === 'none'" colspan="4" class="sub-none">
|
|
130
|
+
No subscription set up
|
|
131
|
+
<button class="sub-setup-btn" @click="openSubscription(item)">
|
|
132
|
+
Set up subscription
|
|
133
|
+
</button>
|
|
153
134
|
</td>
|
|
135
|
+
|
|
136
|
+
<template v-else>
|
|
137
|
+
<td>
|
|
138
|
+
<span class="sub-label" :class="{ 'needs-attention': item.sub.needsAttention }">
|
|
139
|
+
{{ item.sub.label }}
|
|
140
|
+
</span>
|
|
141
|
+
</td>
|
|
142
|
+
<td class="td-muted">{{ item.sub.billingCycle }}</td>
|
|
143
|
+
<td>
|
|
144
|
+
<span class="date-cell">
|
|
145
|
+
<svg viewBox="0 0 16 16" fill="none" width="13" height="13">
|
|
146
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
147
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
148
|
+
<path d="M5 1v3M11 1v3" stroke="#9CA3AF" stroke-width="1.3" stroke-linecap="round"/>
|
|
149
|
+
</svg>
|
|
150
|
+
{{ item.sub.start }}
|
|
151
|
+
</span>
|
|
152
|
+
</td>
|
|
153
|
+
<td>
|
|
154
|
+
<span class="date-cell" :class="{ 'needs-attention': item.sub.needsAttention }">
|
|
155
|
+
<svg viewBox="0 0 16 16" fill="none" width="13" height="13">
|
|
156
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
157
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
158
|
+
<path d="M5 1v3M11 1v3" stroke="#9CA3AF" stroke-width="1.3" stroke-linecap="round"/>
|
|
159
|
+
</svg>
|
|
160
|
+
{{ item.sub.end }}
|
|
161
|
+
</span>
|
|
162
|
+
</td>
|
|
163
|
+
</template>
|
|
154
164
|
<td>
|
|
155
165
|
<span :class="['status-chip', item.status]">{{ item.status }}</span>
|
|
156
166
|
</td>
|
|
@@ -210,9 +220,24 @@
|
|
|
210
220
|
:client="selectedClient"
|
|
211
221
|
@cancel="cancelView"
|
|
212
222
|
@submit="submitClient"
|
|
223
|
+
@manage-subscription="openSubscription(selectedClient)"
|
|
213
224
|
/>
|
|
214
225
|
</div>
|
|
215
226
|
|
|
227
|
+
<!--
|
|
228
|
+
SET UP OR CHANGE A CLIENT'S SUBSCRIPTION. Opened from the row that has
|
|
229
|
+
none, from the row menu, or from the client's own drawer - all three land
|
|
230
|
+
here, so there is one form and one set of rules behind them.
|
|
231
|
+
-->
|
|
232
|
+
<ClientSubscriptionForm
|
|
233
|
+
v-if="subscriptionClient"
|
|
234
|
+
:org-id="subscriptionClient._id"
|
|
235
|
+
:client-name="subscriptionClient.name"
|
|
236
|
+
:site-count="subscriptionClient.sites"
|
|
237
|
+
@cancel="subscriptionClient = null"
|
|
238
|
+
@saved="onSubscriptionSaved"
|
|
239
|
+
/>
|
|
240
|
+
|
|
216
241
|
<div v-if="dialog" class="overlay" @click.self="dialog = false">
|
|
217
242
|
<div class="dialog-box">
|
|
218
243
|
<InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
|
|
@@ -245,6 +270,9 @@
|
|
|
245
270
|
<button class="dd-item" @click="viewClient(activeMenuItem)">
|
|
246
271
|
View Organization
|
|
247
272
|
</button>
|
|
273
|
+
<button class="dd-item" @click="openSubscription(activeMenuItem)">
|
|
274
|
+
{{ activeMenuItem?.sub?.state === 'none' ? 'Set up subscription' : 'Edit subscription' }}
|
|
275
|
+
</button>
|
|
248
276
|
<button
|
|
249
277
|
v-if="activeMenuItem?.status === 'suspended'"
|
|
250
278
|
class="dd-item"
|
|
@@ -271,8 +299,9 @@ import useVerification from '../composables/useVerification'
|
|
|
271
299
|
import useRole from '../composables/useRole'
|
|
272
300
|
import InvitationClientForm from './InvitationClientForm.vue'
|
|
273
301
|
import ClientDetailForm from './ClientDetailForm.vue'
|
|
302
|
+
import ClientSubscriptionForm from './ClientSubscriptionForm.vue'
|
|
274
303
|
import useCustomerSite from '../composables/useCustomerSite'
|
|
275
|
-
import
|
|
304
|
+
import { describeClientSubscription } from '../utils/client-subscription'
|
|
276
305
|
|
|
277
306
|
|
|
278
307
|
/* ── TYPES ── */
|
|
@@ -282,7 +311,6 @@ type TabStatus = 'active' | 'suspended' | 'pending'
|
|
|
282
311
|
const { getOrganizationsWithSubscription, updateStatus } = useOrg()
|
|
283
312
|
const { getVerifications, cancelUserInvitation } = useVerification()
|
|
284
313
|
const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
|
|
285
|
-
const { getByOrgId: getSubscriptionByOrgId } = useSubscription()
|
|
286
314
|
const { getRoleById } = useRole()
|
|
287
315
|
|
|
288
316
|
/* ── CONSTANTS ── */
|
|
@@ -306,7 +334,6 @@ const tableScrollEl = ref<HTMLElement | null>(null)
|
|
|
306
334
|
|
|
307
335
|
// filterStatus
|
|
308
336
|
const filterStatus = ref<'active' | 'suspended'>('active')
|
|
309
|
-
const filterPlan = ref('') // '' = All | any APP_LIST value
|
|
310
337
|
const filterBilling = ref('') // '' = All | 'monthly' | 'yearly'
|
|
311
338
|
|
|
312
339
|
const items = ref<any[]>([])
|
|
@@ -321,6 +348,10 @@ const roleCache = ref<Record<string, string>>({})
|
|
|
321
348
|
const viewMode = ref<'table' | 'detail'>('table')
|
|
322
349
|
const selectedClient = ref<any>(null)
|
|
323
350
|
|
|
351
|
+
/* subscription set-up / edit */
|
|
352
|
+
const subscriptionClient = ref<any>(null)
|
|
353
|
+
const savedNote = ref('')
|
|
354
|
+
|
|
324
355
|
/* suspend confirmation */
|
|
325
356
|
const confirmSuspendItem = ref<any>(null)
|
|
326
357
|
const confirmSuspendLoading = ref(false)
|
|
@@ -338,21 +369,6 @@ const activeMenuItem = computed(() =>
|
|
|
338
369
|
)
|
|
339
370
|
|
|
340
371
|
/* ── HELPERS ── */
|
|
341
|
-
function formatDate(date?: string | Date) {
|
|
342
|
-
if (date == null || date === '') return '-'
|
|
343
|
-
const d = date instanceof Date ? date : new Date(date as string)
|
|
344
|
-
return isNaN(d.getTime()) ? '-' : d.toLocaleDateString('en-US')
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function planTypeLabel(sub: Record<string, any>) {
|
|
348
|
-
if (!sub) return '-'
|
|
349
|
-
if (sub.paidType) return String(sub.paidType)
|
|
350
|
-
if (sub.type === 'organization') return 'Organization'
|
|
351
|
-
if (sub.type === 'affiliate') return 'Affiliate'
|
|
352
|
-
const desc = String(sub.description || '').trim()
|
|
353
|
-
return desc ? (desc.length > 48 ? `${desc.slice(0, 45)}…` : desc) : '-'
|
|
354
|
-
}
|
|
355
|
-
|
|
356
372
|
async function resolveRoleName(id: string) {
|
|
357
373
|
if (!id) return '-'
|
|
358
374
|
if (roleCache.value[id]) return roleCache.value[id]
|
|
@@ -387,6 +403,30 @@ async function activateClient(item: any) {
|
|
|
387
403
|
}
|
|
388
404
|
}
|
|
389
405
|
|
|
406
|
+
/* ── SUBSCRIPTION ── */
|
|
407
|
+
function openSubscription(item: any) {
|
|
408
|
+
if (!item?._id) return
|
|
409
|
+
savedNote.value = ''
|
|
410
|
+
subscriptionClient.value = item
|
|
411
|
+
closeMenu()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function onSubscriptionSaved(message: string) {
|
|
415
|
+
subscriptionClient.value = null
|
|
416
|
+
savedNote.value = message
|
|
417
|
+
// The list derives every subscription state from the record itself, so
|
|
418
|
+
// re-reading it is all that is needed for the row to tell the truth again.
|
|
419
|
+
await fetchData()
|
|
420
|
+
|
|
421
|
+
// The drawer reads the same row, so if it is open it has to be handed the
|
|
422
|
+
// refreshed one - otherwise it would still be showing what was true before
|
|
423
|
+
// the save.
|
|
424
|
+
if (selectedClient.value?._id) {
|
|
425
|
+
selectedClient.value =
|
|
426
|
+
items.value.find(i => i._id === selectedClient.value._id) ?? selectedClient.value
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
390
430
|
/* ── SUSPEND CONFIRMATION ── */
|
|
391
431
|
function askSuspend(item: any) {
|
|
392
432
|
confirmSuspendItem.value = item
|
|
@@ -418,7 +458,6 @@ async function fetchOrgs(status: 'active' | 'suspended') {
|
|
|
418
458
|
search: search.value,
|
|
419
459
|
limit: limit.value,
|
|
420
460
|
status,
|
|
421
|
-
type: filterPlan.value,
|
|
422
461
|
billingCycle: filterBilling.value,
|
|
423
462
|
})
|
|
424
463
|
|
|
@@ -428,8 +467,6 @@ async function fetchOrgs(status: 'active' | 'suspended') {
|
|
|
428
467
|
|
|
429
468
|
items.value = await Promise.all(
|
|
430
469
|
orgs.map(async (org: any) => {
|
|
431
|
-
const sub = org?.subscription ?? {}
|
|
432
|
-
|
|
433
470
|
let siteCount = siteCache[org._id]
|
|
434
471
|
|
|
435
472
|
if (siteCount === undefined) {
|
|
@@ -444,18 +481,14 @@ async function fetchOrgs(status: 'active' | 'suspended') {
|
|
|
444
481
|
}
|
|
445
482
|
|
|
446
483
|
return {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
planType: planTypeLabel(sub),
|
|
484
|
+
...org,
|
|
450
485
|
|
|
451
|
-
|
|
486
|
+
sub: describeClientSubscription(org),
|
|
452
487
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
sites: `${siteCount} Sites`,
|
|
458
|
-
}
|
|
488
|
+
// The column header already says Sites, so the badge is the number.
|
|
489
|
+
// (It also used to read "1 Sites".)
|
|
490
|
+
sites: siteCount,
|
|
491
|
+
}
|
|
459
492
|
})
|
|
460
493
|
)
|
|
461
494
|
|
|
@@ -869,6 +902,50 @@ tbody tr:hover { background: var(--thead); }
|
|
|
869
902
|
tbody td { vertical-align: middle; }
|
|
870
903
|
|
|
871
904
|
.empty { text-align: center; color: var(--muted); padding: 48px 0 !important; }
|
|
905
|
+
|
|
906
|
+
/* A client nobody has set up yet: plainly stated, not shouted, not a dash. */
|
|
907
|
+
.sub-none { color: var(--text2); font-size: 13px; }
|
|
908
|
+
/* The way in, said next to the state it fixes. It sits in a cell that already
|
|
909
|
+
spans four columns, so it adds no width to the table. */
|
|
910
|
+
.sub-setup-btn {
|
|
911
|
+
margin-left: 10px;
|
|
912
|
+
padding: 3px 10px;
|
|
913
|
+
font-size: 12px;
|
|
914
|
+
font-weight: 600;
|
|
915
|
+
border: 1px solid var(--border);
|
|
916
|
+
border-radius: 12px;
|
|
917
|
+
background: var(--card);
|
|
918
|
+
color: var(--accent-text);
|
|
919
|
+
cursor: pointer;
|
|
920
|
+
white-space: nowrap;
|
|
921
|
+
}
|
|
922
|
+
.sub-setup-btn:hover { background: var(--hover); }
|
|
923
|
+
|
|
924
|
+
.saved-note {
|
|
925
|
+
display: flex;
|
|
926
|
+
align-items: center;
|
|
927
|
+
justify-content: space-between;
|
|
928
|
+
gap: 10px;
|
|
929
|
+
margin: 12px 16px 0;
|
|
930
|
+
padding: 9px 12px;
|
|
931
|
+
border: 1px solid var(--border);
|
|
932
|
+
border-radius: 8px;
|
|
933
|
+
background: var(--ok-bg);
|
|
934
|
+
color: var(--text);
|
|
935
|
+
font-size: 13px;
|
|
936
|
+
font-weight: 600;
|
|
937
|
+
}
|
|
938
|
+
.saved-note-close {
|
|
939
|
+
border: none;
|
|
940
|
+
background: none;
|
|
941
|
+
color: var(--text2);
|
|
942
|
+
font-size: 16px;
|
|
943
|
+
line-height: 1;
|
|
944
|
+
cursor: pointer;
|
|
945
|
+
}
|
|
946
|
+
.sub-label { font-size: 13px; }
|
|
947
|
+
/* Owner decision 8: an end date has passed and somebody has to look at it. */
|
|
948
|
+
.needs-attention { color: var(--warn); font-weight: 500; }
|
|
872
949
|
.td-name { font-weight: 500; color: var(--text); }
|
|
873
950
|
.td-muted { color: var(--muted); font-size: 13px; }
|
|
874
951
|
|