@7365admin1/layer-common 3.0.19 → 3.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/CleaningScheduleMain.vue +4 -0
- package/components/ClientMain.vue +189 -39
- package/components/DashboardMain.vue +1 -7
- package/components/EquipmentItemMain.vue +151 -47
- package/components/EquipmentManagementMain.vue +8 -1
- package/components/HidIntercomManagement.vue +1890 -0
- package/components/HygieneUpdateMoreAction.vue +7 -2
- package/components/Input/DateTimePicker.vue +152 -137
- package/components/MemberMain.vue +37 -6
- package/components/ServiceProviderMain.vue +43 -20
- package/composables/useHidAmico.ts +22 -0
- package/composables/useHidNavigation.ts +7 -0
- package/composables/useServiceProvider.ts +2 -1
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 3.0.21
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 75ae45e: Update Layer-common version
|
|
8
|
+
|
|
9
|
+
## 3.0.20
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ea81bba: add dialog confirm for client and update service provider
|
|
14
|
+
|
|
3
15
|
## 3.0.19
|
|
4
16
|
|
|
5
17
|
### 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
|
|
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">
|