@7365admin1/layer-common 3.2.8-staging.203 → 3.2.8-staging.205
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 +35 -1
- package/components/ClientMain.vue +138 -8
- package/components/ClientSubscriptionForm.vue +527 -0
- package/composables/useSubscription.ts +40 -0
- package/package.json +1 -1
- package/utils/client-nature.test.ts +77 -0
- package/utils/subscription-form.test.ts +284 -0
- package/utils/subscription-form.ts +265 -0
|
@@ -79,7 +79,17 @@
|
|
|
79
79
|
a separate piece of work with a real endpoint behind it.
|
|
80
80
|
-->
|
|
81
81
|
<section class="section">
|
|
82
|
-
<
|
|
82
|
+
<div class="section-head">
|
|
83
|
+
<h3>Subscription Information</h3>
|
|
84
|
+
<!--
|
|
85
|
+
The values stay read-only - they are what the client's record says.
|
|
86
|
+
Changing them is a real form with real rules behind it, so this opens
|
|
87
|
+
that rather than pretending a dropdown here would save anything.
|
|
88
|
+
-->
|
|
89
|
+
<button class="btn-manage-sub" @click="$emit('manage-subscription')">
|
|
90
|
+
{{ subscription.state === 'none' ? 'Set up subscription' : 'Edit subscription' }}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
83
93
|
|
|
84
94
|
<p v-if="subscription.state === 'none'" class="sub-none">
|
|
85
95
|
No subscription set up for this client yet.
|
|
@@ -234,6 +244,7 @@ const props = defineProps<{
|
|
|
234
244
|
const emit = defineEmits<{
|
|
235
245
|
(e: 'cancel'): void
|
|
236
246
|
(e: 'submit', payload: Record<string, any>): void
|
|
247
|
+
(e: 'manage-subscription'): void
|
|
237
248
|
}>()
|
|
238
249
|
|
|
239
250
|
/* ── CONSTANTS ── */
|
|
@@ -490,6 +501,29 @@ watch(
|
|
|
490
501
|
.section {
|
|
491
502
|
margin-bottom: 26px;
|
|
492
503
|
}
|
|
504
|
+
.section-head {
|
|
505
|
+
display: flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
justify-content: space-between;
|
|
508
|
+
gap: 12px;
|
|
509
|
+
flex-wrap: wrap;
|
|
510
|
+
}
|
|
511
|
+
.section-head h3 {
|
|
512
|
+
margin-bottom: 0;
|
|
513
|
+
}
|
|
514
|
+
.btn-manage-sub {
|
|
515
|
+
padding: 6px 14px;
|
|
516
|
+
font-size: 12px;
|
|
517
|
+
font-weight: 600;
|
|
518
|
+
border: 1px solid var(--border);
|
|
519
|
+
border-radius: var(--r-pill);
|
|
520
|
+
background: var(--card);
|
|
521
|
+
color: var(--accent-text);
|
|
522
|
+
cursor: pointer;
|
|
523
|
+
white-space: nowrap;
|
|
524
|
+
margin-bottom: 14px;
|
|
525
|
+
}
|
|
526
|
+
.btn-manage-sub:hover { background: var(--hover); }
|
|
493
527
|
.section h3 {
|
|
494
528
|
font-size: 15px;
|
|
495
529
|
font-weight: 700;
|
|
@@ -56,6 +56,11 @@
|
|
|
56
56
|
<div class="table-card">
|
|
57
57
|
|
|
58
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
|
+
|
|
59
64
|
<div class="toolbar">
|
|
60
65
|
<div class="toolbar-left">
|
|
61
66
|
<button class="refresh-btn" :class="{ spinning: loading }" @click="handleRefresh">
|
|
@@ -96,23 +101,48 @@
|
|
|
96
101
|
<thead>
|
|
97
102
|
<tr>
|
|
98
103
|
<th>Organization Name</th>
|
|
99
|
-
<th>
|
|
104
|
+
<th>Nature</th>
|
|
100
105
|
<th>Sites</th>
|
|
101
106
|
<th>Subscription</th>
|
|
102
107
|
<th>Billing Cycle</th>
|
|
103
108
|
<th>Start Date</th>
|
|
104
109
|
<th>End Date</th>
|
|
105
|
-
|
|
110
|
+
<!--
|
|
111
|
+
NO STATUS COLUMN. The table is fetched with the status the
|
|
112
|
+
chosen tab asks for (`fetchOrgs(tab)` -> `status` -> the
|
|
113
|
+
aggregation's `$match`), so on the Active tab every row said
|
|
114
|
+
"active" and on Suspended every row said "suspended" - a
|
|
115
|
+
column that repeated the tab the reader was standing on and
|
|
116
|
+
could never say anything else. Its 138px is what pays for the
|
|
117
|
+
Nature column, and the table now fits its viewport instead of
|
|
118
|
+
scrolling sideways.
|
|
119
|
+
-->
|
|
106
120
|
<th>Actions</th>
|
|
107
121
|
</tr>
|
|
108
122
|
</thead>
|
|
109
123
|
<tbody>
|
|
110
124
|
<tr v-if="!loading && items.length === 0">
|
|
111
|
-
<td colspan="
|
|
125
|
+
<td colspan="8" class="empty">No records found.</td>
|
|
112
126
|
</tr>
|
|
113
127
|
<tr v-for="item in items" :key="item._id">
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
<!--
|
|
129
|
+
WHO THEY ARE, then WHAT KIND OF BUSINESS. The email used to
|
|
130
|
+
hold a column of its own; it identifies the same client the
|
|
131
|
+
name does, so it sits under the name and the column it was
|
|
132
|
+
using goes to the nature - which nothing on this screen said
|
|
133
|
+
before, and which is the one thing the admin app's
|
|
134
|
+
Organizations list showed that this one did not.
|
|
135
|
+
-->
|
|
136
|
+
<td class="td-name">
|
|
137
|
+
{{ item.name }}
|
|
138
|
+
<span class="td-name-sub">{{ item.email }}</span>
|
|
139
|
+
</td>
|
|
140
|
+
<!-- A blank cell reads as a broken screen, so the one case
|
|
141
|
+
with nothing to print says so. `--text2`, not `--muted`:
|
|
142
|
+
this is text to be read, and `--muted` measures 3.24:1. -->
|
|
143
|
+
<td :class="{ 'td-nature-none': !item.nature }">
|
|
144
|
+
{{ item.nature ? formatNature(item.nature) : 'Not recorded' }}
|
|
145
|
+
</td>
|
|
116
146
|
<td>
|
|
117
147
|
<span class="sites-badge">{{ item.sites }}</span>
|
|
118
148
|
</td>
|
|
@@ -123,6 +153,9 @@
|
|
|
123
153
|
-->
|
|
124
154
|
<td v-if="item.sub.state === 'none'" colspan="4" class="sub-none">
|
|
125
155
|
No subscription set up
|
|
156
|
+
<button class="sub-setup-btn" @click="openSubscription(item)">
|
|
157
|
+
Set up subscription
|
|
158
|
+
</button>
|
|
126
159
|
</td>
|
|
127
160
|
|
|
128
161
|
<template v-else>
|
|
@@ -153,9 +186,6 @@
|
|
|
153
186
|
</span>
|
|
154
187
|
</td>
|
|
155
188
|
</template>
|
|
156
|
-
<td>
|
|
157
|
-
<span :class="['status-chip', item.status]">{{ item.status }}</span>
|
|
158
|
-
</td>
|
|
159
189
|
<td>
|
|
160
190
|
<div class="action-wrap">
|
|
161
191
|
<button class="dots-btn" @click.stop="toggleMenu(item._id, $event)">
|
|
@@ -212,9 +242,24 @@
|
|
|
212
242
|
:client="selectedClient"
|
|
213
243
|
@cancel="cancelView"
|
|
214
244
|
@submit="submitClient"
|
|
245
|
+
@manage-subscription="openSubscription(selectedClient)"
|
|
215
246
|
/>
|
|
216
247
|
</div>
|
|
217
248
|
|
|
249
|
+
<!--
|
|
250
|
+
SET UP OR CHANGE A CLIENT'S SUBSCRIPTION. Opened from the row that has
|
|
251
|
+
none, from the row menu, or from the client's own drawer - all three land
|
|
252
|
+
here, so there is one form and one set of rules behind them.
|
|
253
|
+
-->
|
|
254
|
+
<ClientSubscriptionForm
|
|
255
|
+
v-if="subscriptionClient"
|
|
256
|
+
:org-id="subscriptionClient._id"
|
|
257
|
+
:client-name="subscriptionClient.name"
|
|
258
|
+
:site-count="subscriptionClient.sites"
|
|
259
|
+
@cancel="subscriptionClient = null"
|
|
260
|
+
@saved="onSubscriptionSaved"
|
|
261
|
+
/>
|
|
262
|
+
|
|
218
263
|
<div v-if="dialog" class="overlay" @click.self="dialog = false">
|
|
219
264
|
<div class="dialog-box">
|
|
220
265
|
<InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
|
|
@@ -247,6 +292,9 @@
|
|
|
247
292
|
<button class="dd-item" @click="viewClient(activeMenuItem)">
|
|
248
293
|
View Organization
|
|
249
294
|
</button>
|
|
295
|
+
<button class="dd-item" @click="openSubscription(activeMenuItem)">
|
|
296
|
+
{{ activeMenuItem?.sub?.state === 'none' ? 'Set up subscription' : 'Edit subscription' }}
|
|
297
|
+
</button>
|
|
250
298
|
<button
|
|
251
299
|
v-if="activeMenuItem?.status === 'suspended'"
|
|
252
300
|
class="dd-item"
|
|
@@ -271,8 +319,10 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
271
319
|
import useOrg from '../composables/useOrg'
|
|
272
320
|
import useVerification from '../composables/useVerification'
|
|
273
321
|
import useRole from '../composables/useRole'
|
|
322
|
+
import useUtils from '../composables/useUtils'
|
|
274
323
|
import InvitationClientForm from './InvitationClientForm.vue'
|
|
275
324
|
import ClientDetailForm from './ClientDetailForm.vue'
|
|
325
|
+
import ClientSubscriptionForm from './ClientSubscriptionForm.vue'
|
|
276
326
|
import useCustomerSite from '../composables/useCustomerSite'
|
|
277
327
|
import { describeClientSubscription } from '../utils/client-subscription'
|
|
278
328
|
|
|
@@ -285,6 +335,9 @@ const { getOrganizationsWithSubscription, updateStatus } = useOrg()
|
|
|
285
335
|
const { getVerifications, cancelUserInvitation } = useVerification()
|
|
286
336
|
const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
|
|
287
337
|
const { getRoleById } = useRole()
|
|
338
|
+
// The same helper the admin app's Organizations list uses to print a nature,
|
|
339
|
+
// so both screens say the value the same way while both exist.
|
|
340
|
+
const { formatNature } = useUtils()
|
|
288
341
|
|
|
289
342
|
/* ── CONSTANTS ── */
|
|
290
343
|
const APP_LIST = [
|
|
@@ -321,6 +374,10 @@ const roleCache = ref<Record<string, string>>({})
|
|
|
321
374
|
const viewMode = ref<'table' | 'detail'>('table')
|
|
322
375
|
const selectedClient = ref<any>(null)
|
|
323
376
|
|
|
377
|
+
/* subscription set-up / edit */
|
|
378
|
+
const subscriptionClient = ref<any>(null)
|
|
379
|
+
const savedNote = ref('')
|
|
380
|
+
|
|
324
381
|
/* suspend confirmation */
|
|
325
382
|
const confirmSuspendItem = ref<any>(null)
|
|
326
383
|
const confirmSuspendLoading = ref(false)
|
|
@@ -372,6 +429,30 @@ async function activateClient(item: any) {
|
|
|
372
429
|
}
|
|
373
430
|
}
|
|
374
431
|
|
|
432
|
+
/* ── SUBSCRIPTION ── */
|
|
433
|
+
function openSubscription(item: any) {
|
|
434
|
+
if (!item?._id) return
|
|
435
|
+
savedNote.value = ''
|
|
436
|
+
subscriptionClient.value = item
|
|
437
|
+
closeMenu()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function onSubscriptionSaved(message: string) {
|
|
441
|
+
subscriptionClient.value = null
|
|
442
|
+
savedNote.value = message
|
|
443
|
+
// The list derives every subscription state from the record itself, so
|
|
444
|
+
// re-reading it is all that is needed for the row to tell the truth again.
|
|
445
|
+
await fetchData()
|
|
446
|
+
|
|
447
|
+
// The drawer reads the same row, so if it is open it has to be handed the
|
|
448
|
+
// refreshed one - otherwise it would still be showing what was true before
|
|
449
|
+
// the save.
|
|
450
|
+
if (selectedClient.value?._id) {
|
|
451
|
+
selectedClient.value =
|
|
452
|
+
items.value.find(i => i._id === selectedClient.value._id) ?? selectedClient.value
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
375
456
|
/* ── SUSPEND CONFIRMATION ── */
|
|
376
457
|
function askSuspend(item: any) {
|
|
377
458
|
confirmSuspendItem.value = item
|
|
@@ -850,10 +931,59 @@ tbody td { vertical-align: middle; }
|
|
|
850
931
|
|
|
851
932
|
/* A client nobody has set up yet: plainly stated, not shouted, not a dash. */
|
|
852
933
|
.sub-none { color: var(--text2); font-size: 13px; }
|
|
934
|
+
/* The way in, said next to the state it fixes. It sits in a cell that already
|
|
935
|
+
spans four columns, so it adds no width to the table. */
|
|
936
|
+
.sub-setup-btn {
|
|
937
|
+
margin-left: 10px;
|
|
938
|
+
padding: 3px 10px;
|
|
939
|
+
font-size: 12px;
|
|
940
|
+
font-weight: 600;
|
|
941
|
+
border: 1px solid var(--border);
|
|
942
|
+
border-radius: 12px;
|
|
943
|
+
background: var(--card);
|
|
944
|
+
color: var(--accent-text);
|
|
945
|
+
cursor: pointer;
|
|
946
|
+
white-space: nowrap;
|
|
947
|
+
}
|
|
948
|
+
.sub-setup-btn:hover { background: var(--hover); }
|
|
949
|
+
|
|
950
|
+
.saved-note {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: space-between;
|
|
954
|
+
gap: 10px;
|
|
955
|
+
margin: 12px 16px 0;
|
|
956
|
+
padding: 9px 12px;
|
|
957
|
+
border: 1px solid var(--border);
|
|
958
|
+
border-radius: 8px;
|
|
959
|
+
background: var(--ok-bg);
|
|
960
|
+
color: var(--text);
|
|
961
|
+
font-size: 13px;
|
|
962
|
+
font-weight: 600;
|
|
963
|
+
}
|
|
964
|
+
.saved-note-close {
|
|
965
|
+
border: none;
|
|
966
|
+
background: none;
|
|
967
|
+
color: var(--text2);
|
|
968
|
+
font-size: 16px;
|
|
969
|
+
line-height: 1;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
}
|
|
853
972
|
.sub-label { font-size: 13px; }
|
|
854
973
|
/* Owner decision 8: an end date has passed and somebody has to look at it. */
|
|
855
974
|
.needs-attention { color: var(--warn); font-weight: 500; }
|
|
856
975
|
.td-name { font-weight: 500; color: var(--text); }
|
|
976
|
+
/* The email, second line of the same cell. `--text2` is the design's reading
|
|
977
|
+
token for secondary text; `--muted`, which the email used while it had its
|
|
978
|
+
own column, is a decoration weight and measures below AA as body copy. */
|
|
979
|
+
.td-nature-none { color: var(--text2); }
|
|
980
|
+
.td-name-sub {
|
|
981
|
+
display: block;
|
|
982
|
+
margin-top: 2px;
|
|
983
|
+
font-size: 12px;
|
|
984
|
+
font-weight: 400;
|
|
985
|
+
color: var(--text2);
|
|
986
|
+
}
|
|
857
987
|
.td-muted { color: var(--muted); font-size: 13px; }
|
|
858
988
|
|
|
859
989
|
.sites-badge {
|