@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.
@@ -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>