@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
|
@@ -70,177 +70,64 @@
|
|
|
70
70
|
</section>
|
|
71
71
|
|
|
72
72
|
<!-- ===== SUBSCRIPTION INFORMATION ===== -->
|
|
73
|
+
<!--
|
|
74
|
+
READ-ONLY on purpose. This section used to offer six dropdowns and two
|
|
75
|
+
date boxes that looked editable and saved nothing: the plan list was two
|
|
76
|
+
invented names, and Start / End were always blank because nothing ever
|
|
77
|
+
loaded them. It now shows what the client's record actually says, using
|
|
78
|
+
the same wording as the Client List. Setting or changing a subscription is
|
|
79
|
+
a separate piece of work with a real endpoint behind it.
|
|
80
|
+
-->
|
|
73
81
|
<section class="section">
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
</select>
|
|
85
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
86
|
-
<path
|
|
87
|
-
d="M1 1l4 4 4-4"
|
|
88
|
-
stroke="currentColor"
|
|
89
|
-
stroke-width="1.4"
|
|
90
|
-
stroke-linecap="round"
|
|
91
|
-
/>
|
|
92
|
-
</svg>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
<div class="field">
|
|
96
|
-
<label>Auto Renewal</label>
|
|
97
|
-
<div class="select-wrap">
|
|
98
|
-
<select v-model="form.autoRenewal">
|
|
99
|
-
<option
|
|
100
|
-
v-for="opt in AUTO_RENEWAL_OPTIONS"
|
|
101
|
-
:key="opt"
|
|
102
|
-
:value="opt"
|
|
103
|
-
>
|
|
104
|
-
{{ opt }}
|
|
105
|
-
</option>
|
|
106
|
-
</select>
|
|
107
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
108
|
-
<path
|
|
109
|
-
d="M1 1l4 4 4-4"
|
|
110
|
-
stroke="currentColor"
|
|
111
|
-
stroke-width="1.4"
|
|
112
|
-
stroke-linecap="round"
|
|
113
|
-
/>
|
|
114
|
-
</svg>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
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>
|
|
117
92
|
</div>
|
|
118
93
|
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<path
|
|
134
|
-
d="M1 1l4 4 4-4"
|
|
135
|
-
stroke="currentColor"
|
|
136
|
-
stroke-width="1.4"
|
|
137
|
-
stroke-linecap="round"
|
|
138
|
-
/>
|
|
139
|
-
</svg>
|
|
94
|
+
<p v-if="subscription.state === 'none'" class="sub-none">
|
|
95
|
+
No subscription set up for this client yet.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<template v-else>
|
|
99
|
+
<div class="row">
|
|
100
|
+
<div class="field">
|
|
101
|
+
<label>Subscription</label>
|
|
102
|
+
<p
|
|
103
|
+
class="read-value"
|
|
104
|
+
:class="{ 'needs-attention': subscription.needsAttention }"
|
|
105
|
+
>
|
|
106
|
+
{{ subscription.label }}
|
|
107
|
+
</p>
|
|
140
108
|
</div>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<div class="select-wrap">
|
|
145
|
-
<select v-model="form.status">
|
|
146
|
-
<option v-for="opt in STATUS_OPTIONS" :key="opt" :value="opt">
|
|
147
|
-
{{ opt }}
|
|
148
|
-
</option>
|
|
149
|
-
</select>
|
|
150
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
151
|
-
<path
|
|
152
|
-
d="M1 1l4 4 4-4"
|
|
153
|
-
stroke="currentColor"
|
|
154
|
-
stroke-width="1.4"
|
|
155
|
-
stroke-linecap="round"
|
|
156
|
-
/>
|
|
157
|
-
</svg>
|
|
109
|
+
<div class="field">
|
|
110
|
+
<label>Billing Cycle</label>
|
|
111
|
+
<p class="read-value">{{ subscription.billingCycle }}</p>
|
|
158
112
|
</div>
|
|
159
113
|
</div>
|
|
160
|
-
</div>
|
|
161
114
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<select v-model="form.subscriptionPlan">
|
|
167
|
-
<option
|
|
168
|
-
v-for="opt in subscriptionPlanOptions"
|
|
169
|
-
:key="opt"
|
|
170
|
-
:value="opt"
|
|
171
|
-
>
|
|
172
|
-
{{ opt }}
|
|
173
|
-
</option>
|
|
174
|
-
</select>
|
|
175
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
176
|
-
<path
|
|
177
|
-
d="M1 1l4 4 4-4"
|
|
178
|
-
stroke="currentColor"
|
|
179
|
-
stroke-width="1.4"
|
|
180
|
-
stroke-linecap="round"
|
|
181
|
-
/>
|
|
182
|
-
</svg>
|
|
115
|
+
<div class="row">
|
|
116
|
+
<div class="field">
|
|
117
|
+
<label>Start Date</label>
|
|
118
|
+
<p class="read-value">{{ subscription.start }}</p>
|
|
183
119
|
</div>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<div class="field">
|
|
193
|
-
<label>Start Date</label>
|
|
194
|
-
<div class="date-input">
|
|
195
|
-
<input type="text" :value="form.startDate" disabled />
|
|
196
|
-
<svg viewBox="0 0 16 16" fill="none" width="14" height="14">
|
|
197
|
-
<rect
|
|
198
|
-
x="1"
|
|
199
|
-
y="3"
|
|
200
|
-
width="14"
|
|
201
|
-
height="12"
|
|
202
|
-
rx="2"
|
|
203
|
-
stroke="currentColor"
|
|
204
|
-
stroke-width="1.3"
|
|
205
|
-
/>
|
|
206
|
-
<path d="M1 7h14" stroke="currentColor" stroke-width="1.3" />
|
|
207
|
-
<path
|
|
208
|
-
d="M5 1v3M11 1v3"
|
|
209
|
-
stroke="currentColor"
|
|
210
|
-
stroke-width="1.3"
|
|
211
|
-
stroke-linecap="round"
|
|
212
|
-
/>
|
|
213
|
-
</svg>
|
|
120
|
+
<div class="field">
|
|
121
|
+
<label>End Date</label>
|
|
122
|
+
<p
|
|
123
|
+
class="read-value"
|
|
124
|
+
:class="{ 'needs-attention': subscription.needsAttention }"
|
|
125
|
+
>
|
|
126
|
+
{{ subscription.end }}
|
|
127
|
+
</p>
|
|
214
128
|
</div>
|
|
215
129
|
</div>
|
|
216
|
-
|
|
217
|
-
<label>End Date</label>
|
|
218
|
-
<div class="date-input">
|
|
219
|
-
<input type="text" :value="form.endDate" disabled />
|
|
220
|
-
<svg viewBox="0 0 16 16" fill="none" width="14" height="14">
|
|
221
|
-
<rect
|
|
222
|
-
x="1"
|
|
223
|
-
y="3"
|
|
224
|
-
width="14"
|
|
225
|
-
height="12"
|
|
226
|
-
rx="2"
|
|
227
|
-
stroke="currentColor"
|
|
228
|
-
stroke-width="1.3"
|
|
229
|
-
/>
|
|
230
|
-
<path d="M1 7h14" stroke="currentColor" stroke-width="1.3" />
|
|
231
|
-
<path
|
|
232
|
-
d="M5 1v3M11 1v3"
|
|
233
|
-
stroke="currentColor"
|
|
234
|
-
stroke-width="1.3"
|
|
235
|
-
stroke-linecap="round"
|
|
236
|
-
/>
|
|
237
|
-
</svg>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
<p class="hint hint-below">
|
|
242
|
-
Start and end date depend on the billing cycle, and cannot be edited.
|
|
243
|
-
</p>
|
|
130
|
+
</template>
|
|
244
131
|
</section>
|
|
245
132
|
|
|
246
133
|
<!-- ===== INCLUDED APPLICATION ===== -->
|
|
@@ -281,6 +168,7 @@
|
|
|
281
168
|
import { reactive, computed, watch } from 'vue'
|
|
282
169
|
import useOrg from '../composables/useOrg'
|
|
283
170
|
import useCustomerSite from '../composables/useCustomerSite'
|
|
171
|
+
import { describeClientSubscription } from '../utils/client-subscription'
|
|
284
172
|
import useUser from '../composables/useUser'
|
|
285
173
|
|
|
286
174
|
const {
|
|
@@ -356,24 +244,10 @@ const props = defineProps<{
|
|
|
356
244
|
const emit = defineEmits<{
|
|
357
245
|
(e: 'cancel'): void
|
|
358
246
|
(e: 'submit', payload: Record<string, any>): void
|
|
247
|
+
(e: 'manage-subscription'): void
|
|
359
248
|
}>()
|
|
360
249
|
|
|
361
250
|
/* ── CONSTANTS ── */
|
|
362
|
-
const PLAN_TYPE_OPTIONS = [
|
|
363
|
-
'Free (Bundle)',
|
|
364
|
-
'Paid (Bundle)',
|
|
365
|
-
'Paid (Custom App)'
|
|
366
|
-
]
|
|
367
|
-
const AUTO_RENEWAL_OPTIONS = ['No', 'Yes']
|
|
368
|
-
const BILLING_CYCLE_OPTIONS = ['Monthly', 'Annually']
|
|
369
|
-
const STATUS_OPTIONS = ['Active', 'Suspend']
|
|
370
|
-
|
|
371
|
-
// Subscription plan options keyed by billing cycle (annotation from mockup)
|
|
372
|
-
const SUBSCRIPTION_PLAN_BY_CYCLE: Record<string, string[]> = {
|
|
373
|
-
Monthly: ['Standard Plan', 'Premium Plan'],
|
|
374
|
-
Annually: ['Standard Plan', 'Premium Plan']
|
|
375
|
-
}
|
|
376
|
-
|
|
377
251
|
const PHONE_RULES: Record<string, number> = {
|
|
378
252
|
'+60': 9,
|
|
379
253
|
'+61': 9,
|
|
@@ -399,31 +273,15 @@ const form = reactive({
|
|
|
399
273
|
organizationName: '',
|
|
400
274
|
phone: '',
|
|
401
275
|
|
|
402
|
-
planType: 'Paid (Bundle)',
|
|
403
|
-
autoRenewal: 'Yes',
|
|
404
|
-
billingCycle: 'Monthly',
|
|
405
|
-
status: 'Active',
|
|
406
|
-
subscriptionPlan: 'Standard Plan',
|
|
407
|
-
|
|
408
|
-
startDate: '',
|
|
409
|
-
endDate: '',
|
|
410
|
-
|
|
411
276
|
includedApps: [] as string[],
|
|
412
277
|
|
|
413
278
|
sites: [] as SiteRow[]
|
|
414
279
|
})
|
|
415
280
|
|
|
416
281
|
/* ── COMPUTED ── */
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
/* ── WATCH: keep subscription plan valid when billing cycle changes ── */
|
|
422
|
-
watch(subscriptionPlanOptions, options => {
|
|
423
|
-
if (!options.includes(form.subscriptionPlan)) {
|
|
424
|
-
form.subscriptionPlan = options[0] ?? ''
|
|
425
|
-
}
|
|
426
|
-
})
|
|
282
|
+
// The row the Client List already fetched carries the subscription sub-document,
|
|
283
|
+
// so the drawer says exactly what the list says - no second, disagreeing source.
|
|
284
|
+
const subscription = computed(() => describeClientSubscription(props.client))
|
|
427
285
|
|
|
428
286
|
watch(
|
|
429
287
|
() => form.name,
|
|
@@ -454,10 +312,6 @@ watch([phonePrefix, phoneNumber], ([prefix, value]) => {
|
|
|
454
312
|
})
|
|
455
313
|
|
|
456
314
|
/* ── EVENTS ── */
|
|
457
|
-
function onBillingCycleChange() {
|
|
458
|
-
// Start / End date are derived from billing cycle and are not editable.
|
|
459
|
-
// Recompute or refetch here as needed once wired to a real API.
|
|
460
|
-
}
|
|
461
315
|
function validateForm() {
|
|
462
316
|
errors.name = ''
|
|
463
317
|
errors.organizationName = ''
|
|
@@ -647,6 +501,29 @@ watch(
|
|
|
647
501
|
.section {
|
|
648
502
|
margin-bottom: 26px;
|
|
649
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); }
|
|
650
527
|
.section h3 {
|
|
651
528
|
font-size: 15px;
|
|
652
529
|
font-weight: 700;
|
|
@@ -660,10 +537,6 @@ watch(
|
|
|
660
537
|
gap: 20px;
|
|
661
538
|
margin-bottom: 14px;
|
|
662
539
|
}
|
|
663
|
-
.row-single {
|
|
664
|
-
grid-template-columns: 1fr;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
540
|
.field {
|
|
668
541
|
display: flex;
|
|
669
542
|
flex-direction: column;
|
|
@@ -697,68 +570,17 @@ watch(
|
|
|
697
570
|
color: var(--text2);
|
|
698
571
|
}
|
|
699
572
|
|
|
700
|
-
/* =====
|
|
701
|
-
.
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
.select-wrap select {
|
|
705
|
-
width: 100%;
|
|
706
|
-
height: 38px;
|
|
707
|
-
border: 1px solid var(--border);
|
|
708
|
-
border-radius: var(--r-inner);
|
|
709
|
-
padding: 0 30px 0 12px;
|
|
710
|
-
font-size: 13px;
|
|
573
|
+
/* ===== READ-ONLY VALUES ===== */
|
|
574
|
+
/* A value the staff member reads rather than edits. */
|
|
575
|
+
.read-value {
|
|
576
|
+
font-size: 14px;
|
|
711
577
|
color: var(--text);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
cursor: pointer;
|
|
715
|
-
outline: none;
|
|
716
|
-
}
|
|
717
|
-
.select-wrap select:focus {
|
|
718
|
-
border-color: var(--muted);
|
|
719
|
-
}
|
|
720
|
-
.select-wrap .chevron {
|
|
721
|
-
position: absolute;
|
|
722
|
-
right: 11px;
|
|
723
|
-
top: 50%;
|
|
724
|
-
transform: translateY(-50%);
|
|
725
|
-
width: 10px;
|
|
726
|
-
height: 6px;
|
|
727
|
-
pointer-events: none;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/* ===== DATE ===== */
|
|
731
|
-
.date-input {
|
|
732
|
-
position: relative;
|
|
733
|
-
display: flex;
|
|
734
|
-
align-items: center;
|
|
735
|
-
}
|
|
736
|
-
.date-input input {
|
|
737
|
-
width: 100%;
|
|
738
|
-
height: 38px;
|
|
739
|
-
border: 1px solid var(--border);
|
|
740
|
-
border-radius: var(--r-inner);
|
|
741
|
-
padding: 0 34px 0 12px;
|
|
742
|
-
font-size: 13px;
|
|
743
|
-
color: var(--text2);
|
|
744
|
-
background: var(--bg);
|
|
745
|
-
outline: none;
|
|
746
|
-
}
|
|
747
|
-
.date-input svg {
|
|
748
|
-
position: absolute;
|
|
749
|
-
right: 12px;
|
|
750
|
-
pointer-events: none;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/* ===== HINT ===== */
|
|
754
|
-
.hint {
|
|
755
|
-
font-size: 11px;
|
|
756
|
-
color: var(--text2);
|
|
757
|
-
line-height: 1.4;
|
|
758
|
-
}
|
|
759
|
-
.hint-below {
|
|
760
|
-
margin-top: 4px;
|
|
578
|
+
padding: 10px 0;
|
|
579
|
+
min-height: 20px;
|
|
761
580
|
}
|
|
581
|
+
.sub-none { font-size: 14px; color: var(--text2); }
|
|
582
|
+
/* Owner decision 8: an end date has passed and somebody has to look at it. */
|
|
583
|
+
.needs-attention { color: var(--warn); font-weight: 500; }
|
|
762
584
|
|
|
763
585
|
/* ===== INCLUDED APPS ===== */
|
|
764
586
|
.app-list {
|