@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.
@@ -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
- <h3>Subscription Information</h3>
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 = ''">&times;</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>Email</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
- <th>Status</th>
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="9" class="empty">No records found.</td>
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
- <td class="td-name">{{ item.name }}</td>
115
- <td class="td-muted">{{ item.email }}</td>
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 {