@7365admin1/layer-common 1.11.43 → 1.11.44

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.44
4
+
5
+ ### Patch Changes
6
+
7
+ - 7fcac6e: Update Layer-common version
8
+
3
9
  ## 1.11.43
4
10
 
5
11
  ### Patch Changes
@@ -1,18 +1,20 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
- <v-col cols="12" class="mb-2">
3
+ <v-col cols="12" class="mb-3">
4
4
  <v-row no-gutters align="center" justify="space-between">
5
- <div>
5
+ <div class="d-flex flex-wrap align-center">
6
6
  <v-btn
7
+ v-if="showAddButton && canAddServiceProvider"
7
8
  class="text-none mr-2"
8
9
  rounded="pill"
9
10
  variant="tonal"
10
- @click="(createDialog = true), setServiceProvider({ mode: 'add' })"
11
11
  size="large"
12
+ @click="(createDialog = true), setServiceProvider({ mode: 'add' })"
12
13
  >
13
14
  Add
14
15
  </v-btn>
15
16
  <v-btn
17
+ v-if="canInviteServiceProvider"
16
18
  class="text-none mr-2"
17
19
  rounded="pill"
18
20
  variant="tonal"
@@ -25,6 +27,7 @@
25
27
  Invite
26
28
  </v-btn>
27
29
  <v-btn
30
+ v-if="showBillingButton"
28
31
  class="text-none mr-2"
29
32
  rounded="pill"
30
33
  variant="tonal"
@@ -41,16 +44,18 @@
41
44
 
42
45
  <v-text-field
43
46
  v-model="searchText"
44
- placeholder="Search..."
47
+ placeholder="Search"
45
48
  variant="outlined"
46
49
  density="comfortable"
47
50
  clearable
48
51
  hide-details
52
+ prepend-inner-icon="mdi-magnify"
49
53
  class="ml-2"
50
- style="max-width: 250px"
54
+ style="max-width: 280px"
51
55
  />
52
56
  </v-row>
53
57
  </v-col>
58
+
54
59
  <v-col cols="12">
55
60
  <v-card
56
61
  width="100%"
@@ -59,50 +64,227 @@
59
64
  rounded="lg"
60
65
  :loading="loading"
61
66
  >
62
- <v-toolbar density="compact" color="grey-lighten-4">
63
- <template #prepend>
67
+ <div class="sp-table-header">
68
+ <v-toolbar
69
+ density="compact"
70
+ color="grey-lighten-4"
71
+ flat
72
+ class="sp-toolbar-top px-2"
73
+ >
64
74
  <v-btn
65
- fab
66
75
  icon
76
+ variant="text"
67
77
  density="comfortable"
68
- @click="_getAllServiceProvider()"
78
+ @click="loadList()"
69
79
  >
70
80
  <v-icon>mdi-refresh</v-icon>
71
81
  </v-btn>
72
- </template>
73
82
 
74
- <template #append>
75
- <v-row no-gutters justify="end" align="center">
76
- <span class="mr-2 text-caption text-fontgray">
77
- {{ pageRange }}
78
- </span>
83
+ <v-spacer />
84
+
85
+ <template v-if="canViewServiceProviders">
86
+ <span class="mr-2 text-caption text-medium-emphasis">{{
87
+ pageRange
88
+ }}</span>
79
89
  <local-pagination
80
90
  v-model="page"
81
91
  :length="pages"
82
- @update:value="_getAllServiceProvider()"
92
+ @update:value="loadList()"
83
93
  />
84
- </v-row>
85
- </template>
86
- </v-toolbar>
94
+ </template>
95
+ </v-toolbar>
96
+
97
+ <v-tabs
98
+ v-model="listTab"
99
+ color="primary"
100
+ density="comfortable"
101
+ class="sp-tabs"
102
+ align-tabs="start"
103
+ >
104
+ <v-tab value="active" class="text-none">Service Providers</v-tab>
105
+ <v-tab value="pending" class="text-none">Pending</v-tab>
106
+ <v-tab value="inactive" class="text-none">In-Active</v-tab>
107
+ </v-tabs>
108
+ </div>
87
109
 
88
110
  <v-data-table
111
+ v-if="canViewServiceProviders"
89
112
  :headers="headers"
90
113
  :items="items"
91
114
  item-value="_id"
92
- items-per-page="20"
115
+ :items-per-page="pageSize"
93
116
  fixed-header
94
117
  hide-default-footer
95
- hide-default-header
96
- style="max-height: calc(100vh - (180px))"
118
+ class="sp-table"
119
+ style="max-height: calc(100vh - 280px)"
97
120
  >
98
- <template #item.category="{ item }">
99
- <span class="text-subtitle-2">{{ item.category }}</span>
121
+ <template #item.companyName="{ item }">
122
+ <span class="text-body-2 d-block text-truncate sp-col-company">{{
123
+ item.companyName
124
+ }}</span>
125
+ </template>
126
+ <template #item.email="{ item }">
127
+ <span class="text-body-2 d-block text-truncate sp-col-email">{{
128
+ item.email
129
+ }}</span>
130
+ </template>
131
+ <template #item.type="{ item }">
132
+ <span class="text-body-2 d-block text-truncate sp-col-type">{{
133
+ item.type
134
+ }}</span>
135
+ </template>
136
+ <template #item.members="{ item }">
137
+ <span class="text-body-2">{{ item.members }}</span>
138
+ </template>
139
+ <template #item.status="{ item }">
140
+ <v-chip
141
+ :color="statusChipColor(item.status)"
142
+ size="small"
143
+ variant="tonal"
144
+ class="text-none"
145
+ >
146
+ {{ item.status }}
147
+ </v-chip>
148
+ </template>
149
+ <template #item.actions="{ item }">
150
+ <v-menu v-if="item.source === 'provider'" location="bottom end">
151
+ <template #activator="{ props: menuProps }">
152
+ <v-btn
153
+ v-bind="menuProps"
154
+ icon
155
+ variant="text"
156
+ density="comfortable"
157
+ aria-label="Row actions"
158
+ >
159
+ <v-icon>mdi-dots-vertical</v-icon>
160
+ </v-btn>
161
+ </template>
162
+ <v-list density="compact" min-width="140">
163
+ <v-list-item
164
+ v-if="canViewMembers(item)"
165
+ title="View Members"
166
+ @click="openView(item)"
167
+ />
168
+ <v-list-item
169
+ :title="rowActionLabel(item)"
170
+ :base-color="
171
+ item.source === 'provider' && listTab === 'inactive'
172
+ ? undefined
173
+ : 'error'
174
+ "
175
+ @click="onDeleteRow(item)"
176
+ />
177
+ </v-list>
178
+ </v-menu>
100
179
  </template>
101
180
  </v-data-table>
181
+ <v-card-text v-else class="text-body-2 text-medium-emphasis">
182
+ You do not have permission to view service providers.
183
+ </v-card-text>
184
+
102
185
  </v-card>
103
186
  </v-col>
104
187
  </v-row>
105
188
 
189
+ <v-dialog v-model="viewDialog" max-width="514">
190
+ <v-card rounded="lg" class="sp-members-card">
191
+ <v-card-title class="sp-members-title">
192
+ Members ({{ memberDialogCount }})
193
+ </v-card-title>
194
+
195
+ <v-divider />
196
+
197
+ <v-table class="sp-members-table">
198
+ <thead>
199
+ <tr>
200
+ <th>Name</th>
201
+ <th class="text-right">Role</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ <tr v-if="loadingMembers">
206
+ <td colspan="2" class="text-center text-medium-emphasis">
207
+ Loading...
208
+ </td>
209
+ </tr>
210
+ <tr v-else-if="!serviceProviderMembers.length">
211
+ <td colspan="2" class="text-center text-medium-emphasis">
212
+ No members found.
213
+ </td>
214
+ </tr>
215
+ <template v-else>
216
+ <tr v-for="member in serviceProviderMembers" :key="member._id">
217
+ <td>{{ member.name }}</td>
218
+ <td class="text-right">{{ member.role }}</td>
219
+ </tr>
220
+ </template>
221
+ </tbody>
222
+ </v-table>
223
+
224
+ <v-divider />
225
+
226
+ <v-card-actions class="pa-0">
227
+ <v-btn
228
+ block
229
+ variant="text"
230
+ height="56"
231
+ class="text-none font-weight-bold"
232
+ @click="viewDialog = false"
233
+ >
234
+ Close
235
+ </v-btn>
236
+ </v-card-actions>
237
+ </v-card>
238
+ </v-dialog>
239
+
240
+ <v-dialog v-model="statusDialog" max-width="728" persistent>
241
+ <v-card rounded="lg" class="sp-status-card">
242
+ <v-card-title class="sp-status-title">
243
+ {{ statusDialogTitle }}
244
+ </v-card-title>
245
+
246
+ <v-card-text class="sp-status-body">
247
+ <div class="sp-status-question">
248
+ {{ statusDialogQuestion }}
249
+ </div>
250
+ <div class="sp-status-copy">
251
+ {{ statusDialogCopy }}
252
+ </div>
253
+ </v-card-text>
254
+
255
+ <v-divider />
256
+
257
+ <v-card-actions class="pa-0">
258
+ <v-row no-gutters class="fill-height">
259
+ <v-col cols="6">
260
+ <v-btn
261
+ block
262
+ variant="flat"
263
+ height="82"
264
+ class="text-none sp-status-cancel"
265
+ :disabled="statusUpdating"
266
+ @click="closeStatusDialog"
267
+ >
268
+ Cancel
269
+ </v-btn>
270
+ </v-col>
271
+ <v-col cols="6">
272
+ <v-btn
273
+ block
274
+ variant="flat"
275
+ height="82"
276
+ class="text-none sp-status-confirm"
277
+ :loading="statusUpdating"
278
+ @click="confirmStatusChange"
279
+ >
280
+ {{ statusConfirmLabel }}
281
+ </v-btn>
282
+ </v-col>
283
+ </v-row>
284
+ </v-card-actions>
285
+ </v-card>
286
+ </v-dialog>
287
+
106
288
  <v-dialog v-model="createDialog" width="500" persistent>
107
289
  <v-card width="100%">
108
290
  <v-toolbar>
@@ -233,27 +415,28 @@
233
415
  </v-card>
234
416
  </v-dialog>
235
417
 
236
- <v-dialog v-model="createInviteDialog" width="500" persistent>
237
- <v-card width="100%">
238
- <v-toolbar>
418
+ <v-dialog v-model="createInviteDialog" width="680" persistent>
419
+ <v-card width="100%" rounded="lg" class="sp-invite-card">
420
+ <v-toolbar flat color="grey-lighten-4" height="76">
239
421
  <v-row no-gutters class="fill-height px-6" align="center">
240
422
  <span class="font-weight-bold text-h5">
241
423
  Invite Service Provider
242
424
  </span>
243
425
  </v-row>
244
426
  </v-toolbar>
245
- <v-card-text style="max-height: 100vh; overflow-y: auto">
427
+ <v-card-text class="sp-invite-body">
246
428
  <v-form
247
429
  v-model="validServiceProvider"
248
430
  :disabled="disableServiceProvider"
249
431
  >
250
- <v-row no-gutters class="px-4">
251
- <v-col cols="12" class="mt-2">
432
+ <v-row no-gutters>
433
+ <v-col cols="12">
252
434
  <v-row no-gutters>
253
435
  <InputLabel class="text-capitalize" title="Email" required />
254
436
  <v-col cols="12">
255
437
  <v-text-field
256
- v-model="serviceProvider.email"
438
+ v-model="serviceProviderInvite.email"
439
+ variant="outlined"
257
440
  density="comfortable"
258
441
  :rules="[requiredRule, emailRule]"
259
442
  ></v-text-field>
@@ -262,17 +445,61 @@
262
445
  </v-col>
263
446
 
264
447
  <v-col cols="12">
265
- <v-checkbox
266
- v-model="createMoreServiceProvider"
267
- density="comfortable"
268
- hide-details
269
- >
270
- <template #label>
271
- <span class="text-subtitle-2 font-weight-bold">
272
- Invite more
273
- </span>
274
- </template>
275
- </v-checkbox>
448
+ <v-divider class="mb-6" />
449
+ </v-col>
450
+
451
+ <v-col cols="12">
452
+ <v-row no-gutters>
453
+ <InputLabel class="text-capitalize" title="App" required />
454
+ <v-col cols="12">
455
+ <v-select
456
+ v-model="serviceProviderInvite.app"
457
+ :items="serviceProviderInviteApps"
458
+ item-title="title"
459
+ item-value="value"
460
+ variant="outlined"
461
+ density="comfortable"
462
+ :rules="[requiredRule]"
463
+ @update:model-value="loadInviteRoles"
464
+ ></v-select>
465
+ </v-col>
466
+ </v-row>
467
+ </v-col>
468
+
469
+ <v-col cols="12">
470
+ <v-row no-gutters>
471
+ <InputLabel class="text-capitalize" title="Site" required />
472
+ <v-col cols="12">
473
+ <v-select
474
+ v-model="serviceProviderInvite.siteId"
475
+ :items="serviceProviderInviteSites"
476
+ item-title="title"
477
+ item-value="value"
478
+ variant="outlined"
479
+ density="comfortable"
480
+ :rules="[requiredRule]"
481
+ @update:model-value="setInviteSiteName"
482
+ ></v-select>
483
+ </v-col>
484
+ </v-row>
485
+ </v-col>
486
+
487
+ <v-col cols="12">
488
+ <v-row no-gutters>
489
+ <InputLabel class="text-capitalize" title="Role" required />
490
+ <v-col cols="12">
491
+ <v-select
492
+ v-model="serviceProviderInvite.role"
493
+ :items="serviceProviderInviteRoles"
494
+ item-title="name"
495
+ item-value="_id"
496
+ variant="outlined"
497
+ density="comfortable"
498
+ :loading="loadingInviteRoles"
499
+ :rules="[requiredRule]"
500
+ ></v-select>
501
+ </v-col>
502
+ </v-row>
276
503
  </v-col>
277
504
 
278
505
  <v-col cols="12" class="my-2">
@@ -290,27 +517,26 @@
290
517
  </v-form>
291
518
  </v-card-text>
292
519
 
293
- <v-toolbar>
294
- <v-row class="px-6">
295
- <v-col cols="6">
520
+ <v-toolbar flat height="76" class="sp-invite-footer">
521
+ <v-row no-gutters class="fill-height">
522
+ <v-col cols="6" class="fill-height">
296
523
  <v-btn
297
524
  block
298
- variant="text"
299
- class="text-none"
300
- size="large"
525
+ variant="flat"
526
+ class="text-none sp-invite-footer-btn"
527
+ height="76"
301
528
  @click="setServiceProvider({ mode: 'invite', dialog: false })"
302
529
  >
303
530
  Cancel
304
531
  </v-btn>
305
532
  </v-col>
306
533
 
307
- <v-col cols="6">
534
+ <v-col cols="6" class="fill-height">
308
535
  <v-btn
309
536
  block
310
537
  variant="flat"
311
- color="black"
312
- class="text-none"
313
- size="large"
538
+ class="text-none sp-invite-submit-btn"
539
+ height="76"
314
540
  :disabled="!validServiceProvider"
315
541
  :loading="disableServiceProvider"
316
542
  @click="submitServiceProviderInvite()"
@@ -328,8 +554,29 @@
328
554
 
329
555
  <script lang="ts" setup>
330
556
  import useServiceProvider from "../composables/useServiceProvider";
557
+ import useVerification from "../composables/useVerification";
558
+ import useRole from "../composables/useRole";
331
559
  import { errorConverter } from "../utils/data";
332
560
 
561
+ type ListTab = "active" | "pending" | "inactive";
562
+
563
+ type TableRow = {
564
+ _id: string;
565
+ serviceProviderOrgId: string;
566
+ companyName: string;
567
+ email: string;
568
+ type: string;
569
+ members: string;
570
+ status: string;
571
+ source: "provider" | "invite";
572
+ };
573
+
574
+ type ServiceProviderMemberRow = {
575
+ _id: string;
576
+ name: string;
577
+ role: string;
578
+ };
579
+
333
580
  const props = defineProps({
334
581
  orgId: {
335
582
  type: String,
@@ -362,45 +609,86 @@ const props = defineProps({
362
609
  },
363
610
  canAddServiceProvider: {
364
611
  type: Boolean,
365
- default: false,
612
+ default: true,
366
613
  },
367
614
  canInviteServiceProvider: {
368
615
  type: Boolean,
369
- default: false,
616
+ default: true,
370
617
  },
371
618
  canViewServiceProviders: {
372
619
  type: Boolean,
373
- default: false,
620
+ default: true,
621
+ },
622
+ showAddButton: {
623
+ type: Boolean,
624
+ default: true,
625
+ },
626
+ showBillingButton: {
627
+ type: Boolean,
628
+ default: true,
374
629
  },
375
630
  });
376
631
 
377
632
  const headers = [
378
633
  {
379
- title: "Name",
380
- value: "name",
634
+ title: "Company name",
635
+ value: "companyName",
636
+ sortable: true,
637
+ minWidth: "140px",
638
+ maxWidth: "220px",
639
+ },
640
+ {
641
+ title: "Email",
642
+ value: "email",
643
+ sortable: true,
644
+ minWidth: "180px",
645
+ maxWidth: "260px",
646
+ },
647
+ {
648
+ title: "Type",
649
+ value: "type",
650
+ sortable: true,
651
+ minWidth: "120px",
652
+ maxWidth: "180px",
653
+ },
654
+ {
655
+ title: "Members",
656
+ value: "members",
657
+ sortable: false,
658
+ width: "88px",
659
+ align: "start" as const,
381
660
  },
382
661
  {
383
662
  title: "Status",
384
663
  value: "status",
664
+ sortable: true,
665
+ width: "120px",
385
666
  },
386
667
  {
387
- title: "Category",
388
- value: "category",
668
+ title: "",
669
+ value: "actions",
670
+ sortable: false,
671
+ align: "end" as const,
672
+ width: "56px",
673
+ minWidth: "56px",
389
674
  },
390
675
  ];
391
676
 
392
677
  const { natureOfBusiness } = useLocal();
393
- const { requiredRule, emailRule } = useUtils();
678
+ const { requiredRule, emailRule, debounce } = useUtils();
394
679
 
680
+ const listTab = ref<ListTab>("active");
395
681
  const page = ref(1);
396
682
  const pages = ref(0);
397
683
  const pageRange = ref("-- - -- of --");
684
+ const pageSize = 20;
398
685
 
399
686
  const message = ref("");
400
687
  const messageSnackbar = ref(false);
401
688
  const messageColor = ref("");
402
689
 
403
- const items = ref<Array<Record<string, any>>>([]);
690
+ const items = ref<TableRow[]>([]);
691
+ const loading = ref(false);
404
692
 
405
693
  const validServiceProvider = ref(false);
406
694
  const disableServiceProvider = ref(false);
@@ -417,56 +705,348 @@ const serviceProvider = ref({
417
705
  orgName: "",
418
706
  category: "",
419
707
  });
708
+ const defaultInviteApp = "property_management_agency";
709
+ const serviceProviderInvite = ref({
710
+ email: "",
711
+ app: defaultInviteApp,
712
+ siteId: props.siteId,
713
+ siteName: props.siteName,
714
+ role: "",
715
+ });
420
716
 
421
717
  const {
422
718
  getAll: getAllServiceProvider,
423
719
  add: addServiceProvider,
424
720
  createServiceProviderInvite,
721
+ updateStatus: updateServiceProviderStatus,
425
722
  } = useServiceProvider();
723
+ const { getAll: getAllMembers } = useMember();
426
724
 
427
- const { getSiteById } = useSite();
725
+ const { getVerifications, cancelUserInvitation } = useVerification();
726
+ const { getRoles } = useRole();
428
727
 
429
- const { getColorStatus, debounce } = useUtils();
728
+ const { getSiteById } = useSite();
430
729
 
431
730
  const { data: site } = await useLazyAsyncData(
432
731
  "get-site-by-id-" + props.siteId,
433
732
  () => getSiteById(props.siteId)
434
733
  );
435
734
 
436
- const loading = computed(() => getAllReqStatus.value === "pending");
735
+ const searchText = ref("");
437
736
 
438
- const {
439
- data: serviceProviderReq,
440
- refresh: _getAllServiceProvider,
441
- status: getAllReqStatus,
442
- } = await useLazyAsyncData("get-all-service-providers", () =>
443
- getAllServiceProvider({
444
- page: page.value,
445
- siteId: props.siteId,
446
- search: searchText.value || "",
447
- })
737
+ const createDialog = ref(false);
738
+ const createInviteDialog = ref(false);
739
+ const loadingInviteRoles = ref(false);
740
+ const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
741
+
742
+ const serviceProviderInviteApps = computed(() =>
743
+ natureOfBusiness.map((item) => ({
744
+ ...item,
745
+ title:
746
+ item.value === "cleaning_services"
747
+ ? "Cleaning"
748
+ : item.title.replace(" Services", ""),
749
+ }))
448
750
  );
449
751
 
450
- watchEffect(() => {
451
- // console.log("serviceProviderReq", serviceProviderReq.value);
452
- if (serviceProviderReq.value) {
453
- items.value = serviceProviderReq.value.items;
454
- pages.value = serviceProviderReq.value.pages;
455
- pageRange.value = serviceProviderReq.value.pageRange;
752
+ const serviceProviderInviteSites = computed(() => [
753
+ {
754
+ title: site.value?.name || props.siteName || props.siteId,
755
+ value: props.siteId,
756
+ },
757
+ ]);
758
+
759
+ const viewDialog = ref(false);
760
+ const viewRow = ref<TableRow | null>(null);
761
+ const loadingMembers = ref(false);
762
+ const serviceProviderMembers = ref<ServiceProviderMemberRow[]>([]);
763
+ const memberDialogCount = computed(() =>
764
+ loadingMembers.value
765
+ ? viewRow.value?.members ?? 0
766
+ : serviceProviderMembers.value.length
767
+ );
768
+
769
+ function canViewMembers(row: TableRow) {
770
+ return row.source === "provider";
771
+ }
772
+
773
+ const statusDialog = ref(false);
774
+ const statusUpdating = ref(false);
775
+ const statusActionRow = ref<TableRow | null>(null);
776
+ const statusAction = ref<"delete" | "reactivate">("delete");
777
+ const statusDialogTitle = computed(() =>
778
+ statusAction.value === "reactivate"
779
+ ? "Reactivate Service Provider"
780
+ : "Delete Service Provider"
781
+ );
782
+ const statusDialogQuestion = computed(() =>
783
+ statusAction.value === "reactivate"
784
+ ? "Are you sure you want to reactivate this service provider?"
785
+ : "Are you sure you want to delete this service provider?"
786
+ );
787
+ const statusDialogCopy = computed(() =>
788
+ statusAction.value === "reactivate"
789
+ ? "This action will restore the record. The service provider will be moved to the Active tab."
790
+ : "This action will not permanently remove the record. The service provider will be moved to the Inactive tab."
791
+ );
792
+ const statusConfirmLabel = computed(() =>
793
+ statusAction.value === "reactivate" ? "Reactivate" : "Confirm"
794
+ );
795
+
796
+ function rowActionLabel(row: TableRow) {
797
+ return row.source === "provider" && listTab.value === "inactive"
798
+ ? "Reactivate"
799
+ : "Delete";
800
+ }
801
+
802
+ function typeDisplay(type: string, nature?: string) {
803
+ if (nature && String(nature).trim()) return String(nature);
804
+ const list = natureOfBusiness.value as Array<{ title?: string; value?: string }>;
805
+ const hit = list?.find((x) => x.value === type);
806
+ return hit?.title ?? type ?? "-";
807
+ }
808
+
809
+ function statusDisplay(raw: string, source: TableRow["source"]) {
810
+ if (source === "invite") return "Pending";
811
+ const s = (raw || "").toLowerCase();
812
+ if (s === "inactive") return "In-Active";
813
+ if (!raw) return "-";
814
+ return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
815
+ }
816
+
817
+ function statusChipColor(statusLabel: string) {
818
+ const s = statusLabel.toLowerCase();
819
+ if (s === "active") return "success";
820
+ if (s === "pending") return "warning";
821
+ if (s === "in-active" || s === "inactive") return "default";
822
+ return "primary";
823
+ }
824
+
825
+ async function loadInviteRoles() {
826
+ loadingInviteRoles.value = true;
827
+ serviceProviderInvite.value.role = "";
828
+ try {
829
+ const response = await getRoles({
830
+ org: props.orgId,
831
+ type: serviceProviderInvite.value.app,
832
+ limit: 50,
833
+ });
834
+ serviceProviderInviteRoles.value = response?.items ?? [];
835
+ serviceProviderInvite.value.role =
836
+ serviceProviderInviteRoles.value[0]?._id ?? "";
837
+ } catch (error: any) {
838
+ serviceProviderInviteRoles.value = [];
839
+ showMessage(errorConverter(error), "error");
840
+ } finally {
841
+ loadingInviteRoles.value = false;
842
+ }
843
+ }
844
+
845
+ function setInviteSiteName(siteId: string) {
846
+ const selectedSite = serviceProviderInviteSites.value.find(
847
+ (item) => item.value === siteId
848
+ );
849
+ serviceProviderInvite.value.siteName = selectedSite?.title ?? "";
850
+ }
851
+
852
+ function formatMembersCell(value: unknown): string {
853
+ if (value == null || value === "") return "0";
854
+ const n = Number(value);
855
+ if (!Number.isFinite(n)) return "0";
856
+ return String(n);
857
+ }
858
+
859
+ function formatEmailCell(value: unknown): string {
860
+ if (value == null) return "-";
861
+ const s = String(value).trim();
862
+ return s.length ? s : "-";
863
+ }
864
+
865
+ function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
866
+ return raw.map((row) => ({
867
+ _id: String(row._id ?? ""),
868
+ serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
869
+ companyName: row.name ?? "-",
870
+ email: formatEmailCell(row.email),
871
+ type: typeDisplay(row.type ?? "", row.nature),
872
+ members: formatMembersCell(row.members),
873
+ status: statusDisplay(row.status ?? "active", "provider"),
874
+ source: "provider",
875
+ }));
876
+ }
877
+
878
+ function inviteTypeLabel(t: string) {
879
+ const key = (t || "").toLowerCase();
880
+ const map: Record<string, string> = {
881
+ "service-provider-invite": "Service provider invite",
882
+ "service-provider-create-org": "Service provider (new org)",
883
+ };
884
+ return map[key] ?? (t ? t.replace(/-/g, " ") : "-");
885
+ }
886
+
887
+ function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
888
+ return raw.map((row) => ({
889
+ _id: String(row._id ?? ""),
890
+ serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
891
+ companyName: row.metadata?.name || "-",
892
+ email: formatEmailCell(row.email),
893
+ type: row.metadata?.app
894
+ ? typeDisplay(row.metadata.app)
895
+ : inviteTypeLabel(row.type ?? ""),
896
+ members: "-",
897
+ status: "Pending",
898
+ source: "invite",
899
+ }));
900
+ }
901
+
902
+ async function loadList() {
903
+ if (!props.siteId || !props.canViewServiceProviders) {
904
+ items.value = [];
905
+ pages.value = 0;
906
+ pageRange.value = "-- - -- of --";
907
+ return;
456
908
  }
457
- // loading.value = false;
909
+
910
+ loading.value = true;
911
+ try {
912
+ if (listTab.value === "pending") {
913
+ const data = await getVerifications({
914
+ status: "pending",
915
+ type: "service-provider-invite,service-provider-create-org",
916
+ page: page.value,
917
+ search: searchText.value?.trim() || "",
918
+ siteId: props.siteId,
919
+ orgId: props.orgId,
920
+ limit: pageSize,
921
+ });
922
+ items.value = mapInviteRows(data.items ?? []);
923
+ pages.value = data.pages ?? 0;
924
+ pageRange.value = data.pageRange ?? "-- - -- of --";
925
+ return;
926
+ }
927
+
928
+ const statusParam = listTab.value === "active" ? "active" : "inactive";
929
+ const data = await getAllServiceProvider({
930
+ page: page.value,
931
+ siteId: props.siteId,
932
+ search: searchText.value?.trim() || "",
933
+ limit: pageSize,
934
+ status: statusParam,
935
+ });
936
+ items.value = mapProviderRows(data.items ?? []);
937
+ pages.value = data.pages ?? 0;
938
+ pageRange.value = data.pageRange ?? "-- - -- of --";
939
+ } catch (error: any) {
940
+ showMessage(errorConverter(error), "error");
941
+ items.value = [];
942
+ pages.value = 0;
943
+ pageRange.value = "-- - -- of --";
944
+ } finally {
945
+ loading.value = false;
946
+ }
947
+ }
948
+
949
+ async function openView(row: TableRow) {
950
+ viewRow.value = row;
951
+ serviceProviderMembers.value = [];
952
+ viewDialog.value = true;
953
+
954
+ if (!row.serviceProviderOrgId) return;
955
+
956
+ loadingMembers.value = true;
957
+ try {
958
+ const data = await getAllMembers({
959
+ page: 1,
960
+ limit: 1000,
961
+ org: row.serviceProviderOrgId,
962
+ status: "active",
963
+ });
964
+ serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
965
+ _id: String(member._id ?? ""),
966
+ name: member.name || member.fullName || member.email || "-",
967
+ role: member.roleName || member.role?.name || "-",
968
+ }));
969
+ } catch (error: any) {
970
+ showMessage(errorConverter(error), "error");
971
+ } finally {
972
+ loadingMembers.value = false;
973
+ }
974
+ }
975
+
976
+ async function onDeleteRow(row: TableRow) {
977
+ if (row.source === "invite") {
978
+ const ok = confirm("Cancel this invitation?");
979
+ if (!ok) return;
980
+ try {
981
+ await cancelUserInvitation(row._id);
982
+ showMessage("Invitation cancelled.", "success");
983
+ await loadList();
984
+ } catch (error: any) {
985
+ showMessage(errorConverter(error), "error");
986
+ }
987
+ return;
988
+ }
989
+
990
+ statusActionRow.value = row;
991
+ statusAction.value = listTab.value === "inactive" ? "reactivate" : "delete";
992
+ statusDialog.value = true;
993
+ }
994
+
995
+ function closeStatusDialog() {
996
+ if (statusUpdating.value) return;
997
+ statusDialog.value = false;
998
+ statusActionRow.value = null;
999
+ }
1000
+
1001
+ async function confirmStatusChange() {
1002
+ if (!statusActionRow.value) return;
1003
+
1004
+ statusUpdating.value = true;
1005
+ const nextStatus = statusAction.value === "reactivate" ? "active" : "inactive";
1006
+ try {
1007
+ await updateServiceProviderStatus(statusActionRow.value._id, nextStatus);
1008
+ showMessage(
1009
+ statusAction.value === "reactivate"
1010
+ ? "Service provider reactivated."
1011
+ : "Service provider moved to In-Active.",
1012
+ "success"
1013
+ );
1014
+ statusDialog.value = false;
1015
+ statusActionRow.value = null;
1016
+ listTab.value = nextStatus === "active" ? "active" : "inactive";
1017
+ page.value = 1;
1018
+ await loadList();
1019
+ } catch (error: any) {
1020
+ showMessage(errorConverter(error), "error");
1021
+ } finally {
1022
+ statusUpdating.value = false;
1023
+ }
1024
+ }
1025
+
1026
+ watch(listTab, () => {
1027
+ page.value = 1;
1028
+ loadList();
458
1029
  });
459
1030
 
460
- const createDialog = ref(false);
461
- const createInviteDialog = ref(false);
1031
+ watch(page, () => {
1032
+ loadList();
1033
+ });
462
1034
 
463
- const searchText = ref("");
1035
+ const debouncedSearch = debounce(() => {
1036
+ page.value = 1;
1037
+ loadList();
1038
+ }, 500);
464
1039
 
465
- const success = () => {
466
- createDialog.value = false;
467
- // getServiceProvider();
468
- _getAllServiceProvider();
469
- };
1040
+ watch(
1041
+ () => searchText.value,
1042
+ () => {
1043
+ debouncedSearch();
1044
+ }
1045
+ );
1046
+
1047
+ onMounted(() => {
1048
+ loadList();
1049
+ });
470
1050
 
471
1051
  function showMessage(msg: string, color: string) {
472
1052
  message.value = msg;
@@ -474,25 +1054,16 @@ function showMessage(msg: string, color: string) {
474
1054
  messageSnackbar.value = true;
475
1055
  }
476
1056
 
477
- function onNotify(payload: { message: string; color: string }) {
478
- showMessage(payload.message, payload.color);
479
- }
480
-
481
- const dialogServiceProviderInvite = ref(false);
482
- const dialogServiceProviderAdd = ref(false);
483
-
484
1057
  function setServiceProvider({
485
1058
  mode = "invite",
486
1059
  dialog = true,
487
1060
  data = {} as Record<string, any>,
488
1061
  } = {}) {
489
1062
  if (mode === "invite") {
490
- // dialogServiceProviderInvite.value = dialog;
491
1063
  createInviteDialog.value = dialog;
492
1064
  }
493
1065
 
494
1066
  if (mode === "add") {
495
- // dialogServiceProviderAdd.value = dialog;
496
1067
  createDialog.value = dialog;
497
1068
  }
498
1069
 
@@ -508,6 +1079,18 @@ function setServiceProvider({
508
1079
  orgName: "Org Name",
509
1080
  category: mode === "add" ? "internal" : "external",
510
1081
  };
1082
+
1083
+ if (mode === "invite") {
1084
+ serviceProviderInvite.value = {
1085
+ email: "",
1086
+ app: defaultInviteApp,
1087
+ siteId: props.siteId,
1088
+ siteName: site.value?.name || props.siteName,
1089
+ role: "",
1090
+ };
1091
+ createMoreServiceProvider.value = false;
1092
+ void loadInviteRoles();
1093
+ }
511
1094
  return;
512
1095
  }
513
1096
 
@@ -522,11 +1105,9 @@ async function submitServiceProviderAdd() {
522
1105
  messageServiceProvider.value = "";
523
1106
 
524
1107
  try {
525
- console.log("serviceProvider.value");
526
- console.log(serviceProvider.value);
527
1108
  const result = await addServiceProvider(serviceProvider.value);
528
1109
  await setServiceProvider({ mode: "add", dialog: false });
529
- await _getAllServiceProvider();
1110
+ await loadList();
530
1111
 
531
1112
  if (result?.id) {
532
1113
  showMessage(result?.message, "success");
@@ -546,33 +1127,195 @@ async function submitServiceProviderInvite() {
546
1127
  messageServiceProvider.value = "";
547
1128
  try {
548
1129
  const payload = {
549
- email: serviceProvider.value.email?.trim(),
1130
+ email: serviceProviderInvite.value.email.trim(),
1131
+ name: "",
1132
+ app: serviceProviderInvite.value.app,
1133
+ role: serviceProviderInvite.value.role,
550
1134
  orgId: props.orgId,
551
- siteId: props.siteId,
552
- siteName: site.value?.name || props.siteName,
1135
+ siteId: serviceProviderInvite.value.siteId,
1136
+ siteName:
1137
+ serviceProviderInvite.value.siteName || site.value?.name || props.siteName,
553
1138
  };
554
1139
 
555
1140
  await createServiceProviderInvite(payload);
1141
+ showMessage("Invitation sent.", "success");
556
1142
  if (createMoreServiceProvider.value) {
557
- serviceProvider.value.email = "";
1143
+ serviceProviderInvite.value.email = "";
558
1144
  } else {
559
1145
  await setServiceProvider({ mode: "invite", dialog: false });
560
1146
  }
1147
+ await loadList();
561
1148
  } catch (error: any) {
562
1149
  messageServiceProvider.value =
563
1150
  error?.response?._data?.message ??
564
1151
  "An error occurred while inviting the service provider.";
1152
+ showMessage(errorConverter(error), "error");
565
1153
  } finally {
566
1154
  disableServiceProvider.value = false;
567
1155
  }
568
1156
  }
1157
+ </script>
569
1158
 
570
- const debounceSearch = debounce(_getAllServiceProvider, 500);
1159
+ <style scoped>
1160
+ .sp-toolbar-top {
1161
+ display: flex;
1162
+ align-items: center;
1163
+ flex-wrap: nowrap;
1164
+ gap: 4px;
1165
+ }
571
1166
 
572
- watch(
573
- () => searchText.value,
574
- () => {
575
- debounceSearch();
576
- }
577
- );
578
- </script>
1167
+ .sp-table-header {
1168
+ background: #f5f5f5;
1169
+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
1170
+ }
1171
+
1172
+ .sp-tabs {
1173
+ width: 100%;
1174
+ min-height: 40px;
1175
+ }
1176
+
1177
+ .sp-tabs :deep(.v-tab) {
1178
+ min-width: 120px;
1179
+ justify-content: flex-start;
1180
+ text-transform: none;
1181
+ }
1182
+
1183
+ .sp-col-company {
1184
+ max-width: 220px;
1185
+ }
1186
+
1187
+ .sp-col-email {
1188
+ max-width: 260px;
1189
+ }
1190
+
1191
+ .sp-col-type {
1192
+ max-width: 180px;
1193
+ }
1194
+
1195
+ .sp-table :deep(thead th) {
1196
+ font-weight: 600;
1197
+ font-size: 0.8125rem;
1198
+ color: rgba(0, 0, 0, 0.7);
1199
+ }
1200
+
1201
+ .sp-table :deep(tbody td) {
1202
+ vertical-align: middle;
1203
+ }
1204
+
1205
+ .sp-invite-card {
1206
+ overflow: hidden;
1207
+ }
1208
+
1209
+ .sp-invite-body {
1210
+ max-height: 100vh;
1211
+ overflow-y: auto;
1212
+ padding: 48px 44px 34px;
1213
+ }
1214
+
1215
+ .sp-invite-body :deep(.v-field) {
1216
+ border-radius: 6px;
1217
+ }
1218
+
1219
+ .sp-invite-body :deep(.v-field__input) {
1220
+ min-height: 64px;
1221
+ font-size: 1.25rem;
1222
+ }
1223
+
1224
+ .sp-invite-footer {
1225
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
1226
+ }
1227
+
1228
+ .sp-invite-footer-btn {
1229
+ border-radius: 0;
1230
+ background: #ffffff !important;
1231
+ color: #111827 !important;
1232
+ font-size: 1.25rem;
1233
+ font-weight: 700;
1234
+ }
1235
+
1236
+ .sp-invite-submit-btn {
1237
+ border-radius: 0;
1238
+ background: #0b253b !important;
1239
+ color: #ffffff !important;
1240
+ font-size: 1.25rem;
1241
+ font-weight: 700;
1242
+ }
1243
+
1244
+ .sp-members-card {
1245
+ overflow: hidden;
1246
+ }
1247
+
1248
+ .sp-members-title {
1249
+ min-height: 56px;
1250
+ display: flex;
1251
+ align-items: center;
1252
+ padding: 0 16px;
1253
+ font-size: 1.25rem;
1254
+ font-weight: 700;
1255
+ }
1256
+
1257
+ .sp-members-table {
1258
+ max-height: 444px;
1259
+ overflow-y: auto;
1260
+ }
1261
+
1262
+ .sp-members-table :deep(th) {
1263
+ height: 64px;
1264
+ color: #667085;
1265
+ font-size: 1rem;
1266
+ font-weight: 500;
1267
+ }
1268
+
1269
+ .sp-members-table :deep(td) {
1270
+ height: 64px;
1271
+ color: #667085;
1272
+ font-size: 1rem;
1273
+ }
1274
+
1275
+ .sp-status-card {
1276
+ overflow: hidden;
1277
+ }
1278
+
1279
+ .sp-status-title {
1280
+ min-height: 82px;
1281
+ display: flex;
1282
+ align-items: center;
1283
+ padding: 0 24px;
1284
+ background: #f5f5f5;
1285
+ color: #101828;
1286
+ font-size: 1.75rem;
1287
+ font-weight: 700;
1288
+ }
1289
+
1290
+ .sp-status-body {
1291
+ padding: 44px 46px 42px;
1292
+ }
1293
+
1294
+ .sp-status-question {
1295
+ color: #101828;
1296
+ font-size: 1.5rem;
1297
+ font-weight: 700;
1298
+ line-height: 1.35;
1299
+ }
1300
+
1301
+ .sp-status-copy {
1302
+ margin-top: 28px;
1303
+ color: #667085;
1304
+ font-size: 1.5rem;
1305
+ line-height: 1.2;
1306
+ }
1307
+
1308
+ .sp-status-cancel {
1309
+ color: #101828;
1310
+ background: #ffffff;
1311
+ font-size: 1.5rem;
1312
+ font-weight: 700;
1313
+ }
1314
+
1315
+ .sp-status-confirm {
1316
+ color: #ffffff;
1317
+ background: #00263a;
1318
+ font-size: 1.5rem;
1319
+ font-weight: 700;
1320
+ }
1321
+ </style>
@@ -14,7 +14,7 @@
14
14
  :class="['ma-1', isSelected && 'selected']"
15
15
  width="150"
16
16
  height="100"
17
- @click="clickable && emit('onClickCarousel', data.path)"
17
+ @click="clickable && emit('onClickCarousel', data)"
18
18
  class="rounded-lg border-thin"
19
19
  border=" opacity-50 thin "
20
20
  >
@@ -57,10 +57,19 @@ export default function useServiceProvider() {
57
57
  serviceProviderOrgId = "",
58
58
  orgId = "",
59
59
  siteId = "",
60
+ status = "",
60
61
  } = {}) {
61
62
  return useNuxtApp().$api<Record<string, any>>("/api/service-providers", {
62
63
  method: "GET",
63
- query: { page, search, limit, serviceProviderOrgId, orgId, siteId },
64
+ query: {
65
+ page,
66
+ search,
67
+ limit,
68
+ serviceProviderOrgId,
69
+ orgId,
70
+ siteId,
71
+ ...(status ? { status } : {}),
72
+ },
64
73
  });
65
74
  }
66
75
 
@@ -128,32 +137,36 @@ export default function useServiceProvider() {
128
137
 
129
138
  async function createServiceProviderInvite({
130
139
  email,
140
+ name = "",
141
+ app = "",
142
+ role = "",
131
143
  orgId,
132
144
  siteId,
133
145
  siteName,
134
146
  }: {
135
147
  email: string;
148
+ name?: string;
149
+ app?: string;
150
+ role?: string;
136
151
  orgId: string;
137
152
  siteId: string;
138
153
  siteName: string;
139
154
  }) {
140
- try {
141
- const res = await useNuxtApp().$api<Record<string, any>>(
142
- "/api/auth/invite/service-provider",
143
- {
144
- method: "POST",
145
- body: {
146
- email,
147
- orgId,
148
- siteId,
149
- siteName,
150
- },
151
- }
152
- );
153
- console.log(">>> response:", res);
154
- } catch (err) {
155
- console.error("Error creating service provider invite:", err);
156
- }
155
+ return useNuxtApp().$api<Record<string, any>>(
156
+ "/api/auth/invite/service-provider/with-org",
157
+ {
158
+ method: "POST",
159
+ body: {
160
+ email,
161
+ name,
162
+ app,
163
+ role,
164
+ orgId,
165
+ siteId,
166
+ siteName,
167
+ },
168
+ }
169
+ );
157
170
  }
158
171
 
159
172
  async function invite({
@@ -211,6 +224,16 @@ export default function useServiceProvider() {
211
224
  );
212
225
  }
213
226
 
227
+ async function updateStatus(id: string, status: "active" | "inactive") {
228
+ return useNuxtApp().$api<Record<string, any>>(
229
+ `/api/service-providers/${id}/status`,
230
+ {
231
+ method: "PATCH",
232
+ body: { status },
233
+ }
234
+ );
235
+ }
236
+
214
237
  return {
215
238
  serviceProviders,
216
239
  serviceProvider,
@@ -228,5 +251,6 @@ export default function useServiceProvider() {
228
251
  invite,
229
252
  add,
230
253
  addViaInvite,
254
+ updateStatus,
231
255
  };
232
256
  }
@@ -5,7 +5,10 @@ export default function useVerification() {
5
5
  search = "",
6
6
  page = 1,
7
7
  email = "",
8
- app = ""
8
+ app = "",
9
+ siteId = "",
10
+ orgId = "",
11
+ limit = 20,
9
12
  } = {}): Promise<{
10
13
  items: TMiniVerification[];
11
14
  pages: number;
@@ -13,7 +16,7 @@ export default function useVerification() {
13
16
  }> {
14
17
  return useNuxtApp().$api("/api/verifications", {
15
18
  method: "GET",
16
- query: { status, search, page, type, email, app },
19
+ query: { status, search, page, type, email, app, siteId, orgId, limit },
17
20
  });
18
21
  }
19
22
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.11.43",
5
+ "version": "1.11.44",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {