@7365admin1/layer-common 3.0.19 → 3.0.20

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
+ ## 3.0.20
4
+
5
+ ### Patch Changes
6
+
7
+ - ea81bba: add dialog confirm for client and update service provider
8
+
3
9
  ## 3.0.19
4
10
 
5
11
  ### Patch Changes
@@ -26,11 +26,15 @@
26
26
  v-model:utc="startDate"
27
27
  density="compact"
28
28
  hide-details
29
+ :date-only="true"
30
+ placeholder="Start Date"
29
31
  />
30
32
  <InputDateTimePicker
31
33
  v-model:utc="endDate"
32
34
  density="compact"
33
35
  hide-details
36
+ :date-only="true"
37
+ placeholder="End Date"
34
38
  />
35
39
  <v-select
36
40
  v-model="status"
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="page-wrap">
3
3
 
4
- <!-- ===== TABLE VIEW: topbar + table đi chung 1 khối ===== -->
4
+ <!-- ===== TABLE VIEW ===== -->
5
5
  <template v-if="viewMode === 'table'">
6
6
 
7
7
  <!-- ===== TOP BAR ===== -->
@@ -24,7 +24,7 @@
24
24
  </svg>
25
25
  </div>
26
26
 
27
- <!-- Filters: chỉ hiện ở active/suspended -->
27
+ <!-- Filters -->
28
28
  <div v-if="tab !== 'pending'" class="filter-wrap">
29
29
  <div class="filter-field">
30
30
  <label>Filter by status</label>
@@ -104,7 +104,7 @@
104
104
  <div v-if="loading" class="loading-bar"><div class="loading-bar-fill" /></div>
105
105
 
106
106
  <!-- TABLE: active / suspended -->
107
- <div v-if="tab !== 'pending'" class="table-scroll">
107
+ <div v-if="tab !== 'pending'" ref="tableScrollEl" class="table-scroll" @scroll="closeMenu">
108
108
  <table>
109
109
  <thead>
110
110
  <tr>
@@ -156,32 +156,9 @@
156
156
  </td>
157
157
  <td>
158
158
  <div class="action-wrap">
159
- <button class="dots-btn" @click.stop="toggleMenu(item._id)">
159
+ <button class="dots-btn" @click.stop="toggleMenu(item._id, $event)">
160
160
  <span /><span /><span />
161
161
  </button>
162
- <div v-if="openMenuId === item._id" class="dropdown">
163
- <button
164
- class="dd-item"
165
- @click="viewClient(item)"
166
- >
167
- View Organization
168
- </button>
169
- <button
170
- v-if="item.status === 'suspended'"
171
- class="dd-item"
172
- @click="activateClient(item)"
173
- >
174
- Activate
175
- </button>
176
-
177
- <button
178
- v-else
179
- class="dd-item danger"
180
- @click="suspendClient(item)"
181
- >
182
- Suspend
183
- </button>
184
- </div>
185
162
  </div>
186
163
  </td>
187
164
  </tr>
@@ -228,7 +205,6 @@
228
205
 
229
206
  </template>
230
207
 
231
- <!-- ===== DETAIL VIEW: chiếm toàn bộ page-wrap, không còn topbar/table ===== -->
232
208
  <div v-else-if="viewMode === 'detail'" class="client-detail">
233
209
  <ClientDetailForm
234
210
  :client="selectedClient"
@@ -237,17 +213,59 @@
237
213
  />
238
214
  </div>
239
215
 
240
- <!-- ===== DIALOG (luôn nằm ngoài, có thể mở ở bất kỳ view nào) ===== -->
241
216
  <div v-if="dialog" class="overlay" @click.self="dialog = false">
242
217
  <div class="dialog-box">
243
218
  <InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
244
219
  </div>
245
220
  </div>
221
+
222
+ <!-- ===== SUSPEND CONFIRMATION DIALOG ===== -->
223
+ <div v-if="confirmSuspendItem" class="overlay" @click.self="cancelSuspend">
224
+ <div class="confirm-box">
225
+ <h3 class="confirm-title">Suspend account</h3>
226
+ <p class="confirm-text">
227
+ Are you sure you want to suspend <strong>{{ confirmSuspendItem?.name }}</strong>'s account?
228
+ </p>
229
+ <div class="confirm-actions">
230
+ <button class="btn-cancel" @click="cancelSuspend">Cancel</button>
231
+ <button class="btn-confirm-danger" :disabled="confirmSuspendLoading" @click="confirmSuspend">
232
+ <span v-if="confirmSuspendLoading" class="spinner spinner-light" />
233
+ <span v-else>Suspend</span>
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <Teleport to="body">
240
+ <div
241
+ v-if="openMenuId"
242
+ class="dropdown"
243
+ :style="{ top: menuPos.top + 'px', left: menuPos.left + 'px' }"
244
+ >
245
+ <button class="dd-item" @click="viewClient(activeMenuItem)">
246
+ View Organization
247
+ </button>
248
+ <button
249
+ v-if="activeMenuItem?.status === 'suspended'"
250
+ class="dd-item"
251
+ @click="activateClient(activeMenuItem)"
252
+ >
253
+ Activate
254
+ </button>
255
+ <button
256
+ v-else
257
+ class="dd-item danger"
258
+ @click="askSuspend(activeMenuItem)"
259
+ >
260
+ Suspend
261
+ </button>
262
+ </div>
263
+ </Teleport>
246
264
  </div>
247
265
  </template>
248
266
 
249
267
  <script setup lang="ts">
250
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
268
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
251
269
  import useOrg from '../composables/useOrg'
252
270
  import useVerification from '../composables/useVerification'
253
271
  import useRole from '../composables/useRole'
@@ -283,6 +301,9 @@ const APP_LIST = [
283
301
  const tab = ref<TabStatus>('active')
284
302
  const dialog = ref(false)
285
303
  const openMenuId = ref<string | null>(null)
304
+ const menuPos = ref({ top: 0, left: 0 })
305
+ const tableScrollEl = ref<HTMLElement | null>(null)
306
+
286
307
  // filterStatus
287
308
  const filterStatus = ref<'active' | 'suspended'>('active')
288
309
  const filterPlan = ref('') // '' = All | any APP_LIST value
@@ -300,6 +321,10 @@ const roleCache = ref<Record<string, string>>({})
300
321
  const viewMode = ref<'table' | 'detail'>('table')
301
322
  const selectedClient = ref<any>(null)
302
323
 
324
+ /* suspend confirmation */
325
+ const confirmSuspendItem = ref<any>(null)
326
+ const confirmSuspendLoading = ref(false)
327
+
303
328
  /* ── COMPUTED ── */
304
329
  const pageRange = computed(() => {
305
330
  if (!totalItems.value) return '0-0 of 0'
@@ -308,6 +333,10 @@ const pageRange = computed(() => {
308
333
  return `${start}-${end} of ${totalItems.value}`
309
334
  })
310
335
 
336
+ const activeMenuItem = computed(() =>
337
+ items.value.find(i => i._id === openMenuId.value) ?? null
338
+ )
339
+
311
340
  /* ── HELPERS ── */
312
341
  function formatDate(date?: string | Date) {
313
342
  if (date == null || date === '') return '-'
@@ -358,6 +387,28 @@ async function activateClient(item: any) {
358
387
  }
359
388
  }
360
389
 
390
+ /* ── SUSPEND CONFIRMATION ── */
391
+ function askSuspend(item: any) {
392
+ confirmSuspendItem.value = item
393
+ closeMenu()
394
+ }
395
+
396
+ function cancelSuspend() {
397
+ if (confirmSuspendLoading.value) return
398
+ confirmSuspendItem.value = null
399
+ }
400
+
401
+ async function confirmSuspend() {
402
+ if (!confirmSuspendItem.value) return
403
+ confirmSuspendLoading.value = true
404
+ try {
405
+ await suspendClient(confirmSuspendItem.value)
406
+ confirmSuspendItem.value = null
407
+ } finally {
408
+ confirmSuspendLoading.value = false
409
+ }
410
+ }
411
+
361
412
  async function fetchOrgs(status: 'active' | 'suspended') {
362
413
  loading.value = true
363
414
 
@@ -484,7 +535,6 @@ async function handleUpdatePage(p: number) {
484
535
 
485
536
  async function onTabChange(value: TabStatus) {
486
537
  tab.value = value
487
- // sync filterStatus ( active/suspended)
488
538
  if (value !== 'pending') filterStatus.value = value
489
539
  page.value = 1
490
540
  await fetchData()
@@ -509,14 +559,44 @@ function handleFilterChange() {
509
559
  fetchData()
510
560
  }
511
561
 
512
- /* ── MENU ── */
513
- function toggleMenu(id: string) {
514
- openMenuId.value = openMenuId.value === id ? null : id
562
+ const DROPDOWN_WIDTH = 130
563
+
564
+ function toggleMenu(id: string, event: MouseEvent) {
565
+ if (openMenuId.value === id) {
566
+ closeMenu()
567
+ return
568
+ }
569
+ const btn = event.currentTarget as HTMLElement
570
+ const rect = btn.getBoundingClientRect()
571
+
572
+ let left = rect.right - DROPDOWN_WIDTH
573
+ if (left < 8) left = 8
574
+ if (left + DROPDOWN_WIDTH > window.innerWidth - 8) {
575
+ left = window.innerWidth - DROPDOWN_WIDTH - 8
576
+ }
577
+
578
+ menuPos.value = {
579
+ top: rect.bottom + 4,
580
+ left,
581
+ }
582
+ openMenuId.value = id
515
583
  }
584
+
516
585
  function closeMenu() {
517
586
  openMenuId.value = null
518
587
  }
519
588
 
589
+ function handleOutsideClick(e: MouseEvent) {
590
+ const target = e.target as HTMLElement
591
+ if (!target.closest('.dropdown') && !target.closest('.dots-btn')) {
592
+ closeMenu()
593
+ }
594
+ }
595
+
596
+ function handleWindowResize() {
597
+ closeMenu()
598
+ }
599
+
520
600
  /* ── DIALOG ── */
521
601
  function openInviteDialog() { dialog.value = true }
522
602
  function handleSuccess() { dialog.value = false; fetchData() }
@@ -532,7 +612,18 @@ async function cancelInvite(item: any) {
532
612
  }
533
613
  }
534
614
 
535
- onMounted(() => fetchData())
615
+ onMounted(() => {
616
+ fetchData()
617
+ document.addEventListener('click', handleOutsideClick)
618
+ document.addEventListener('scroll', closeMenu, true)
619
+ window.addEventListener('resize', handleWindowResize)
620
+ })
621
+
622
+ onUnmounted(() => {
623
+ document.removeEventListener('click', handleOutsideClick)
624
+ document.removeEventListener('scroll', closeMenu, true)
625
+ window.removeEventListener('resize', handleWindowResize)
626
+ })
536
627
  </script>
537
628
 
538
629
  <style scoped>
@@ -823,15 +914,13 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
823
914
  .dots-btn span { display: block; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
824
915
 
825
916
  .dropdown {
826
- position: absolute;
827
- right: 0;
828
- top: calc(100% + 4px);
917
+ position: fixed;
829
918
  background: #fff;
830
919
  border: 1px solid #E5E7EB;
831
920
  border-radius: 8px;
832
921
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
833
922
  min-width: 130px;
834
- z-index: 50;
923
+ z-index: 1100;
835
924
  overflow: hidden;
836
925
  }
837
926
  .dd-item {
@@ -874,6 +963,10 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
874
963
  border-radius: 50%;
875
964
  animation: spin 0.7s linear infinite;
876
965
  }
966
+ .spinner-light {
967
+ border: 1.5px solid rgba(255,255,255,0.5);
968
+ border-top-color: #fff;
969
+ }
877
970
 
878
971
  .client-detail {
879
972
  margin: 12px 0 0 0;
@@ -896,4 +989,61 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
896
989
  box-shadow: 0 16px 48px rgba(0,0,0,0.12);
897
990
  overflow: hidden;
898
991
  }
992
+
993
+ /* ===== SUSPEND CONFIRMATION DIALOG ===== */
994
+ .confirm-box {
995
+ background: #fff;
996
+ border-radius: 12px;
997
+ max-width: 380px;
998
+ width: 90%;
999
+ box-shadow: 0 16px 48px rgba(0,0,0,0.12);
1000
+ padding: 24px;
1001
+ }
1002
+ .confirm-title {
1003
+ font-size: 16px;
1004
+ font-weight: 600;
1005
+ color: #111827;
1006
+ margin-bottom: 8px;
1007
+ }
1008
+ .confirm-text {
1009
+ font-size: 13.5px;
1010
+ color: #4B5563;
1011
+ line-height: 1.5;
1012
+ margin-bottom: 20px;
1013
+ }
1014
+ .confirm-actions {
1015
+ display: flex;
1016
+ justify-content: flex-end;
1017
+ gap: 10px;
1018
+ }
1019
+ .btn-cancel {
1020
+ padding: 8px 16px;
1021
+ font-size: 13px;
1022
+ font-weight: 500;
1023
+ border: 1px solid #D1D5DB;
1024
+ border-radius: 8px;
1025
+ background: #fff;
1026
+ color: #374151;
1027
+ cursor: pointer;
1028
+ transition: background 0.15s;
1029
+ }
1030
+ .btn-cancel:hover { background: #F9FAFB; }
1031
+ .btn-confirm-danger {
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ justify-content: center;
1035
+ gap: 6px;
1036
+ min-width: 78px;
1037
+ padding: 8px 16px;
1038
+ font-size: 13px;
1039
+ font-weight: 500;
1040
+ border: none;
1041
+ border-radius: 8px;
1042
+ background: #DC2626;
1043
+ color: #fff;
1044
+ cursor: pointer;
1045
+ transition: background 0.15s;
1046
+ }
1047
+ .btn-confirm-danger:hover:not(:disabled) { background: #B91C1C; }
1048
+ .btn-confirm-danger:disabled { opacity: 0.7; cursor: not-allowed; }
899
1049
  </style>
@@ -824,13 +824,7 @@
824
824
  <div class="dashboard-card dashboard-panel figma-panel">
825
825
  <div class="figma-panel-header">
826
826
  <h2>Today's Attentions</h2>
827
- <button
828
- class="figma-panel-action"
829
- type="button"
830
- @click="goToDashboardRoute('incident-reports')"
831
- >
832
- View All
833
- </button>
827
+
834
828
  </div>
835
829
  <div class="figma-panel-body">
836
830
  <div v-if="todayAttentions.length" class="figma-list">
@@ -1,186 +1,201 @@
1
1
  <template>
2
- <div class="d-flex flex-column">
3
- <v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly" autocomplete="off"
4
- :placeholder="inputPlaceholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
5
- <template #append-inner>
6
- <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
7
- </template>
8
- </v-text-field>
9
- <div class="w-100 d-flex align-end ga-3 hidden-input">
10
- <input ref="dateInput" :type="inputType" v-model="dateTime" :min="minValue" />
11
- </div>
2
+ <div class="d-flex flex-column">
3
+ <v-text-field
4
+ v-bind="$attrs"
5
+ ref="dateTimePickerRef"
6
+ :model-value="dateTimeFormattedReadOnly"
7
+ autocomplete="off"
8
+ :placeholder="inputPlaceholder"
9
+ :rules="rules"
10
+ style="z-index: 10"
11
+ @click="openDatePicker"
12
+ >
13
+ <template #append-inner>
14
+ <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
15
+ </template>
16
+ </v-text-field>
17
+ <div class="w-100 d-flex align-end ga-3 hidden-input">
18
+ <input
19
+ ref="dateInput"
20
+ :type="inputType"
21
+ v-model="dateTime"
22
+ :min="minValue"
23
+ />
12
24
  </div>
25
+ </div>
13
26
  </template>
14
27
 
15
28
  <script setup lang="ts">
16
- import { ref, computed, watch } from 'vue'
29
+ import { ref, computed, watch } from "vue";
17
30
 
18
31
  const prop = defineProps({
19
- rules: {
20
- type: Array as PropType<Array<any>>,
21
- default: () => []
22
- },
23
- placeholder: {
24
- type: String,
25
- default: 'DD/MM/YYYY, HH:MM AM/PM'
26
- },
27
- dateOnly: {
28
- type: Boolean,
29
- default: false
30
- },
31
- min: {
32
- type: String,
33
- default: undefined
34
- }
35
- })
36
-
37
- const { formatDateISO8601 } = useUtils()
38
- const dateTime = defineModel<string | null>({ default: null }) //2025-10-10T13:09 format
39
- const dateTimeUTC = defineModel<string | null>('utc', { default: null }) // UTC format
40
-
41
- const dateTimeFormattedReadOnly = ref<string | null>(null)
42
- const inputType = computed(() => (prop.dateOnly ? 'date' : 'datetime-local'))
43
-
32
+ rules: {
33
+ type: Array as PropType<Array<any>>,
34
+ default: () => [],
35
+ },
36
+ placeholder: {
37
+ type: String,
38
+ default: "DD/MM/YYYY, HH:MM AM/PM",
39
+ },
40
+ dateOnly: {
41
+ type: Boolean,
42
+ default: false,
43
+ },
44
+ min: {
45
+ type: String,
46
+ default: undefined,
47
+ },
48
+ });
49
+
50
+ const { formatDateISO8601 } = useUtils();
51
+ const dateTime = defineModel<string | null>({ default: null }); //2025-10-10T13:09 format
52
+ const dateTimeUTC = defineModel<string | null>("utc", { default: null }); // UTC format
53
+
54
+ const dateTimeFormattedReadOnly = ref<string | null>(null);
55
+ const inputType = computed(() => (prop.dateOnly ? "date" : "datetime-local"));
44
56
 
45
57
  const minValue = computed(() => {
46
- if (!prop.min) return undefined
47
- if (/^\d{4}-\d{2}-\d{2}$/.test(prop.min)) {
48
- return prop.dateOnly ? prop.min : `${prop.min}T00:00`
49
- }
50
- return prop.min
51
- })
52
- const inputPlaceholder = computed(() => (
53
- prop.placeholder || (prop.dateOnly ? 'MM/DD/YYYY' : 'DD/MM/YYYY, HH:MM AM/PM')
54
- ))
55
-
56
-
57
-
58
-
59
- const dateInput = ref<HTMLInputElement | null>(null)
60
- const dateTimePickerRef = ref<HTMLInputElement | null>(null)
61
-
62
- const isInitialLoad = ref(true)
58
+ if (!prop.min) return undefined;
59
+ if (/^\d{4}-\d{2}-\d{2}$/.test(prop.min)) {
60
+ return prop.dateOnly ? prop.min : `${prop.min}T00:00`;
61
+ }
62
+ return prop.min;
63
+ });
64
+ const inputPlaceholder = computed(
65
+ () =>
66
+ prop.placeholder ||
67
+ (prop.dateOnly ? "MM/DD/YYYY" : "DD/MM/YYYY, HH:MM AM/PM")
68
+ );
69
+
70
+ const dateInput = ref<HTMLInputElement | null>(null);
71
+ const dateTimePickerRef = ref<HTMLInputElement | null>(null);
72
+
73
+ const isInitialLoad = ref(true);
63
74
 
64
75
  function openDatePicker() {
65
- setTimeout(() => {
66
- dateInput.value?.showPicker?.()
67
- }, 0)
76
+ setTimeout(() => {
77
+ dateInput.value?.showPicker?.();
78
+ }, 0);
68
79
  }
69
80
 
70
81
  function validate() {
71
- (dateTimePickerRef.value as any)?.validate()
82
+ (dateTimePickerRef.value as any)?.validate();
72
83
  }
73
84
 
74
85
  function convertToReadableFormat(dateStr: string): string {
86
+ if (!dateStr) return "";
75
87
 
76
- if (!dateStr) return "";
88
+ const date = new Date(dateStr);
89
+ if (Number.isNaN(date.getTime())) return "";
77
90
 
78
- const date = new Date(dateStr)
79
- if (Number.isNaN(date.getTime())) return "";
91
+ const month = String(date.getMonth() + 1).padStart(2, "0");
92
+ const day = String(date.getDate()).padStart(2, "0");
93
+ const year = date.getFullYear();
80
94
 
81
- const month = String(date.getMonth() + 1).padStart(2, '0')
82
- const day = String(date.getDate()).padStart(2, '0')
83
- const year = date.getFullYear()
95
+ if (prop.dateOnly) {
96
+ return `${day}/${month}/${year}`;
97
+ }
84
98
 
85
- if (prop.dateOnly) {
86
- return `${month}/${day}/${year}`
87
- }
99
+ let hours = date.getHours();
100
+ const minutes = String(date.getMinutes()).padStart(2, "0");
88
101
 
89
- let hours = date.getHours()
90
- const minutes = String(date.getMinutes()).padStart(2, '0')
102
+ const ampm = hours >= 12 ? "PM" : "AM";
103
+ hours = hours % 12;
104
+ hours = hours ? hours : 12;
91
105
 
92
- const ampm = hours >= 12 ? 'PM' : 'AM'
93
- hours = hours % 12
94
- hours = hours ? hours : 12
106
+ const formattedTime = `${String(hours).padStart(2, "0")}:${minutes} ${ampm}`;
95
107
 
96
- const formattedTime = `${String(hours).padStart(2, '0')}:${minutes} ${ampm}`
97
-
98
- return `${day}/${month}/${year}, ${formattedTime}`
108
+ return `${day}/${month}/${year}, ${formattedTime}`;
99
109
  }
100
110
 
101
111
  function toDateOnlyInputValue(dateStr: string): string {
102
- if (!dateStr) return ''
103
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr
104
- const date = new Date(dateStr)
105
- if (Number.isNaN(date.getTime())) return ''
106
- const year = date.getFullYear()
107
- const month = String(date.getMonth() + 1).padStart(2, '0')
108
- const day = String(date.getDate()).padStart(2, '0')
109
- return `${year}-${month}-${day}`
112
+ if (!dateStr) return "";
113
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
114
+ const date = new Date(dateStr);
115
+ if (Number.isNaN(date.getTime())) return "";
116
+ const year = date.getFullYear();
117
+ const month = String(date.getMonth() + 1).padStart(2, "0");
118
+ const day = String(date.getDate()).padStart(2, "0");
119
+ return `${year}-${month}-${day}`;
110
120
  }
111
121
 
112
- function handleInitialDate(){
113
- const dateDefault = dateTime.value
114
- const dateUTC = dateTimeUTC.value
115
- if(dateDefault){
116
- dateTimeFormattedReadOnly.value = convertToReadableFormat(dateDefault)
117
- if (prop.dateOnly) {
118
- dateTimeUTC.value = toDateOnlyInputValue(dateDefault)
119
- } else {
120
- const localDate = new Date(dateDefault)
121
- dateTimeUTC.value = localDate.toISOString()
122
- }
123
- } else if (dateUTC){
124
- dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC)
125
- if (prop.dateOnly) {
126
- dateTime.value = toDateOnlyInputValue(dateUTC)
127
- } else {
128
- const localDate = new Date(dateUTC)
129
- dateTime.value = formatDateISO8601(localDate)
130
- }
122
+ function handleInitialDate() {
123
+ const dateDefault = dateTime.value;
124
+ const dateUTC = dateTimeUTC.value;
125
+ if (dateDefault) {
126
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateDefault);
127
+ if (prop.dateOnly) {
128
+ dateTimeUTC.value = toDateOnlyInputValue(dateDefault);
129
+ } else {
130
+ const localDate = new Date(dateDefault);
131
+ dateTimeUTC.value = localDate.toISOString();
132
+ }
133
+ } else if (dateUTC) {
134
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC);
135
+ if (prop.dateOnly) {
136
+ dateTime.value = toDateOnlyInputValue(dateUTC);
131
137
  } else {
132
- dateTimeFormattedReadOnly.value = null
138
+ const localDate = new Date(dateUTC);
139
+ dateTime.value = formatDateISO8601(localDate);
133
140
  }
141
+ } else {
142
+ dateTimeFormattedReadOnly.value = null;
143
+ }
134
144
  }
135
145
 
136
-
137
- watch(dateTime, (dateVal) => {
138
- if (isInitialLoad.value) return // ignore the first run
146
+ watch(
147
+ dateTime,
148
+ (dateVal) => {
149
+ if (isInitialLoad.value) return; // ignore the first run
139
150
  if (!dateVal) {
140
- dateTimeFormattedReadOnly.value = null;
141
- dateTimeUTC.value = null
142
- return
151
+ dateTimeFormattedReadOnly.value = null;
152
+ dateTimeUTC.value = null;
153
+ return;
143
154
  }
144
155
 
145
- dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal)
156
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal);
146
157
  if (prop.dateOnly) {
147
- dateTimeUTC.value = toDateOnlyInputValue(dateVal)
158
+ dateTimeUTC.value = toDateOnlyInputValue(dateVal);
148
159
  } else {
149
- const localDate = new Date(dateVal)
150
- dateTimeUTC.value = localDate.toISOString()
160
+ const localDate = new Date(dateVal);
161
+ dateTimeUTC.value = localDate.toISOString();
151
162
  }
152
-
153
- }, { immediate: false })
154
-
155
- watch(dateTimeUTC, () => {
156
- handleInitialDate()
157
- }, { immediate: true})
158
-
163
+ },
164
+ { immediate: false }
165
+ );
166
+
167
+ watch(
168
+ dateTimeUTC,
169
+ () => {
170
+ handleInitialDate();
171
+ },
172
+ { immediate: true }
173
+ );
159
174
 
160
175
  onMounted(async () => {
161
- await nextTick()
162
- isInitialLoad.value = false
163
- // Wait until Vuetify renders its internal input
164
- const nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector('input')
165
- if (nativeInput) {
166
- nativeInput.addEventListener('click', (e: MouseEvent) => {
167
- e.stopPropagation()
168
- openDatePicker()
169
- })
170
- }
171
-
172
- })
173
-
176
+ await nextTick();
177
+ isInitialLoad.value = false;
178
+ // Wait until Vuetify renders its internal input
179
+ const nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector(
180
+ "input"
181
+ );
182
+ if (nativeInput) {
183
+ nativeInput.addEventListener("click", (e: MouseEvent) => {
184
+ e.stopPropagation();
185
+ openDatePicker();
186
+ });
187
+ }
188
+ });
174
189
 
175
190
  defineExpose({
176
- validate
177
- })
191
+ validate,
192
+ });
178
193
  </script>
179
194
 
180
195
  <style scoped>
181
196
  .hidden-input {
182
- opacity: 0;
183
- height: 0;
184
- width: 1px;
197
+ opacity: 0;
198
+ height: 0;
199
+ width: 1px;
185
200
  }
186
201
  </style>
@@ -732,6 +732,7 @@ const {
732
732
  add: addServiceProvider,
733
733
  createServiceProviderInvite,
734
734
  updateStatus: updateServiceProviderStatus,
735
+ invite
735
736
  } = useServiceProvider();
736
737
  const { getAll: getAllMembers } = useMember();
737
738
 
@@ -759,9 +760,10 @@ const createInviteDialog = ref(false);
759
760
  // const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
760
761
 
761
762
  const { getByUserType } = useMember();
763
+ const { getOrgs } = useOrg();
762
764
 
763
765
  async function checkExistingAccount(email: string) {
764
- // existingAccount.value = null;
766
+ existingAccount.value = null;
765
767
 
766
768
  if (!email.trim() || emailRule(email) !== true) return;
767
769
 
@@ -772,19 +774,23 @@ async function checkExistingAccount(email: string) {
772
774
 
773
775
  if (!user?._id) return;
774
776
 
775
- const member = await getByUserType(user._id, "organization");
776
- console.log("member raw:", member);
777
- console.log("onboardingCompleted:", member?.onboardingCompleted);
777
+ const orgs = await getOrgs({
778
+ user: user._id,
779
+ });
780
+
781
+ const matchedOrg = orgs.items?.find(
782
+ (org: any) => org.type === serviceProviderInvite.value.app
783
+ );
784
+
785
+ if (matchedOrg) {
786
+ existingAccount.value = {
787
+ _id: matchedOrg.value,
788
+ text: matchedOrg.text,
789
+ ...matchedOrg,
790
+ };
791
+ }
778
792
 
779
- if (member?.onboardingCompleted) {
780
- existingAccount.value = {
781
- _id: member.org,
782
- text: member.orgName,
783
- ...member,
784
- };
785
- }
786
793
  } catch (error) {
787
- console.error(error);
788
794
  existingAccount.value = null;
789
795
  } finally {
790
796
  checkingExistingAccount.value = false;
@@ -799,6 +805,14 @@ watch(
799
805
  await checkExistingAccount(value);
800
806
  }
801
807
  );
808
+ watch(
809
+ () => serviceProviderInvite.value.app,
810
+ () => {
811
+ if (serviceProviderInvite.value.email) {
812
+ checkExistingAccount(serviceProviderInvite.value.email);
813
+ }
814
+ }
815
+ );
802
816
 
803
817
  const serviceProviderInviteApps = computed(() =>
804
818
  natureOfBusinessV3.map((item) => ({
@@ -1183,33 +1197,42 @@ async function submitServiceProviderAdd() {
1183
1197
  disableServiceProvider.value = false;
1184
1198
  }
1185
1199
  }
1186
-
1187
1200
  async function submitServiceProviderInvite() {
1188
1201
  disableServiceProvider.value = true;
1189
1202
  messageServiceProvider.value = "";
1203
+
1190
1204
  try {
1191
- const payload = {
1205
+ await invite({
1192
1206
  email: serviceProviderInvite.value.email.trim(),
1193
- name: "",
1194
- app: serviceProviderInvite.value.app,
1195
1207
  orgId: props.orgId,
1196
1208
  siteId: serviceProviderInvite.value.siteId,
1197
1209
  siteName:
1198
- serviceProviderInvite.value.siteName || site.value?.name || props.siteName,
1199
- };
1210
+ serviceProviderInvite.value.siteName ||
1211
+ site.value?.name ||
1212
+ props.siteName,
1213
+
1214
+ inviteType: existingAccount.value
1215
+ ? "create-org"
1216
+ : "organization-invite",
1217
+ });
1200
1218
 
1201
- await createServiceProviderInvite(payload);
1202
1219
  showMessage("Invitation sent.", "success");
1220
+
1203
1221
  if (createMoreServiceProvider.value) {
1204
1222
  serviceProviderInvite.value.email = "";
1205
1223
  } else {
1206
- await setServiceProvider({ mode: "invite", dialog: false });
1224
+ await setServiceProvider({
1225
+ mode: "invite",
1226
+ dialog: false,
1227
+ });
1207
1228
  }
1229
+
1208
1230
  await loadList();
1209
1231
  } catch (error: any) {
1210
1232
  messageServiceProvider.value =
1211
1233
  error?.response?._data?.message ??
1212
1234
  "An error occurred while inviting the service provider.";
1235
+
1213
1236
  showMessage(errorConverter(error), "error");
1214
1237
  } finally {
1215
1238
  disableServiceProvider.value = false;
@@ -174,12 +174,13 @@ export default function useServiceProvider() {
174
174
  orgId = "",
175
175
  siteId = "",
176
176
  siteName = "",
177
+ inviteType=""
177
178
  } = {}) {
178
179
  return useNuxtApp().$api<Record<string, any>>(
179
180
  "/api/service-providers/invite",
180
181
  {
181
182
  method: "POST",
182
- body: { email, orgId, siteId, siteName },
183
+ body: { email, orgId, siteId, siteName, inviteType },
183
184
  }
184
185
  );
185
186
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.19",
5
+ "version": "3.0.20",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {