@7365admin1/layer-common 3.0.14 → 3.0.16
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/CHANGELOG.md +12 -0
- package/components/ClientDetailForm.vue +831 -0
- package/components/ClientMain.vue +273 -206
- package/components/DashboardMain.vue +981 -38
- package/components/OnlineFormFill.vue +51 -1
- package/composables/useOrg.ts +17 -1
- package/composables/usePDFDownload.ts +23 -3
- package/package.json +1 -1
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="form-wrap">
|
|
3
|
+
<!-- ===== HEADER ===== -->
|
|
4
|
+
<div class="header">
|
|
5
|
+
<div>
|
|
6
|
+
<h2>Client List</h2>
|
|
7
|
+
<p>{{ form.organizationName || client?.name }}</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="actions">
|
|
11
|
+
<button class="btn-cancel" @click="$emit('cancel')">Cancel</button>
|
|
12
|
+
<button class="btn-submit" @click="handleSubmit">Submit</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- ===== CLIENT INFORMATION ===== -->
|
|
17
|
+
<section class="section">
|
|
18
|
+
<h3>Client Information</h3>
|
|
19
|
+
|
|
20
|
+
<div class="row">
|
|
21
|
+
<div class="field">
|
|
22
|
+
<label>Name</label>
|
|
23
|
+
<input type="text" v-model="form.name" />
|
|
24
|
+
|
|
25
|
+
<span v-if="errors.name" class="error">
|
|
26
|
+
{{ errors.name }}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="field">
|
|
30
|
+
<label>Email</label>
|
|
31
|
+
<input
|
|
32
|
+
type="email"
|
|
33
|
+
v-model="form.email"
|
|
34
|
+
placeholder="seventh@email.com"
|
|
35
|
+
:disabled="true"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="row">
|
|
41
|
+
<div class="field">
|
|
42
|
+
<label>Organization Name</label>
|
|
43
|
+
<input type="text" v-model="form.organizationName" />
|
|
44
|
+
|
|
45
|
+
<span v-if="errors.organizationName" class="error">
|
|
46
|
+
{{ errors.organizationName }}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="field">
|
|
50
|
+
<label>Phone</label>
|
|
51
|
+
|
|
52
|
+
<div class="phone-input">
|
|
53
|
+
<input class="prefix" :value="phonePrefix" disabled />
|
|
54
|
+
|
|
55
|
+
<input
|
|
56
|
+
class="number"
|
|
57
|
+
v-model="phoneNumber"
|
|
58
|
+
type="text"
|
|
59
|
+
:maxlength="PHONE_RULES[phonePrefix]"
|
|
60
|
+
@input="phoneNumber = phoneNumber.replace(/\D/g, '')"
|
|
61
|
+
placeholder="97890123"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<span v-if="errors.phone" class="error">
|
|
66
|
+
{{ errors.phone }}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<!-- ===== SUBSCRIPTION INFORMATION ===== -->
|
|
73
|
+
<section class="section">
|
|
74
|
+
<h3>Subscription Information</h3>
|
|
75
|
+
|
|
76
|
+
<div class="row">
|
|
77
|
+
<div class="field">
|
|
78
|
+
<label>Subscription plan type</label>
|
|
79
|
+
<div class="select-wrap">
|
|
80
|
+
<select v-model="form.planType">
|
|
81
|
+
<option v-for="opt in PLAN_TYPE_OPTIONS" :key="opt" :value="opt">
|
|
82
|
+
{{ opt }}
|
|
83
|
+
</option>
|
|
84
|
+
</select>
|
|
85
|
+
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
86
|
+
<path
|
|
87
|
+
d="M1 1l4 4 4-4"
|
|
88
|
+
stroke="#6B7280"
|
|
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="#6B7280"
|
|
111
|
+
stroke-width="1.4"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="row">
|
|
120
|
+
<div class="field">
|
|
121
|
+
<label>Billing Cycle</label>
|
|
122
|
+
<div class="select-wrap">
|
|
123
|
+
<select v-model="form.billingCycle" @change="onBillingCycleChange">
|
|
124
|
+
<option
|
|
125
|
+
v-for="opt in BILLING_CYCLE_OPTIONS"
|
|
126
|
+
:key="opt"
|
|
127
|
+
:value="opt"
|
|
128
|
+
>
|
|
129
|
+
{{ opt }}
|
|
130
|
+
</option>
|
|
131
|
+
</select>
|
|
132
|
+
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
133
|
+
<path
|
|
134
|
+
d="M1 1l4 4 4-4"
|
|
135
|
+
stroke="#6B7280"
|
|
136
|
+
stroke-width="1.4"
|
|
137
|
+
stroke-linecap="round"
|
|
138
|
+
/>
|
|
139
|
+
</svg>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="field">
|
|
143
|
+
<label>Status</label>
|
|
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="#6B7280"
|
|
154
|
+
stroke-width="1.4"
|
|
155
|
+
stroke-linecap="round"
|
|
156
|
+
/>
|
|
157
|
+
</svg>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="row row-single">
|
|
163
|
+
<div class="field">
|
|
164
|
+
<label>Subscription plan</label>
|
|
165
|
+
<div class="select-wrap">
|
|
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="#6B7280"
|
|
179
|
+
stroke-width="1.4"
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="hint">
|
|
185
|
+
Values of dropdown are determined by the list under selected billing
|
|
186
|
+
cycle.
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="row">
|
|
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="#9CA3AF"
|
|
204
|
+
stroke-width="1.3"
|
|
205
|
+
/>
|
|
206
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3" />
|
|
207
|
+
<path
|
|
208
|
+
d="M5 1v3M11 1v3"
|
|
209
|
+
stroke="#9CA3AF"
|
|
210
|
+
stroke-width="1.3"
|
|
211
|
+
stroke-linecap="round"
|
|
212
|
+
/>
|
|
213
|
+
</svg>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="field">
|
|
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="#9CA3AF"
|
|
228
|
+
stroke-width="1.3"
|
|
229
|
+
/>
|
|
230
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3" />
|
|
231
|
+
<path
|
|
232
|
+
d="M5 1v3M11 1v3"
|
|
233
|
+
stroke="#9CA3AF"
|
|
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>
|
|
244
|
+
</section>
|
|
245
|
+
|
|
246
|
+
<!-- ===== INCLUDED APPLICATION ===== -->
|
|
247
|
+
<section class="section">
|
|
248
|
+
<h3>Included Application</h3>
|
|
249
|
+
<ul class="app-list">
|
|
250
|
+
<li v-for="(app, idx) in form.includedApps" :key="idx">{{ app }}</li>
|
|
251
|
+
</ul>
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
<!-- ===== SITE PROPERTIES ===== -->
|
|
255
|
+
<section class="section">
|
|
256
|
+
<h3>Site Properties</h3>
|
|
257
|
+
<div class="site-table">
|
|
258
|
+
<table>
|
|
259
|
+
<thead>
|
|
260
|
+
<tr>
|
|
261
|
+
<th>Name</th>
|
|
262
|
+
<th>Location</th>
|
|
263
|
+
</tr>
|
|
264
|
+
</thead>
|
|
265
|
+
<tbody>
|
|
266
|
+
<tr v-if="form.sites.length === 0">
|
|
267
|
+
<td colspan="2" class="empty">No sites found.</td>
|
|
268
|
+
</tr>
|
|
269
|
+
<tr v-for="site in form.sites" :key="site.name">
|
|
270
|
+
<td class="td-name">{{ site.name }}</td>
|
|
271
|
+
<td class="td-muted">{{ site.location }}</td>
|
|
272
|
+
</tr>
|
|
273
|
+
</tbody>
|
|
274
|
+
</table>
|
|
275
|
+
</div>
|
|
276
|
+
</section>
|
|
277
|
+
</div>
|
|
278
|
+
</template>
|
|
279
|
+
|
|
280
|
+
<script setup lang="ts">
|
|
281
|
+
import { reactive, computed, watch } from 'vue'
|
|
282
|
+
import useOrg from '../composables/useOrg'
|
|
283
|
+
import useCustomerSite from '../composables/useCustomerSite'
|
|
284
|
+
import useUser from '../composables/useUser'
|
|
285
|
+
|
|
286
|
+
const {
|
|
287
|
+
APP_SECURITY,
|
|
288
|
+
APP_HYGIENE,
|
|
289
|
+
APP_PROPERTY_MANAGEMENT,
|
|
290
|
+
APP_MECHANICAL_ELECTRICAL,
|
|
291
|
+
APP_PEST_CONTROL,
|
|
292
|
+
APP_LANDSCAPING,
|
|
293
|
+
APP_POOL_MAINTENANCE
|
|
294
|
+
} = useRuntimeConfig().public
|
|
295
|
+
const APP_CONFIG: Record<string, any> = {
|
|
296
|
+
property_management_agency: {
|
|
297
|
+
id: 'property-management',
|
|
298
|
+
title: 'Property Management',
|
|
299
|
+
icon: 'mdi-home-city-outline',
|
|
300
|
+
link: APP_PROPERTY_MANAGEMENT
|
|
301
|
+
},
|
|
302
|
+
security_agency: {
|
|
303
|
+
id: 'security',
|
|
304
|
+
title: 'Security',
|
|
305
|
+
icon: 'mdi-shield-outline',
|
|
306
|
+
link: APP_SECURITY
|
|
307
|
+
},
|
|
308
|
+
cleaning_services: {
|
|
309
|
+
id: 'hygiene',
|
|
310
|
+
title: 'Hygiene',
|
|
311
|
+
icon: 'mdi-spray-bottle',
|
|
312
|
+
link: APP_HYGIENE
|
|
313
|
+
},
|
|
314
|
+
mechanical_electrical_services: {
|
|
315
|
+
id: 'm&e',
|
|
316
|
+
title: 'M&E',
|
|
317
|
+
icon: 'mdi-tools',
|
|
318
|
+
link: APP_MECHANICAL_ELECTRICAL
|
|
319
|
+
},
|
|
320
|
+
pest_control_services: {
|
|
321
|
+
id: 'pest-control-services',
|
|
322
|
+
title: 'Pest Control',
|
|
323
|
+
icon: 'mdi-bug-outline',
|
|
324
|
+
link: APP_PEST_CONTROL
|
|
325
|
+
},
|
|
326
|
+
landscaping_services: {
|
|
327
|
+
id: 'landscaping',
|
|
328
|
+
title: 'Landscaping',
|
|
329
|
+
icon: 'mdi-tree-outline',
|
|
330
|
+
link: APP_LANDSCAPING
|
|
331
|
+
},
|
|
332
|
+
pool_maintenance_services: {
|
|
333
|
+
id: 'pool-maintenance',
|
|
334
|
+
title: 'Pool Maintenance',
|
|
335
|
+
icon: 'mdi-pool',
|
|
336
|
+
link: APP_POOL_MAINTENANCE
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const orgApi = useOrg()
|
|
341
|
+
const userApi = useUser()
|
|
342
|
+
const customerSitesApi = useCustomerSite()
|
|
343
|
+
const orgId = computed(() => props.client?._id)
|
|
344
|
+
|
|
345
|
+
/* ── TYPES ── */
|
|
346
|
+
interface SiteRow {
|
|
347
|
+
name: string
|
|
348
|
+
location: string
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ── PROPS / EMITS ── */
|
|
352
|
+
const props = defineProps<{
|
|
353
|
+
client?: Record<string, any> | null
|
|
354
|
+
}>()
|
|
355
|
+
|
|
356
|
+
const emit = defineEmits<{
|
|
357
|
+
(e: 'cancel'): void
|
|
358
|
+
(e: 'submit', payload: Record<string, any>): void
|
|
359
|
+
}>()
|
|
360
|
+
|
|
361
|
+
/* ── 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
|
+
const PHONE_RULES: Record<string, number> = {
|
|
378
|
+
'+60': 9,
|
|
379
|
+
'+61': 9,
|
|
380
|
+
'+63': 10,
|
|
381
|
+
'+65': 8,
|
|
382
|
+
'+66': 9,
|
|
383
|
+
'+84': 9
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const errors = reactive({
|
|
387
|
+
name: '',
|
|
388
|
+
organizationName: '',
|
|
389
|
+
phone: ''
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const phonePrefix = ref('+65')
|
|
393
|
+
const phoneNumber = ref('')
|
|
394
|
+
|
|
395
|
+
/* ── FORM STATE ── */
|
|
396
|
+
const form = reactive({
|
|
397
|
+
name: '',
|
|
398
|
+
email: '',
|
|
399
|
+
organizationName: '',
|
|
400
|
+
phone: '',
|
|
401
|
+
|
|
402
|
+
planType: 'Paid (Bundle)',
|
|
403
|
+
autoRenewal: 'Yes',
|
|
404
|
+
billingCycle: 'Monthly',
|
|
405
|
+
status: 'Active',
|
|
406
|
+
subscriptionPlan: 'Standard Plan',
|
|
407
|
+
|
|
408
|
+
startDate: '',
|
|
409
|
+
endDate: '',
|
|
410
|
+
|
|
411
|
+
includedApps: [] as string[],
|
|
412
|
+
|
|
413
|
+
sites: [] as SiteRow[]
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
/* ── COMPUTED ── */
|
|
417
|
+
const subscriptionPlanOptions = computed(
|
|
418
|
+
() => SUBSCRIPTION_PLAN_BY_CYCLE[form.billingCycle] ?? []
|
|
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
|
+
})
|
|
427
|
+
|
|
428
|
+
watch(
|
|
429
|
+
() => form.name,
|
|
430
|
+
value => {
|
|
431
|
+
errors.name = value.trim() ? '' : 'Name is required.'
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
watch(
|
|
436
|
+
() => form.organizationName,
|
|
437
|
+
value => {
|
|
438
|
+
errors.organizationName = value.trim()
|
|
439
|
+
? ''
|
|
440
|
+
: 'Organization name is required.'
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
watch([phonePrefix, phoneNumber], ([prefix, value]) => {
|
|
445
|
+
const expected = PHONE_RULES[prefix]
|
|
446
|
+
|
|
447
|
+
if (!value) {
|
|
448
|
+
errors.phone = 'Phone is required.'
|
|
449
|
+
} else if (value.length !== expected) {
|
|
450
|
+
errors.phone = `Phone number must contain exactly ${expected} digits.`
|
|
451
|
+
} else {
|
|
452
|
+
errors.phone = ''
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
/* ── 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
|
+
function validateForm() {
|
|
462
|
+
errors.name = ''
|
|
463
|
+
errors.organizationName = ''
|
|
464
|
+
errors.phone = ''
|
|
465
|
+
|
|
466
|
+
let valid = true
|
|
467
|
+
|
|
468
|
+
if (!form.name.trim()) {
|
|
469
|
+
errors.name = 'Name is required.'
|
|
470
|
+
valid = false
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!form.organizationName.trim()) {
|
|
474
|
+
errors.organizationName = 'Organization name is required.'
|
|
475
|
+
valid = false
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!phoneNumber.value) {
|
|
479
|
+
errors.phone = 'Phone is required.'
|
|
480
|
+
valid = false
|
|
481
|
+
} else {
|
|
482
|
+
const expected = PHONE_RULES[phonePrefix.value]
|
|
483
|
+
|
|
484
|
+
if (expected && phoneNumber.value.length !== expected) {
|
|
485
|
+
errors.phone = `Phone number must contain ${expected} digits.`
|
|
486
|
+
valid = false
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return valid
|
|
491
|
+
}
|
|
492
|
+
async function handleSubmit() {
|
|
493
|
+
if (!validateForm()) return
|
|
494
|
+
|
|
495
|
+
form.phone = `${phonePrefix.value} ${phoneNumber.value}`
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
if (userId.value) {
|
|
499
|
+
await userApi.updateUserFieldById(userId.value, 'name', form.name)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await orgApi.updateOrg(orgId.value!, {
|
|
503
|
+
name: form.organizationName,
|
|
504
|
+
contact: form.phone
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
emit('submit', { ...form })
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error(error)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const userId = ref('')
|
|
513
|
+
async function loadOrganization() {
|
|
514
|
+
if (!orgId.value) return
|
|
515
|
+
|
|
516
|
+
const org = await orgApi.getById(orgId.value)
|
|
517
|
+
|
|
518
|
+
const user = await userApi.getUserByEmail(org.email)
|
|
519
|
+
|
|
520
|
+
userId.value = user?._id || user?.id || ''
|
|
521
|
+
|
|
522
|
+
form.name = user?.name || ''
|
|
523
|
+
form.email = org.email || ''
|
|
524
|
+
form.organizationName = org.name || ''
|
|
525
|
+
const phone = org.contact || ''
|
|
526
|
+
|
|
527
|
+
const match = phone.match(/^(\+\d+)\s*(.*)$/)
|
|
528
|
+
|
|
529
|
+
if (match) {
|
|
530
|
+
phonePrefix.value = match[1]
|
|
531
|
+
phoneNumber.value = match[2].replace(/\D/g, '')
|
|
532
|
+
} else {
|
|
533
|
+
phonePrefix.value = '+65'
|
|
534
|
+
phoneNumber.value = phone.replace(/\D/g, '')
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
form.includedApps =
|
|
538
|
+
org.type && APP_CONFIG[org.type] ? [APP_CONFIG[org.type].title] : []
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function loadSites() {
|
|
542
|
+
if (!orgId.value) return
|
|
543
|
+
|
|
544
|
+
const res = await customerSitesApi.getAll({
|
|
545
|
+
org: orgId.value,
|
|
546
|
+
limit: 100
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const rows = res.items ?? []
|
|
550
|
+
|
|
551
|
+
form.sites = rows.map((site: any) => ({
|
|
552
|
+
name: site.name,
|
|
553
|
+
location: [
|
|
554
|
+
site.address?.line1,
|
|
555
|
+
site.address?.city,
|
|
556
|
+
site.address?.state,
|
|
557
|
+
site.address?.country
|
|
558
|
+
]
|
|
559
|
+
.filter(Boolean)
|
|
560
|
+
.join(', ')
|
|
561
|
+
}))
|
|
562
|
+
}
|
|
563
|
+
watch(
|
|
564
|
+
() => props.client,
|
|
565
|
+
async client => {
|
|
566
|
+
if (!client?._id) return
|
|
567
|
+
|
|
568
|
+
await Promise.all([loadOrganization(), loadSites()])
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
immediate: true
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
</script>
|
|
575
|
+
|
|
576
|
+
<style scoped>
|
|
577
|
+
* {
|
|
578
|
+
box-sizing: border-box;
|
|
579
|
+
margin: 0;
|
|
580
|
+
padding: 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.form-wrap {
|
|
584
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
585
|
+
font-size: 14px;
|
|
586
|
+
color: #111827;
|
|
587
|
+
background: #fff;
|
|
588
|
+
border: 1px solid #e5e7eb;
|
|
589
|
+
border-radius: 10px;
|
|
590
|
+
padding: 24px 28px 32px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* ===== HEADER ===== */
|
|
594
|
+
.header {
|
|
595
|
+
display: flex;
|
|
596
|
+
align-items: flex-start;
|
|
597
|
+
justify-content: space-between;
|
|
598
|
+
padding-bottom: 18px;
|
|
599
|
+
border-bottom: 1px solid #e5e7eb;
|
|
600
|
+
margin-bottom: 22px;
|
|
601
|
+
}
|
|
602
|
+
.header h2 {
|
|
603
|
+
font-size: 20px;
|
|
604
|
+
font-weight: 700;
|
|
605
|
+
color: #111827;
|
|
606
|
+
}
|
|
607
|
+
.header p {
|
|
608
|
+
font-size: 13px;
|
|
609
|
+
color: #2563eb;
|
|
610
|
+
margin-top: 2px;
|
|
611
|
+
}
|
|
612
|
+
.actions {
|
|
613
|
+
display: flex;
|
|
614
|
+
gap: 10px;
|
|
615
|
+
flex-shrink: 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.btn-cancel,
|
|
619
|
+
.btn-submit {
|
|
620
|
+
border: none;
|
|
621
|
+
border-radius: 20px;
|
|
622
|
+
padding: 9px 22px;
|
|
623
|
+
font-size: 13px;
|
|
624
|
+
font-weight: 600;
|
|
625
|
+
cursor: pointer;
|
|
626
|
+
transition: opacity 0.15s;
|
|
627
|
+
}
|
|
628
|
+
.btn-cancel {
|
|
629
|
+
background: #e5e7eb;
|
|
630
|
+
color: #374151;
|
|
631
|
+
}
|
|
632
|
+
.btn-cancel:hover {
|
|
633
|
+
opacity: 0.85;
|
|
634
|
+
}
|
|
635
|
+
.btn-submit {
|
|
636
|
+
background: #e5e7eb;
|
|
637
|
+
color: #374151;
|
|
638
|
+
}
|
|
639
|
+
.btn-submit:hover {
|
|
640
|
+
opacity: 0.85;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* ===== SECTIONS ===== */
|
|
644
|
+
.section {
|
|
645
|
+
margin-bottom: 26px;
|
|
646
|
+
}
|
|
647
|
+
.section h3 {
|
|
648
|
+
font-size: 15px;
|
|
649
|
+
font-weight: 700;
|
|
650
|
+
color: #111827;
|
|
651
|
+
margin-bottom: 14px;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.row {
|
|
655
|
+
display: grid;
|
|
656
|
+
grid-template-columns: 1fr 1fr;
|
|
657
|
+
gap: 20px;
|
|
658
|
+
margin-bottom: 14px;
|
|
659
|
+
}
|
|
660
|
+
.row-single {
|
|
661
|
+
grid-template-columns: 1fr;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.field {
|
|
665
|
+
display: flex;
|
|
666
|
+
flex-direction: column;
|
|
667
|
+
gap: 6px;
|
|
668
|
+
position: relative;
|
|
669
|
+
}
|
|
670
|
+
.field label {
|
|
671
|
+
font-size: 12px;
|
|
672
|
+
color: #6b7280;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.field input[type='text'],
|
|
676
|
+
.field input[type='email'] {
|
|
677
|
+
height: 38px;
|
|
678
|
+
border: 1px solid #e5e7eb;
|
|
679
|
+
border-radius: 8px;
|
|
680
|
+
padding: 0 12px;
|
|
681
|
+
font-size: 13px;
|
|
682
|
+
color: #111827;
|
|
683
|
+
background: #fff;
|
|
684
|
+
outline: none;
|
|
685
|
+
}
|
|
686
|
+
.field input:focus {
|
|
687
|
+
border-color: #9ca3af;
|
|
688
|
+
}
|
|
689
|
+
.field input::placeholder {
|
|
690
|
+
color: #9ca3af;
|
|
691
|
+
}
|
|
692
|
+
.field input:disabled {
|
|
693
|
+
background: #f9fafb;
|
|
694
|
+
color: #6b7280;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* ===== SELECT ===== */
|
|
698
|
+
.select-wrap {
|
|
699
|
+
position: relative;
|
|
700
|
+
}
|
|
701
|
+
.select-wrap select {
|
|
702
|
+
width: 100%;
|
|
703
|
+
height: 38px;
|
|
704
|
+
border: 1px solid #e5e7eb;
|
|
705
|
+
border-radius: 8px;
|
|
706
|
+
padding: 0 30px 0 12px;
|
|
707
|
+
font-size: 13px;
|
|
708
|
+
color: #111827;
|
|
709
|
+
background: #fff;
|
|
710
|
+
appearance: none;
|
|
711
|
+
cursor: pointer;
|
|
712
|
+
outline: none;
|
|
713
|
+
}
|
|
714
|
+
.select-wrap select:focus {
|
|
715
|
+
border-color: #9ca3af;
|
|
716
|
+
}
|
|
717
|
+
.select-wrap .chevron {
|
|
718
|
+
position: absolute;
|
|
719
|
+
right: 11px;
|
|
720
|
+
top: 50%;
|
|
721
|
+
transform: translateY(-50%);
|
|
722
|
+
width: 10px;
|
|
723
|
+
height: 6px;
|
|
724
|
+
pointer-events: none;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* ===== DATE ===== */
|
|
728
|
+
.date-input {
|
|
729
|
+
position: relative;
|
|
730
|
+
display: flex;
|
|
731
|
+
align-items: center;
|
|
732
|
+
}
|
|
733
|
+
.date-input input {
|
|
734
|
+
width: 100%;
|
|
735
|
+
height: 38px;
|
|
736
|
+
border: 1px solid #e5e7eb;
|
|
737
|
+
border-radius: 8px;
|
|
738
|
+
padding: 0 34px 0 12px;
|
|
739
|
+
font-size: 13px;
|
|
740
|
+
color: #6b7280;
|
|
741
|
+
background: #f9fafb;
|
|
742
|
+
outline: none;
|
|
743
|
+
}
|
|
744
|
+
.date-input svg {
|
|
745
|
+
position: absolute;
|
|
746
|
+
right: 12px;
|
|
747
|
+
pointer-events: none;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/* ===== HINT ===== */
|
|
751
|
+
.hint {
|
|
752
|
+
font-size: 11px;
|
|
753
|
+
color: #9ca3af;
|
|
754
|
+
line-height: 1.4;
|
|
755
|
+
}
|
|
756
|
+
.hint-below {
|
|
757
|
+
margin-top: 4px;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/* ===== INCLUDED APPS ===== */
|
|
761
|
+
.app-list {
|
|
762
|
+
list-style: none;
|
|
763
|
+
display: flex;
|
|
764
|
+
flex-direction: column;
|
|
765
|
+
gap: 8px;
|
|
766
|
+
}
|
|
767
|
+
.app-list li {
|
|
768
|
+
font-size: 13px;
|
|
769
|
+
color: #374151;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* ===== SITE TABLE ===== */
|
|
773
|
+
.site-table {
|
|
774
|
+
border: 1px solid #e5e7eb;
|
|
775
|
+
border-radius: 8px;
|
|
776
|
+
overflow: hidden;
|
|
777
|
+
}
|
|
778
|
+
.site-table table {
|
|
779
|
+
width: 100%;
|
|
780
|
+
border-collapse: collapse;
|
|
781
|
+
font-size: 13px;
|
|
782
|
+
}
|
|
783
|
+
.site-table thead th {
|
|
784
|
+
padding: 10px 16px;
|
|
785
|
+
text-align: left;
|
|
786
|
+
font-weight: 600;
|
|
787
|
+
color: #374151;
|
|
788
|
+
background: #f9fafb;
|
|
789
|
+
border-bottom: 1px solid #e5e7eb;
|
|
790
|
+
}
|
|
791
|
+
.site-table tbody tr {
|
|
792
|
+
border-bottom: 1px solid #f3f4f6;
|
|
793
|
+
}
|
|
794
|
+
.site-table tbody tr:last-child {
|
|
795
|
+
border-bottom: none;
|
|
796
|
+
}
|
|
797
|
+
.site-table tbody td {
|
|
798
|
+
padding: 12px 16px;
|
|
799
|
+
}
|
|
800
|
+
.site-table .td-name {
|
|
801
|
+
font-weight: 500;
|
|
802
|
+
color: #111827;
|
|
803
|
+
}
|
|
804
|
+
.site-table .td-muted {
|
|
805
|
+
color: #6b7280;
|
|
806
|
+
}
|
|
807
|
+
.site-table .empty {
|
|
808
|
+
text-align: center;
|
|
809
|
+
color: #9ca3af;
|
|
810
|
+
padding: 24px 0 !important;
|
|
811
|
+
}
|
|
812
|
+
.phone-input {
|
|
813
|
+
display: flex;
|
|
814
|
+
gap: 8px;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.phone-input .prefix {
|
|
818
|
+
width: 80px;
|
|
819
|
+
background: #f3f4f6;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.phone-input .number {
|
|
823
|
+
flex: 1;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.error {
|
|
827
|
+
margin-top: 4px;
|
|
828
|
+
font-size: 12px;
|
|
829
|
+
color: #ef4444;
|
|
830
|
+
}
|
|
831
|
+
</style>
|