@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 +6 -0
- package/components/CleaningScheduleMain.vue +4 -0
- package/components/ClientMain.vue +189 -39
- package/components/DashboardMain.vue +1 -7
- package/components/Input/DateTimePicker.vue +152 -137
- package/components/ServiceProviderMain.vue +43 -20
- package/composables/useServiceProvider.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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(() =>
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
29
|
+
import { ref, computed, watch } from "vue";
|
|
17
30
|
|
|
18
31
|
const prop = defineProps({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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>(
|
|
40
|
-
|
|
41
|
-
const dateTimeFormattedReadOnly = ref<string | null>(null)
|
|
42
|
-
const inputType = computed(() => (prop.dateOnly ?
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
const inputPlaceholder = computed(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
dateInput.value?.showPicker?.();
|
|
78
|
+
}, 0);
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
function validate() {
|
|
71
|
-
|
|
82
|
+
(dateTimePickerRef.value as any)?.validate();
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
function convertToReadableFormat(dateStr: string): string {
|
|
86
|
+
if (!dateStr) return "";
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
const date = new Date(dateStr);
|
|
89
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
if (prop.dateOnly) {
|
|
96
|
+
return `${day}/${month}/${year}`;
|
|
97
|
+
}
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
99
|
+
let hours = date.getHours();
|
|
100
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
const ampm = hours >= 12 ? "PM" : "AM";
|
|
103
|
+
hours = hours % 12;
|
|
104
|
+
hours = hours ? hours : 12;
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
hours = hours % 12
|
|
94
|
-
hours = hours ? hours : 12
|
|
106
|
+
const formattedTime = `${String(hours).padStart(2, "0")}:${minutes} ${ampm}`;
|
|
95
107
|
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
146
|
+
watch(
|
|
147
|
+
dateTime,
|
|
148
|
+
(dateVal) => {
|
|
149
|
+
if (isInitialLoad.value) return; // ignore the first run
|
|
139
150
|
if (!dateVal) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
158
|
+
dateTimeUTC.value = toDateOnlyInputValue(dateVal);
|
|
148
159
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
const localDate = new Date(dateVal);
|
|
161
|
+
dateTimeUTC.value = localDate.toISOString();
|
|
151
162
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
177
|
-
})
|
|
191
|
+
validate,
|
|
192
|
+
});
|
|
178
193
|
</script>
|
|
179
194
|
|
|
180
195
|
<style scoped>
|
|
181
196
|
.hidden-input {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
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 ||
|
|
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({
|
|
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
|
}
|