@7365admin1/layer-common 3.0.27 → 3.0.28
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/AccessCardPreviewDialog.vue +6 -3
- package/components/BulletinBoardView.vue +368 -288
- package/components/VisitorForm.vue +229 -16
- package/composables/useVisitor.ts +29 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">
|
|
42
42
|
Person <span class="text-error">*</span>
|
|
43
43
|
</div>
|
|
44
|
-
<v-
|
|
44
|
+
<v-select
|
|
45
45
|
v-model="assignPerson"
|
|
46
46
|
:items="filteredPeopleItems"
|
|
47
47
|
density="compact"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
{{ personTypeLabel(item.raw) }}
|
|
64
64
|
</v-chip>
|
|
65
65
|
</template>
|
|
66
|
-
</v-
|
|
66
|
+
</v-select>
|
|
67
67
|
<p v-if="assignError" class="text-error text-caption mt-2">{{ assignError }}</p>
|
|
68
68
|
</v-card-text>
|
|
69
69
|
|
|
@@ -545,7 +545,10 @@ async function confirmAssign() {
|
|
|
545
545
|
cancelAssign();
|
|
546
546
|
emit("refresh");
|
|
547
547
|
} catch (e: any) {
|
|
548
|
-
const
|
|
548
|
+
const rawMsg = e?.data?.message ?? "";
|
|
549
|
+
const msg = rawMsg.includes("500")
|
|
550
|
+
? "EntryPass server is unavailable. Please contact your administrator."
|
|
551
|
+
: rawMsg || "Failed to assign. Please try again.";
|
|
549
552
|
assignError.value = msg;
|
|
550
553
|
showToast(msg, "error");
|
|
551
554
|
} finally {
|
|
@@ -1,337 +1,417 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
<v-card width="100%" :loading="processing">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
+
<span class="font-weight-bold text-subtitle-1 text-capitalize">
|
|
6
|
+
Preview Announcement
|
|
7
|
+
</span>
|
|
8
|
+
</v-row>
|
|
9
|
+
</v-toolbar>
|
|
10
|
+
|
|
11
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0 my-3">
|
|
12
|
+
<v-row no-gutters>
|
|
13
|
+
<template v-for="(label, key) in formattedFields" :key="key">
|
|
14
|
+
<v-col cols="12" class="mb-5">
|
|
15
|
+
<v-row no-gutters class="">
|
|
16
|
+
<v-col class="font-weight-bold">{{ label }}:</v-col>
|
|
17
|
+
|
|
18
|
+
<v-col cols="9" class="px-10">
|
|
19
|
+
<span v-if="key === 'content'" v-html="getFieldValue(key)" />
|
|
20
|
+
<span v-else-if="key === 'noExpiration'">
|
|
21
|
+
<BulletinExpirationChip
|
|
22
|
+
:value="getFieldValue(key) as boolean"
|
|
23
|
+
/>
|
|
7
24
|
</span>
|
|
25
|
+
<span v-else>
|
|
26
|
+
{{ getFieldValue(key) }}
|
|
27
|
+
</span>
|
|
28
|
+
</v-col>
|
|
8
29
|
</v-row>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<v-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<template #prepend>
|
|
71
|
-
<v-avatar size="36" rounded="sm" class="mr-3">
|
|
72
|
-
<v-img :src="getFileUrl(item._id)" cover>
|
|
73
|
-
<template #error>
|
|
74
|
-
<v-icon color="green" size="20">mdi-image</v-icon>
|
|
75
|
-
</template>
|
|
76
|
-
</v-img>
|
|
77
|
-
</v-avatar>
|
|
78
|
-
</template>
|
|
79
|
-
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
80
|
-
<v-list-item-subtitle class="text-caption">{{ item.file.type }}</v-list-item-subtitle>
|
|
81
|
-
<template #append>
|
|
82
|
-
<v-icon size="16" color="grey-lighten-1">mdi-eye-outline</v-icon>
|
|
83
|
-
</template>
|
|
84
|
-
</v-list-item>
|
|
85
|
-
</template>
|
|
86
|
-
</v-list>
|
|
87
|
-
</template>
|
|
88
|
-
|
|
89
|
-
<template v-if="filesArray.length > 0">
|
|
90
|
-
<div class="text-caption text-medium-emphasis mb-1" :class="{ 'mt-3': videosArray.length > 0 || photosArray.length > 0 }">Files ({{ filesArray.length }})</div>
|
|
91
|
-
<v-list density="compact" class="pa-0 rounded-lg border">
|
|
92
|
-
<template v-for="(item, i) in filesArray" :key="item._id">
|
|
93
|
-
<v-divider v-if="i > 0" />
|
|
94
|
-
<v-list-item
|
|
95
|
-
class="cursor-pointer"
|
|
96
|
-
@click="openFileViewer(item._id, item.file.name)"
|
|
97
|
-
>
|
|
98
|
-
<template #prepend>
|
|
99
|
-
<v-icon :color="getFileIconColor(item.file.type)" class="mr-3">{{ getFileIcon(item.file.type) }}</v-icon>
|
|
100
|
-
</template>
|
|
101
|
-
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
102
|
-
<v-list-item-subtitle class="text-caption">{{ item.file.type }}</v-list-item-subtitle>
|
|
103
|
-
<template #append>
|
|
104
|
-
<v-icon size="16" color="grey-lighten-1">mdi-open-in-new</v-icon>
|
|
105
|
-
</template>
|
|
106
|
-
</v-list-item>
|
|
107
|
-
</template>
|
|
108
|
-
</v-list>
|
|
30
|
+
</v-col>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<v-col
|
|
34
|
+
cols="12"
|
|
35
|
+
v-if="prop.activeAnnouncement?.file?.length > 0"
|
|
36
|
+
class="mb-5"
|
|
37
|
+
>
|
|
38
|
+
<v-row no-gutters>
|
|
39
|
+
<v-col class="font-weight-bold">Attachments:</v-col>
|
|
40
|
+
<v-col cols="9" class="px-10">
|
|
41
|
+
<template v-if="videosArray.length > 0">
|
|
42
|
+
<div class="text-caption text-medium-emphasis mb-1">
|
|
43
|
+
Videos ({{ videosArray.length }})
|
|
44
|
+
</div>
|
|
45
|
+
<v-list density="compact" class="pa-0 rounded-lg border mb-3">
|
|
46
|
+
<template v-for="(item, i) in videosArray" :key="item._id">
|
|
47
|
+
<v-divider v-if="i > 0" />
|
|
48
|
+
<v-list-item
|
|
49
|
+
:prepend-icon="getFileIcon(item.file.type)"
|
|
50
|
+
:title="item.file.name"
|
|
51
|
+
:subtitle="item.file.type"
|
|
52
|
+
class="cursor-pointer"
|
|
53
|
+
@click="openMediaViewer(item._id, 'media')"
|
|
54
|
+
>
|
|
55
|
+
<template #prepend>
|
|
56
|
+
<v-icon
|
|
57
|
+
:color="getFileIconColor(item.file.type)"
|
|
58
|
+
class="mr-3"
|
|
59
|
+
>{{ getFileIcon(item.file.type) }}</v-icon
|
|
60
|
+
>
|
|
61
|
+
</template>
|
|
62
|
+
<template #append>
|
|
63
|
+
<v-icon size="16" color="grey-lighten-1"
|
|
64
|
+
>mdi-play-circle-outline</v-icon
|
|
65
|
+
>
|
|
66
|
+
</template>
|
|
67
|
+
</v-list-item>
|
|
68
|
+
</template>
|
|
69
|
+
</v-list>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<template v-if="photosArray.length > 0">
|
|
73
|
+
<div
|
|
74
|
+
class="text-caption text-medium-emphasis mb-1"
|
|
75
|
+
:class="{ 'mt-3': videosArray.length > 0 }"
|
|
76
|
+
>
|
|
77
|
+
Photos ({{ photosArray.length }})
|
|
78
|
+
</div>
|
|
79
|
+
<v-list density="compact" class="pa-0 rounded-lg border mb-3">
|
|
80
|
+
<template v-for="(item, i) in photosArray" :key="item._id">
|
|
81
|
+
<v-divider v-if="i > 0" />
|
|
82
|
+
<v-list-item
|
|
83
|
+
class="cursor-pointer"
|
|
84
|
+
@click="openMediaViewer(item._id, 'media')"
|
|
85
|
+
>
|
|
86
|
+
<template #prepend>
|
|
87
|
+
<v-avatar size="36" rounded="sm" class="mr-3">
|
|
88
|
+
<v-img :src="getFileUrl(item._id)" cover>
|
|
89
|
+
<template #error>
|
|
90
|
+
<v-icon color="green" size="20">mdi-image</v-icon>
|
|
109
91
|
</template>
|
|
110
|
-
|
|
111
|
-
</v-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
92
|
+
</v-img>
|
|
93
|
+
</v-avatar>
|
|
94
|
+
</template>
|
|
95
|
+
<v-list-item-title class="text-body-2">{{
|
|
96
|
+
item.file.name
|
|
97
|
+
}}</v-list-item-title>
|
|
98
|
+
<v-list-item-subtitle class="text-caption">{{
|
|
99
|
+
item.file.type
|
|
100
|
+
}}</v-list-item-subtitle>
|
|
101
|
+
<template #append>
|
|
102
|
+
<v-icon size="16" color="grey-lighten-1"
|
|
103
|
+
>mdi-eye-outline</v-icon
|
|
104
|
+
>
|
|
105
|
+
</template>
|
|
106
|
+
</v-list-item>
|
|
107
|
+
</template>
|
|
108
|
+
</v-list>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<template v-if="filesArray.length > 0">
|
|
112
|
+
<div
|
|
113
|
+
class="text-caption text-medium-emphasis mb-1"
|
|
114
|
+
:class="{
|
|
115
|
+
'mt-3': videosArray.length > 0 || photosArray.length > 0,
|
|
116
|
+
}"
|
|
117
|
+
>
|
|
118
|
+
Files ({{ filesArray.length }})
|
|
119
|
+
</div>
|
|
120
|
+
<v-list density="compact" class="pa-0 rounded-lg border">
|
|
121
|
+
<template v-for="(item, i) in filesArray" :key="item._id">
|
|
122
|
+
<v-divider v-if="i > 0" />
|
|
123
|
+
<v-list-item
|
|
124
|
+
class="cursor-pointer"
|
|
125
|
+
@click="openFileViewer(item._id, item.file.name)"
|
|
126
|
+
>
|
|
127
|
+
<template #prepend>
|
|
128
|
+
<v-icon
|
|
129
|
+
:color="getFileIconColor(item.file.type)"
|
|
130
|
+
class="mr-3"
|
|
131
|
+
>{{ getFileIcon(item.file.type) }}</v-icon
|
|
132
|
+
>
|
|
133
|
+
</template>
|
|
134
|
+
<v-list-item-title class="text-body-2">{{
|
|
135
|
+
item.file.name
|
|
136
|
+
}}</v-list-item-title>
|
|
137
|
+
<v-list-item-subtitle class="text-caption">{{
|
|
138
|
+
item.file.type
|
|
139
|
+
}}</v-list-item-subtitle>
|
|
140
|
+
<template #append>
|
|
141
|
+
<v-icon size="16" color="grey-lighten-1"
|
|
142
|
+
>mdi-open-in-new</v-icon
|
|
143
|
+
>
|
|
144
|
+
</template>
|
|
145
|
+
</v-list-item>
|
|
146
|
+
</template>
|
|
147
|
+
</v-list>
|
|
148
|
+
</template>
|
|
149
|
+
</v-col>
|
|
150
|
+
</v-row>
|
|
151
|
+
</v-col>
|
|
152
|
+
|
|
153
|
+
<!-- Media Carousel (images + videos) -->
|
|
154
|
+
<ImageCarousel
|
|
155
|
+
v-model="showCarousel"
|
|
156
|
+
:active-file-id="activeFileId"
|
|
157
|
+
:files="carouselFiles"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<!-- File Viewer Dialog (PDF, docs, etc.) -->
|
|
161
|
+
<v-dialog v-model="showFileDialog" max-width="900" scrollable>
|
|
162
|
+
<v-card>
|
|
163
|
+
<v-toolbar density="compact">
|
|
164
|
+
<v-toolbar-title class="text-subtitle-2 text-truncate">{{
|
|
165
|
+
activeFileName
|
|
166
|
+
}}</v-toolbar-title>
|
|
167
|
+
<v-spacer />
|
|
168
|
+
<v-btn
|
|
169
|
+
icon="mdi-open-in-new"
|
|
170
|
+
variant="text"
|
|
171
|
+
size="small"
|
|
172
|
+
@click="openInNewTab(activeFileId)"
|
|
173
|
+
/>
|
|
174
|
+
<v-btn
|
|
175
|
+
icon="mdi-close"
|
|
176
|
+
variant="text"
|
|
177
|
+
size="small"
|
|
178
|
+
@click="showFileDialog = false"
|
|
179
|
+
/>
|
|
180
|
+
</v-toolbar>
|
|
181
|
+
<v-card-text class="pa-0" style="height: 75vh">
|
|
182
|
+
<iframe
|
|
183
|
+
:src="getFileUrl(activeFileId)"
|
|
184
|
+
width="100%"
|
|
185
|
+
height="100%"
|
|
186
|
+
style="border: none"
|
|
187
|
+
:title="activeFileName"
|
|
188
|
+
/>
|
|
189
|
+
</v-card-text>
|
|
190
|
+
<v-card-actions>
|
|
191
|
+
<v-spacer />
|
|
192
|
+
<v-btn
|
|
193
|
+
variant="text"
|
|
194
|
+
class="text-none"
|
|
195
|
+
@click="openInNewTab(activeFileId)"
|
|
196
|
+
>
|
|
197
|
+
<v-icon start>mdi-download</v-icon>
|
|
198
|
+
Open / Download
|
|
199
|
+
</v-btn>
|
|
200
|
+
</v-card-actions>
|
|
201
|
+
</v-card>
|
|
202
|
+
</v-dialog>
|
|
203
|
+
</v-row>
|
|
204
|
+
</v-card-text>
|
|
205
|
+
|
|
206
|
+
<v-toolbar class="pa-0" density="compact">
|
|
207
|
+
<v-row no-gutters>
|
|
208
|
+
<v-col :col="!canUpdate && !canDelete ? 12 : 6" class="pa-0">
|
|
209
|
+
<v-btn
|
|
210
|
+
block
|
|
211
|
+
variant="text"
|
|
212
|
+
height="48"
|
|
213
|
+
class="text-none"
|
|
214
|
+
@click="emit('close')"
|
|
215
|
+
>
|
|
216
|
+
Close
|
|
217
|
+
</v-btn>
|
|
218
|
+
</v-col>
|
|
219
|
+
|
|
220
|
+
<v-col v-if="canUpdate || canDelete" cols="6" class="pa-0">
|
|
221
|
+
<v-menu contained>
|
|
222
|
+
<template #activator="{ props }">
|
|
223
|
+
<v-btn
|
|
224
|
+
block
|
|
225
|
+
variant="flat"
|
|
226
|
+
color="black"
|
|
227
|
+
height="48"
|
|
228
|
+
class="text-none"
|
|
229
|
+
tile
|
|
230
|
+
v-bind="props"
|
|
231
|
+
>
|
|
232
|
+
More actions
|
|
233
|
+
</v-btn>
|
|
234
|
+
</template>
|
|
235
|
+
|
|
236
|
+
<v-list class="pa-0">
|
|
237
|
+
<v-list-item v-if="canUpdate" @click="handleEdit">
|
|
238
|
+
<v-list-item-title class="text-subtitle-2">
|
|
239
|
+
Edit Announcement
|
|
240
|
+
</v-list-item-title>
|
|
241
|
+
</v-list-item>
|
|
242
|
+
|
|
243
|
+
<v-list-item
|
|
244
|
+
v-if="canDelete"
|
|
245
|
+
class="text-red"
|
|
246
|
+
@click="handleDelete"
|
|
247
|
+
>
|
|
248
|
+
<v-list-item-title class="text-subtitle-2">
|
|
249
|
+
Delete Announcement
|
|
250
|
+
</v-list-item-title>
|
|
251
|
+
</v-list-item>
|
|
252
|
+
</v-list>
|
|
253
|
+
</v-menu>
|
|
254
|
+
</v-col>
|
|
255
|
+
</v-row>
|
|
256
|
+
</v-toolbar>
|
|
257
|
+
</v-card>
|
|
184
258
|
</template>
|
|
185
259
|
|
|
186
|
-
|
|
187
260
|
<script setup lang="ts">
|
|
188
|
-
|
|
189
261
|
const prop = defineProps({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
})
|
|
262
|
+
activeAnnouncement: {
|
|
263
|
+
type: Object as PropType<TAnnouncement>,
|
|
264
|
+
required: true,
|
|
265
|
+
},
|
|
266
|
+
canUpdate: Boolean,
|
|
267
|
+
canDelete: Boolean,
|
|
268
|
+
});
|
|
197
269
|
|
|
198
|
-
const processing = ref(false)
|
|
199
|
-
const { recipientList } = useBulletin()
|
|
200
|
-
const { getFileUrl, urlToFile } = useFile()
|
|
270
|
+
const processing = ref(false);
|
|
271
|
+
const { recipientList } = useBulletin();
|
|
272
|
+
const { getFileUrl, urlToFile } = useFile();
|
|
201
273
|
|
|
202
|
-
const emit = defineEmits([
|
|
274
|
+
const emit = defineEmits(["close", "edit", "delete"]);
|
|
203
275
|
|
|
204
276
|
/* Field labels */
|
|
205
277
|
const formattedFields: Partial<Record<keyof TAnnouncement, string>> = {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
278
|
+
title: "Title",
|
|
279
|
+
createdAt: "Date Created",
|
|
280
|
+
recipients: "Recipients",
|
|
281
|
+
content: "Content",
|
|
282
|
+
startDate: "Start Date",
|
|
283
|
+
endDate: "End Date",
|
|
284
|
+
noExpiration: "No Expiration",
|
|
285
|
+
};
|
|
214
286
|
|
|
215
287
|
const formattedRecipients = computed(() => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
288
|
+
const arr = recipientList.filter((x) =>
|
|
289
|
+
prop.activeAnnouncement?.recipients?.includes(x.value)
|
|
290
|
+
);
|
|
291
|
+
return arr.map((item) => item.title).join(", ");
|
|
292
|
+
});
|
|
221
293
|
|
|
222
294
|
function getFieldValue(key: keyof TAnnouncement) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
295
|
+
const value = prop.activeAnnouncement?.[key];
|
|
296
|
+
|
|
297
|
+
if (value === null || value === "" || value === undefined) return "";
|
|
298
|
+
|
|
299
|
+
switch (key) {
|
|
300
|
+
case "recipients":
|
|
301
|
+
return formattedRecipients.value;
|
|
302
|
+
case "createdAt":
|
|
303
|
+
case "startDate":
|
|
304
|
+
case "endDate":
|
|
305
|
+
return toLocalDate(value as string);
|
|
306
|
+
default:
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
237
309
|
}
|
|
238
310
|
|
|
239
|
-
const rawFileArray = ref<{ file: File
|
|
311
|
+
const rawFileArray = ref<{ file: File; _id: string; preview: boolean }[]>([]);
|
|
240
312
|
|
|
241
|
-
const photosArray = getByType(
|
|
242
|
-
const videosArray = getByType(
|
|
243
|
-
const filesArray = getByType(
|
|
313
|
+
const photosArray = getByType("image/");
|
|
314
|
+
const videosArray = getByType("video/");
|
|
315
|
+
const filesArray = getByType("other");
|
|
244
316
|
|
|
245
317
|
/* Viewer state */
|
|
246
|
-
const showCarousel = ref(false)
|
|
247
|
-
const showFileDialog = ref(false)
|
|
248
|
-
const activeFileId = ref(
|
|
249
|
-
const activeFileName = ref(
|
|
250
|
-
const carouselFiles = ref<string[]>([])
|
|
251
|
-
|
|
252
|
-
function openMediaViewer(id: string, _type:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
318
|
+
const showCarousel = ref(false);
|
|
319
|
+
const showFileDialog = ref(false);
|
|
320
|
+
const activeFileId = ref("");
|
|
321
|
+
const activeFileName = ref("");
|
|
322
|
+
const carouselFiles = ref<string[]>([]);
|
|
323
|
+
|
|
324
|
+
function openMediaViewer(id: string, _type: "media") {
|
|
325
|
+
activeFileId.value = id;
|
|
326
|
+
carouselFiles.value = [
|
|
327
|
+
...photosArray.value.map((x) => x._id),
|
|
328
|
+
...videosArray.value.map((x) => x._id),
|
|
329
|
+
];
|
|
330
|
+
showCarousel.value = true;
|
|
259
331
|
}
|
|
260
332
|
|
|
261
333
|
function openFileViewer(id: string, name: string) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
334
|
+
activeFileId.value = id;
|
|
335
|
+
activeFileName.value = name;
|
|
336
|
+
showFileDialog.value = true;
|
|
265
337
|
}
|
|
266
338
|
|
|
267
339
|
function openInNewTab(id: string) {
|
|
268
|
-
|
|
340
|
+
window.open(getFileUrl(id), "_blank", "noopener,noreferrer");
|
|
269
341
|
}
|
|
270
342
|
|
|
271
343
|
function getFileIcon(mimeType: string): string {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
344
|
+
if (mimeType.startsWith("image/")) return "mdi-image";
|
|
345
|
+
if (mimeType.startsWith("video/")) return "mdi-video";
|
|
346
|
+
if (mimeType === "application/pdf") return "mdi-file-pdf-box";
|
|
347
|
+
if (mimeType.includes("word") || mimeType.includes("document"))
|
|
348
|
+
return "mdi-file-word-box";
|
|
349
|
+
if (mimeType.includes("excel") || mimeType.includes("spreadsheet"))
|
|
350
|
+
return "mdi-file-excel-box";
|
|
351
|
+
if (mimeType.includes("powerpoint") || mimeType.includes("presentation"))
|
|
352
|
+
return "mdi-file-powerpoint-box";
|
|
353
|
+
if (mimeType === "text/plain") return "mdi-file-document-outline";
|
|
354
|
+
if (
|
|
355
|
+
mimeType.includes("zip") ||
|
|
356
|
+
mimeType.includes("compressed") ||
|
|
357
|
+
mimeType.includes("archive")
|
|
358
|
+
)
|
|
359
|
+
return "mdi-folder-zip-outline";
|
|
360
|
+
return "mdi-file-outline";
|
|
281
361
|
}
|
|
282
362
|
|
|
283
363
|
function getFileIconColor(mimeType: string): string {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return
|
|
364
|
+
if (mimeType === "application/pdf") return "red";
|
|
365
|
+
if (mimeType.includes("word") || mimeType.includes("document")) return "blue";
|
|
366
|
+
if (mimeType.includes("excel") || mimeType.includes("spreadsheet"))
|
|
367
|
+
return "green";
|
|
368
|
+
if (mimeType.includes("powerpoint") || mimeType.includes("presentation"))
|
|
369
|
+
return "orange";
|
|
370
|
+
if (mimeType.startsWith("video/")) return "purple";
|
|
371
|
+
return "grey";
|
|
290
372
|
}
|
|
291
373
|
|
|
292
|
-
function getByType(prefix:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
374
|
+
function getByType(prefix: "image/" | "video/" | "other") {
|
|
375
|
+
return computed(() => {
|
|
376
|
+
return rawFileArray.value.filter((x) => {
|
|
377
|
+
const type = x?.file?.type;
|
|
378
|
+
if (typeof type !== "string") return false;
|
|
379
|
+
|
|
380
|
+
if (prefix === "other") {
|
|
381
|
+
return !type.startsWith("image/") && !type.startsWith("video/");
|
|
382
|
+
}
|
|
383
|
+
return type.startsWith(prefix);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
304
386
|
}
|
|
305
387
|
|
|
306
388
|
function toLocalDate(utcString: string) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
389
|
+
if (typeof utcString !== "string") return;
|
|
390
|
+
return new Date(utcString).toLocaleDateString("en-GB", {
|
|
391
|
+
year: "numeric",
|
|
392
|
+
month: "2-digit",
|
|
393
|
+
day: "2-digit",
|
|
394
|
+
});
|
|
313
395
|
}
|
|
314
396
|
|
|
315
397
|
function handleEdit() {
|
|
316
|
-
|
|
398
|
+
emit("edit");
|
|
317
399
|
}
|
|
318
400
|
|
|
319
401
|
function handleDelete() {
|
|
320
|
-
|
|
402
|
+
emit("delete");
|
|
321
403
|
}
|
|
322
404
|
|
|
323
405
|
watchEffect(async () => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
|
|
406
|
+
const data = prop.activeAnnouncement;
|
|
407
|
+
if (!data) return;
|
|
408
|
+
|
|
409
|
+
rawFileArray.value = await Promise.all(
|
|
410
|
+
data.file.map(async (item) => {
|
|
411
|
+
const fileUrl = getFileUrl(item._id);
|
|
412
|
+
const file = await urlToFile(fileUrl, item.name);
|
|
413
|
+
return { file, _id: item._id, preview: item.preview ?? false };
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
});
|
|
337
417
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<v-toolbar>
|
|
4
4
|
<v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
|
|
5
5
|
<span class="font-weight-bold text-h5 text-capitalize">
|
|
6
|
-
{{
|
|
6
|
+
{{ formMode }} {{ formatVisitorType(type) }}
|
|
7
7
|
</span>
|
|
8
8
|
<ButtonClose @click="emit('close:all')" />
|
|
9
9
|
</v-row>
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
:selected-nfc-cards="passType === 'NFC' ? passCards : []" />
|
|
218
218
|
</v-col>
|
|
219
219
|
|
|
220
|
-
<v-col v-if="
|
|
220
|
+
<v-col v-if="formMode === 'add'" cols="12" class="mt-2">
|
|
221
221
|
<v-checkbox v-model="createMore" density="comfortable" hide-details>
|
|
222
222
|
<template #label>
|
|
223
223
|
<span class="text-subtitle-2 font-weight-bold">
|
|
@@ -239,11 +239,11 @@
|
|
|
239
239
|
<v-row no-gutters>
|
|
240
240
|
<v-col cols="6">
|
|
241
241
|
<v-btn v-if="
|
|
242
|
-
|
|
242
|
+
formMode === 'add' &&
|
|
243
243
|
(withStep2 || withStep3) &&
|
|
244
244
|
contractorStep > 1
|
|
245
245
|
" tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
|
|
246
|
-
<v-btn v-else-if="
|
|
246
|
+
<v-btn v-else-if="formMode === 'add' || formMode === 'register'" tile block variant="text" class="text-none"
|
|
247
247
|
size="48" @click="backToSelection" text="Back to Selection" />
|
|
248
248
|
<v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
|
|
249
249
|
</v-col>
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
:disabled="processing" @click="handleNextPage" :text="'Next'" />
|
|
253
253
|
<v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
|
|
254
254
|
:disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
|
|
255
|
-
:text="
|
|
255
|
+
:text="formMode == 'add' ? 'Submit' : 'Update'" />
|
|
256
256
|
</v-col>
|
|
257
257
|
</v-row>
|
|
258
258
|
</v-toolbar>
|
|
@@ -297,6 +297,82 @@
|
|
|
297
297
|
</v-card-text>
|
|
298
298
|
</v-card>
|
|
299
299
|
</v-dialog>
|
|
300
|
+
|
|
301
|
+
<v-dialog
|
|
302
|
+
v-if="enableUnregistered"
|
|
303
|
+
v-model="dialog.showUnregisteredPlateDialog"
|
|
304
|
+
max-width="900"
|
|
305
|
+
persistent
|
|
306
|
+
>
|
|
307
|
+
<v-card color="background" class="pa-4">
|
|
308
|
+
<v-card-actions class="justify-end pa-0">
|
|
309
|
+
<v-btn
|
|
310
|
+
icon="mdi-close"
|
|
311
|
+
variant="text"
|
|
312
|
+
size="large"
|
|
313
|
+
@click="closeUnregisteredPlateDialog"
|
|
314
|
+
/>
|
|
315
|
+
</v-card-actions>
|
|
316
|
+
|
|
317
|
+
<v-card-text class="text-center pt-2">
|
|
318
|
+
<v-icon color="orange" size="96">mdi-alert-circle-outline</v-icon>
|
|
319
|
+
|
|
320
|
+
<div :class="`text-body-${$vuetify.display.xs ? 'mobile' : 'tablet'} mt-8`">
|
|
321
|
+
Vehicle Number {{ unregisteredPlateSearchText }} is similar to below
|
|
322
|
+
<br />
|
|
323
|
+
Unregistered visitors last 24 hours. Please select to
|
|
324
|
+
<br />
|
|
325
|
+
Update
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<v-table class="mt-8 border rounded">
|
|
329
|
+
<thead>
|
|
330
|
+
<tr>
|
|
331
|
+
<th class="text-left">Plate Number</th>
|
|
332
|
+
<th class="text-left">Check In</th>
|
|
333
|
+
<th class="text-center">Action</th>
|
|
334
|
+
</tr>
|
|
335
|
+
</thead>
|
|
336
|
+
<tbody>
|
|
337
|
+
<tr
|
|
338
|
+
v-for="match in unregisteredPlateMatches"
|
|
339
|
+
:key="match._id || match.plateNumber"
|
|
340
|
+
>
|
|
341
|
+
<td class="text-left font-weight-bold text-uppercase">
|
|
342
|
+
{{ match.plateNumber || "N/A" }}
|
|
343
|
+
</td>
|
|
344
|
+
<td class="text-left">
|
|
345
|
+
{{ formatUnregisteredCheckIn(match.checkIn) }}
|
|
346
|
+
</td>
|
|
347
|
+
<td class="text-center">
|
|
348
|
+
<v-btn
|
|
349
|
+
color="red"
|
|
350
|
+
class="text-capitalize"
|
|
351
|
+
density="compact"
|
|
352
|
+
size="large"
|
|
353
|
+
@click="selectUnregisteredPlateMatch(match)"
|
|
354
|
+
>
|
|
355
|
+
Update
|
|
356
|
+
</v-btn>
|
|
357
|
+
</td>
|
|
358
|
+
</tr>
|
|
359
|
+
</tbody>
|
|
360
|
+
</v-table>
|
|
361
|
+
</v-card-text>
|
|
362
|
+
|
|
363
|
+
<v-card-actions>
|
|
364
|
+
<v-btn
|
|
365
|
+
block
|
|
366
|
+
color="red"
|
|
367
|
+
variant="flat"
|
|
368
|
+
size="x-large"
|
|
369
|
+
@click="closeUnregisteredPlateDialog"
|
|
370
|
+
>
|
|
371
|
+
Cancel & Add New
|
|
372
|
+
</v-btn>
|
|
373
|
+
</v-card-actions>
|
|
374
|
+
</v-card>
|
|
375
|
+
</v-dialog>
|
|
300
376
|
</v-card>
|
|
301
377
|
</template>
|
|
302
378
|
|
|
@@ -353,6 +429,7 @@ const {
|
|
|
353
429
|
typeFieldMap,
|
|
354
430
|
contractorTypes,
|
|
355
431
|
getVisitors,
|
|
432
|
+
checkSearchUncheckedOutUnregisteredVisitor,
|
|
356
433
|
updateVisitor,
|
|
357
434
|
} = useVisitor();
|
|
358
435
|
const { getBySiteId: getEntryPassSettingsBySiteId } =
|
|
@@ -408,10 +485,12 @@ const registeredUnitCompanyName = ref("N/A");
|
|
|
408
485
|
const dialog = reactive({
|
|
409
486
|
vehicleNumberUsersList: false,
|
|
410
487
|
showNonCheckedOutDialog: false,
|
|
488
|
+
showUnregisteredPlateDialog: false,
|
|
411
489
|
});
|
|
412
490
|
|
|
413
491
|
const loading = reactive({
|
|
414
492
|
checkingOut: false,
|
|
493
|
+
checkingUnregisteredPlate: false,
|
|
415
494
|
});
|
|
416
495
|
|
|
417
496
|
const validForm = ref(false);
|
|
@@ -437,9 +516,18 @@ const deliveryCompanyInput = ref("");
|
|
|
437
516
|
const deliveryCompanyList = ref<string[]>([]);
|
|
438
517
|
|
|
439
518
|
const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([]);
|
|
519
|
+
const unregisteredPlateMatches = ref<TVisitor[]>([]);
|
|
520
|
+
const unregisteredPlateSearchText = ref("");
|
|
521
|
+
const selectedUnregisteredVisitor = ref<TVisitor | null>(null);
|
|
522
|
+
const skipUnregisteredPlateCheck = ref(false);
|
|
523
|
+
const pendingUnregisteredAction = ref<"next" | "submit" | null>(null);
|
|
440
524
|
|
|
441
525
|
const vehicleNumberUserItems = ref<TPeople[]>([]);
|
|
442
526
|
|
|
527
|
+
const formMode = computed<"add" | "edit" | "register">(() => {
|
|
528
|
+
return selectedUnregisteredVisitor.value ? "register" : prop.mode;
|
|
529
|
+
});
|
|
530
|
+
|
|
443
531
|
const shouldShowField = (
|
|
444
532
|
fieldKey: keyof TVisitorPayload | "delivery-company"
|
|
445
533
|
) => {
|
|
@@ -467,6 +555,10 @@ const showNextButton = computed(() => {
|
|
|
467
555
|
} else return false;
|
|
468
556
|
});
|
|
469
557
|
|
|
558
|
+
const enableUnregistered = computed(() => {
|
|
559
|
+
return prop.mode === "add" && shouldShowField("plateNumber");
|
|
560
|
+
});
|
|
561
|
+
|
|
470
562
|
const showShowKeysField = computed(() => {
|
|
471
563
|
return ["guest", "contractor", "walk-in"].includes(prop.type);
|
|
472
564
|
});
|
|
@@ -696,12 +788,16 @@ const debounceSearchVisitorsByPlateNumbers = debounce(() => {
|
|
|
696
788
|
fetchVisitorListByVehicleNumberRefresh();
|
|
697
789
|
}, 300);
|
|
698
790
|
|
|
699
|
-
watch(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
791
|
+
// watch(
|
|
792
|
+
// () => visitor.plateNumber,
|
|
793
|
+
// (newVal) => {
|
|
794
|
+
// skipUnregisteredPlateCheck.value = false;
|
|
795
|
+
// selectedUnregisteredVisitor.value = null;
|
|
796
|
+
// unregisteredPlateMatches.value = [];
|
|
797
|
+
// dialog.showUnregisteredPlateDialog = false;
|
|
798
|
+
// debounceSearchVisitorsByPlateNumbers();
|
|
799
|
+
// }
|
|
800
|
+
// );
|
|
705
801
|
|
|
706
802
|
async function handleCheckout(visitorId: string) {
|
|
707
803
|
if (!visitorId) {
|
|
@@ -988,6 +1084,95 @@ function formatVisitorType(type: TVisitorType): string {
|
|
|
988
1084
|
}
|
|
989
1085
|
}
|
|
990
1086
|
|
|
1087
|
+
async function checkUnregisteredPlateBeforeContinue(action: "next" | "submit") {
|
|
1088
|
+
if (
|
|
1089
|
+
!enableUnregistered.value ||
|
|
1090
|
+
skipUnregisteredPlateCheck.value ||
|
|
1091
|
+
selectedUnregisteredVisitor.value ||
|
|
1092
|
+
!visitor.plateNumber
|
|
1093
|
+
) {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
try {
|
|
1098
|
+
loading.checkingUnregisteredPlate = true;
|
|
1099
|
+
unregisteredPlateSearchText.value = visitor.plateNumber;
|
|
1100
|
+
const res = await checkSearchUncheckedOutUnregisteredVisitor({
|
|
1101
|
+
site: prop.site,
|
|
1102
|
+
plateNumber: visitor.plateNumber,
|
|
1103
|
+
hourAgo: 24,
|
|
1104
|
+
page: 1,
|
|
1105
|
+
limit: 100,
|
|
1106
|
+
});
|
|
1107
|
+
const items = Array.isArray(res?.items)
|
|
1108
|
+
? res.items
|
|
1109
|
+
: Array.isArray(res?.data?.items)
|
|
1110
|
+
? res.data.items
|
|
1111
|
+
: Array.isArray(res?.data)
|
|
1112
|
+
? res.data
|
|
1113
|
+
: Array.isArray(res)
|
|
1114
|
+
? res
|
|
1115
|
+
: [];
|
|
1116
|
+
|
|
1117
|
+
unregisteredPlateMatches.value = items;
|
|
1118
|
+
if (items.length === 0) return true;
|
|
1119
|
+
|
|
1120
|
+
pendingUnregisteredAction.value = action;
|
|
1121
|
+
dialog.showUnregisteredPlateDialog = true;
|
|
1122
|
+
return false;
|
|
1123
|
+
} catch (error: any) {
|
|
1124
|
+
errorMessage.value =
|
|
1125
|
+
error?.data?.message ||
|
|
1126
|
+
error?.response?._data?.message ||
|
|
1127
|
+
"Unable to check unregistered visitors. Please try again.";
|
|
1128
|
+
return false;
|
|
1129
|
+
} finally {
|
|
1130
|
+
loading.checkingUnregisteredPlate = false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function formatUnregisteredCheckIn(checkIn?: string) {
|
|
1135
|
+
return checkIn ? UTCToLocalTIme(checkIn) : "N/A";
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function continuePendingUnregisteredAction() {
|
|
1139
|
+
const action = pendingUnregisteredAction.value;
|
|
1140
|
+
pendingUnregisteredAction.value = null;
|
|
1141
|
+
if (action === "next") {
|
|
1142
|
+
await handleNextPage();
|
|
1143
|
+
} else if (action === "submit") {
|
|
1144
|
+
await submit();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function closeUnregisteredPlateDialog() {
|
|
1149
|
+
dialog.showUnregisteredPlateDialog = false;
|
|
1150
|
+
unregisteredPlateMatches.value = [];
|
|
1151
|
+
skipUnregisteredPlateCheck.value = true;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function selectUnregisteredPlateMatch(match: TVisitor) {
|
|
1155
|
+
selectedUnregisteredVisitor.value = match;
|
|
1156
|
+
skipUnregisteredPlateCheck.value = true;
|
|
1157
|
+
dialog.showUnregisteredPlateDialog = false;
|
|
1158
|
+
unregisteredPlateMatches.value = [];
|
|
1159
|
+
|
|
1160
|
+
visitor.plateNumber = match.plateNumber;
|
|
1161
|
+
visitor.attachments = match.attachments || visitor.attachments;
|
|
1162
|
+
visitor.block = match.block || visitor.block;
|
|
1163
|
+
visitor.level = match.level || visitor.level;
|
|
1164
|
+
visitor.unit = match.unit || visitor.unit;
|
|
1165
|
+
visitor.company = match.company || visitor.company;
|
|
1166
|
+
visitor.contractorType = match.contractorType || visitor.contractorType;
|
|
1167
|
+
visitor.name = match.name || visitor.name;
|
|
1168
|
+
visitor.contact = match.contact || visitor.contact;
|
|
1169
|
+
visitor.nric = match.nric || visitor.nric;
|
|
1170
|
+
visitor.company = match.company || visitor.company;
|
|
1171
|
+
visitor.remarks = match.remarks || visitor.remarks;
|
|
1172
|
+
|
|
1173
|
+
pendingUnregisteredAction.value = null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
991
1176
|
async function handleNextPage() {
|
|
992
1177
|
errorMessage.value = "";
|
|
993
1178
|
formRef.value!.validate();
|
|
@@ -1010,6 +1195,7 @@ async function handleNextPage() {
|
|
|
1010
1195
|
errorMessage.value = "Please select at least one NFC card before proceeding.";
|
|
1011
1196
|
return;
|
|
1012
1197
|
}
|
|
1198
|
+
if (!(await checkUnregisteredPlateBeforeContinue("next"))) return;
|
|
1013
1199
|
if (step < 3) contractorStep.value++;
|
|
1014
1200
|
else await submit();
|
|
1015
1201
|
}
|
|
@@ -1023,6 +1209,7 @@ async function submit() {
|
|
|
1023
1209
|
}
|
|
1024
1210
|
|
|
1025
1211
|
if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
|
|
1212
|
+
if (!(await checkUnregisteredPlateBeforeContinue("submit"))) return;
|
|
1026
1213
|
|
|
1027
1214
|
processing.value = true;
|
|
1028
1215
|
|
|
@@ -1033,7 +1220,7 @@ async function submit() {
|
|
|
1033
1220
|
createdBy: prop.currentUser,
|
|
1034
1221
|
};
|
|
1035
1222
|
|
|
1036
|
-
if (
|
|
1223
|
+
if (formMode.value === "add" || formMode.value === "register") {
|
|
1037
1224
|
const allowedFields = typeFieldMap[prop.type];
|
|
1038
1225
|
for (const key of allowedFields as (keyof TVisitorPayload)[]) {
|
|
1039
1226
|
if (visitor[key] !== undefined) {
|
|
@@ -1071,7 +1258,7 @@ async function submit() {
|
|
|
1071
1258
|
}
|
|
1072
1259
|
|
|
1073
1260
|
// add register status when registering unregistered visitor
|
|
1074
|
-
if (
|
|
1261
|
+
if (formMode.value === "register") {
|
|
1075
1262
|
payload.status = "registered";
|
|
1076
1263
|
const currentUserId = currentUser.value?._id;
|
|
1077
1264
|
if(currentUserId) {
|
|
@@ -1081,7 +1268,18 @@ async function submit() {
|
|
|
1081
1268
|
|
|
1082
1269
|
try {
|
|
1083
1270
|
let res;
|
|
1084
|
-
if (prop.mode === "add") {
|
|
1271
|
+
if (prop.mode === "add" && selectedUnregisteredVisitor.value?._id) {
|
|
1272
|
+
payload.status = "registered";
|
|
1273
|
+
const currentUserId = currentUser.value?._id;
|
|
1274
|
+
if (currentUserId) {
|
|
1275
|
+
payload.updatedBy = currentUserId;
|
|
1276
|
+
}
|
|
1277
|
+
const { unitName, ...rest } = payload;
|
|
1278
|
+
res = await updateVisitor(
|
|
1279
|
+
selectedUnregisteredVisitor.value._id,
|
|
1280
|
+
rest as TVisitorPayload
|
|
1281
|
+
);
|
|
1282
|
+
} else if (formMode.value === "add") {
|
|
1085
1283
|
res = await createVisitor(payload as TVisitorPayload);
|
|
1086
1284
|
} else if (
|
|
1087
1285
|
(prop.mode === "edit" || prop.mode === "register") &&
|
|
@@ -1224,7 +1422,7 @@ async function submit() {
|
|
|
1224
1422
|
const err = error?.data?.message;
|
|
1225
1423
|
errorMessage.value =
|
|
1226
1424
|
err ||
|
|
1227
|
-
`Failed to ${
|
|
1425
|
+
`Failed to ${formMode.value === "add" ? "add" : "update"
|
|
1228
1426
|
} visitor. Please try again.`;
|
|
1229
1427
|
} finally {
|
|
1230
1428
|
processing.value = false;
|
|
@@ -1368,7 +1566,22 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
|
|
|
1368
1566
|
watch(
|
|
1369
1567
|
() => visitor.plateNumber,
|
|
1370
1568
|
(newVal) => {
|
|
1371
|
-
|
|
1569
|
+
const normalizedPlateNumber = newVal?.toUpperCase();
|
|
1570
|
+
if (newVal !== normalizedPlateNumber) {
|
|
1571
|
+
visitor.plateNumber = normalizedPlateNumber;
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (
|
|
1576
|
+
selectedUnregisteredVisitor.value &&
|
|
1577
|
+
normalizedPlateNumber !== selectedUnregisteredVisitor.value.plateNumber?.toUpperCase()
|
|
1578
|
+
) {
|
|
1579
|
+
selectedUnregisteredVisitor.value = null;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
skipUnregisteredPlateCheck.value = false;
|
|
1583
|
+
unregisteredPlateMatches.value = [];
|
|
1584
|
+
dialog.showUnregisteredPlateDialog = false;
|
|
1372
1585
|
}
|
|
1373
1586
|
);
|
|
1374
1587
|
|
|
@@ -94,6 +94,14 @@ export default function () {
|
|
|
94
94
|
tab?: string;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
type SearchUncheckedOutUnregisteredVisitorParams = {
|
|
98
|
+
site: string;
|
|
99
|
+
plateNumber: string;
|
|
100
|
+
hourAgo?: number;
|
|
101
|
+
page?: number;
|
|
102
|
+
limit?: number;
|
|
103
|
+
};
|
|
104
|
+
|
|
97
105
|
const tab = useState("visitorsTab", () => "visitor");
|
|
98
106
|
|
|
99
107
|
async function getVisitors({
|
|
@@ -159,6 +167,26 @@ export default function () {
|
|
|
159
167
|
);
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
async function checkSearchUncheckedOutUnregisteredVisitor({
|
|
171
|
+
site,
|
|
172
|
+
plateNumber,
|
|
173
|
+
hourAgo = 24,
|
|
174
|
+
page = 1,
|
|
175
|
+
limit = 100,
|
|
176
|
+
}: SearchUncheckedOutUnregisteredVisitorParams) {
|
|
177
|
+
return await useNuxtApp().$api<Record<string, any>>("/api/visitors", {
|
|
178
|
+
method: "GET",
|
|
179
|
+
query: {
|
|
180
|
+
site,
|
|
181
|
+
type: "uncheckout-unregistered",
|
|
182
|
+
plateNumber,
|
|
183
|
+
hourAgo,
|
|
184
|
+
page,
|
|
185
|
+
limit,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
162
190
|
async function deleteVisitor(_id: string) {
|
|
163
191
|
return await useNuxtApp().$api<Record<string, any>>(
|
|
164
192
|
`/api/visitor-transactions/id/${_id}`,
|
|
@@ -198,6 +226,7 @@ export default function () {
|
|
|
198
226
|
contractorTypes,
|
|
199
227
|
createVisitor,
|
|
200
228
|
getVisitors,
|
|
229
|
+
checkSearchUncheckedOutUnregisteredVisitor,
|
|
201
230
|
updateVisitor,
|
|
202
231
|
deleteVisitor,
|
|
203
232
|
validateVisitorQrCode,
|