@7365admin1/layer-common 1.11.24 → 1.11.26
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/AttachmentsViewer.vue +266 -0
- package/components/BuildingForm.vue +63 -95
- package/components/BuildingManagement/buildings.vue +11 -12
- package/components/DocumentViewer.vue +75 -0
- package/components/VisitorManagement.vue +323 -36
- package/composables/useVisitor.ts +3 -0
- package/package.json +1 -1
- package/types/building.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-col cols="12" v-if="files?.length > 0" class="mb-5">
|
|
3
|
+
<v-row no-gutters>
|
|
4
|
+
<v-col v-if="showTitle" class="font-weight-bold">{{ title }}</v-col>
|
|
5
|
+
<v-col cols="12" class="">
|
|
6
|
+
|
|
7
|
+
<!-- Loading Skeleton -->
|
|
8
|
+
<template v-if="loading && files?.length > 0">
|
|
9
|
+
<v-card v-for="(file, index) in files" :key="index" height="50px" flat border="sm black" class="mb-2">
|
|
10
|
+
<v-skeleton-loader type="list-item" class="mb-2" />
|
|
11
|
+
</v-card>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<!-- Files Content -->
|
|
15
|
+
<template v-else>
|
|
16
|
+
|
|
17
|
+
<!-- Videos -->
|
|
18
|
+
<template v-if="videosArray.length > 0">
|
|
19
|
+
<div v-if="showFileTypeTitle" class="text-caption text-medium-emphasis mb-1">Videos ({{
|
|
20
|
+
videosArray.length }})</div>
|
|
21
|
+
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
22
|
+
<template v-for="(item, i) in videosArray" :key="item._id">
|
|
23
|
+
<v-divider v-if="i > 0" />
|
|
24
|
+
<v-list-item :prepend-icon="getFileIcon(item.mimeType)" :title="item.file.name"
|
|
25
|
+
:subtitle="item.mimeType" class="cursor-pointer"
|
|
26
|
+
@click="openMediaViewer(item._id, item.mimeType)">
|
|
27
|
+
<template #prepend>
|
|
28
|
+
<v-icon :color="getFileIconColor(item.mimeType)" class="mr-3">{{
|
|
29
|
+
getFileIcon(item.mimeType) }}</v-icon>
|
|
30
|
+
</template>
|
|
31
|
+
<template #append>
|
|
32
|
+
<v-icon size="16" color="grey-lighten-1">mdi-play-circle-outline</v-icon>
|
|
33
|
+
</template>
|
|
34
|
+
</v-list-item>
|
|
35
|
+
</template>
|
|
36
|
+
</v-list>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<!-- Photos -->
|
|
40
|
+
<template v-if="photosArray.length > 0">
|
|
41
|
+
<div v-if="showFileTypeTitle" class="text-caption text-medium-emphasis mb-1"
|
|
42
|
+
:class="{ 'mt-3': videosArray.length > 0 }">
|
|
43
|
+
Photos ({{ photosArray.length }})</div>
|
|
44
|
+
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
45
|
+
<template v-for="(item, i) in photosArray" :key="item._id">
|
|
46
|
+
<v-divider v-if="i > 0" />
|
|
47
|
+
<v-list-item class="cursor-pointer" @click="openMediaViewer(item._id, item.mimeType)">
|
|
48
|
+
<template #prepend>
|
|
49
|
+
<v-avatar size="36" rounded="sm" class="mr-3">
|
|
50
|
+
<v-img :src="getFileUrl(item._id)" cover>
|
|
51
|
+
<template #error>
|
|
52
|
+
<v-icon color="green" size="20">mdi-image</v-icon>
|
|
53
|
+
</template>
|
|
54
|
+
</v-img>
|
|
55
|
+
</v-avatar>
|
|
56
|
+
</template>
|
|
57
|
+
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
58
|
+
<v-list-item-subtitle class="text-caption">{{ item.mimeType
|
|
59
|
+
}}</v-list-item-subtitle>
|
|
60
|
+
<template #append>
|
|
61
|
+
<v-icon size="16" color="grey-lighten-1">mdi-eye-outline</v-icon>
|
|
62
|
+
</template>
|
|
63
|
+
</v-list-item>
|
|
64
|
+
</template>
|
|
65
|
+
</v-list>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<!-- Documents & Other Files -->
|
|
69
|
+
<template v-if="filesArray.length > 0">
|
|
70
|
+
<div v-if="showFileTypeTitle" class="text-caption text-medium-emphasis mb-1"
|
|
71
|
+
:class="{ 'mt-3': videosArray.length > 0 || photosArray.length > 0 }">Files ({{
|
|
72
|
+
filesArray.length }})</div>
|
|
73
|
+
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
74
|
+
<template v-for="(item, i) in filesArray" :key="item._id">
|
|
75
|
+
<v-divider v-if="i > 0" />
|
|
76
|
+
<v-list-item class="cursor-pointer"
|
|
77
|
+
@click="openDocumentViewer(item._id, item.file.name, item.mimeType)">
|
|
78
|
+
<template #prepend>
|
|
79
|
+
<v-icon :color="getFileIconColor(item.mimeType)" class="mr-3">{{
|
|
80
|
+
getFileIcon(item.mimeType) }}</v-icon>
|
|
81
|
+
</template>
|
|
82
|
+
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
83
|
+
<v-list-item-subtitle class="text-caption">{{ item.mimeType
|
|
84
|
+
}}</v-list-item-subtitle>
|
|
85
|
+
<template #append>
|
|
86
|
+
<v-icon size="16" color="grey-lighten-1">mdi-open-in-new</v-icon>
|
|
87
|
+
</template>
|
|
88
|
+
</v-list-item>
|
|
89
|
+
</template>
|
|
90
|
+
</v-list>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
</v-col>
|
|
96
|
+
</v-row>
|
|
97
|
+
|
|
98
|
+
<!-- Media Carousel (images + videos) -->
|
|
99
|
+
<ImageCarousel v-model="showCarousel" :active-file-id="activeFileId" :files="carouselFiles" />
|
|
100
|
+
|
|
101
|
+
<!-- Document Viewer -->
|
|
102
|
+
<DocumentViewer v-model="showDocumentViewer" :file-id="activeFileId" :file-name="activeFileName"
|
|
103
|
+
:mime-type="activeFileMimeType" />
|
|
104
|
+
</v-col>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
const props = defineProps({
|
|
109
|
+
files: {
|
|
110
|
+
type: Array as PropType<{ id: string, name: string }[]>,
|
|
111
|
+
required: true,
|
|
112
|
+
},
|
|
113
|
+
title: {
|
|
114
|
+
type: String,
|
|
115
|
+
default: "Attachments",
|
|
116
|
+
},
|
|
117
|
+
showTitle: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
showFileTypeTitle: {
|
|
123
|
+
type: Boolean,
|
|
124
|
+
default: false,
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const { getFileUrl, urlToFile, getFileById } = useFile();
|
|
129
|
+
|
|
130
|
+
const rawFileArray = ref<{ file: File, _id: string, preview: boolean, mimeType: string }[]>([]);
|
|
131
|
+
const loading = ref(true);
|
|
132
|
+
|
|
133
|
+
const photosArray = getByType('image/');
|
|
134
|
+
const videosArray = getByType('video/');
|
|
135
|
+
const filesArray = getByType('other');
|
|
136
|
+
|
|
137
|
+
/* Viewer state */
|
|
138
|
+
const showCarousel = ref(false);
|
|
139
|
+
const showDocumentViewer = ref(false);
|
|
140
|
+
const activeFileId = ref('');
|
|
141
|
+
const activeFileName = ref('');
|
|
142
|
+
const activeFileMimeType = ref('application/octet-stream');
|
|
143
|
+
const carouselFiles = ref<string[]>([]);
|
|
144
|
+
|
|
145
|
+
function openMediaViewer(id: string, mimeType: string) {
|
|
146
|
+
activeFileId.value = id;
|
|
147
|
+
carouselFiles.value = [
|
|
148
|
+
...photosArray.value.map(x => x._id),
|
|
149
|
+
...videosArray.value.map(x => x._id),
|
|
150
|
+
];
|
|
151
|
+
showCarousel.value = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function openDocumentViewer(id: string, name: string, mimeType: string) {
|
|
155
|
+
activeFileId.value = id;
|
|
156
|
+
activeFileName.value = name;
|
|
157
|
+
activeFileMimeType.value = mimeType;
|
|
158
|
+
showDocumentViewer.value = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getFileIcon(mimeType: string): string {
|
|
162
|
+
if (mimeType.startsWith('image/')) return 'mdi-image';
|
|
163
|
+
if (mimeType.startsWith('video/')) return 'mdi-video';
|
|
164
|
+
if (mimeType === 'application/pdf') return 'mdi-file-pdf-box';
|
|
165
|
+
if (mimeType.includes('word') || mimeType.includes('document')) return 'mdi-file-word-box';
|
|
166
|
+
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'mdi-file-excel-box';
|
|
167
|
+
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'mdi-file-powerpoint-box';
|
|
168
|
+
if (mimeType === 'text/plain') return 'mdi-file-document-outline';
|
|
169
|
+
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) return 'mdi-folder-zip-outline';
|
|
170
|
+
return 'mdi-file-outline';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getFileIconColor(mimeType: string): string {
|
|
174
|
+
if (mimeType === 'application/pdf') return 'red';
|
|
175
|
+
if (mimeType.includes('word') || mimeType.includes('document')) return 'blue';
|
|
176
|
+
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'green';
|
|
177
|
+
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'orange';
|
|
178
|
+
if (mimeType.startsWith('video/')) return 'purple';
|
|
179
|
+
return 'grey';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getByType(prefix: 'image/' | 'video/' | 'other') {
|
|
183
|
+
return computed(() => {
|
|
184
|
+
return rawFileArray.value.filter(x => {
|
|
185
|
+
const type = x?.mimeType || x?.file?.type;
|
|
186
|
+
if (typeof type !== 'string') return false;
|
|
187
|
+
|
|
188
|
+
if (prefix === 'other') {
|
|
189
|
+
return !type.startsWith('image/') && !type.startsWith('video/');
|
|
190
|
+
}
|
|
191
|
+
return type.startsWith(prefix);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
watchEffect(async () => {
|
|
197
|
+
loading.value = true;
|
|
198
|
+
const processedFiles: typeof rawFileArray.value = [];
|
|
199
|
+
|
|
200
|
+
for (const item of (props.files || [])) {
|
|
201
|
+
// Handle building files format: { id, name }
|
|
202
|
+
if (item.id && item.name && !item.file) {
|
|
203
|
+
let mimeType = 'application/octet-stream';
|
|
204
|
+
try {
|
|
205
|
+
const fileMeta = await getFileById(item.id) as any;
|
|
206
|
+
const apiMimeType = fileMeta?.data?.mimeType || fileMeta?.mimeType;
|
|
207
|
+
// Use API mime type only if it's not generic
|
|
208
|
+
if (apiMimeType && apiMimeType !== 'application/octet-stream') {
|
|
209
|
+
mimeType = apiMimeType;
|
|
210
|
+
} else {
|
|
211
|
+
// Fallback to filename-based detection
|
|
212
|
+
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
213
|
+
mimeType = inferMimeType(ext);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Fallback: infer MIME type from file name
|
|
217
|
+
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
218
|
+
mimeType = inferMimeType(ext);
|
|
219
|
+
}
|
|
220
|
+
processedFiles.push({
|
|
221
|
+
_id: item.id,
|
|
222
|
+
file: new File([], item.name, { type: mimeType }),
|
|
223
|
+
preview: item.preview ?? false,
|
|
224
|
+
mimeType
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Handle announcement files format: { file, _id, ... }
|
|
228
|
+
else if (item.file && item._id) {
|
|
229
|
+
processedFiles.push({
|
|
230
|
+
...item,
|
|
231
|
+
preview: item.preview ?? false,
|
|
232
|
+
mimeType: item.file.type || 'application/octet-stream'
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
rawFileArray.value = processedFiles;
|
|
238
|
+
loading.value = false;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function inferMimeType(ext?: string): string {
|
|
242
|
+
if (!ext) return 'application/octet-stream';
|
|
243
|
+
const mimeMap: Record<string, string> = {
|
|
244
|
+
'pdf': 'application/pdf',
|
|
245
|
+
'doc': 'application/msword',
|
|
246
|
+
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
247
|
+
'xls': 'application/vnd.ms-excel',
|
|
248
|
+
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
249
|
+
'ppt': 'application/vnd.ms-powerpoint',
|
|
250
|
+
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
251
|
+
'txt': 'text/plain',
|
|
252
|
+
'jpg': 'image/jpeg',
|
|
253
|
+
'jpeg': 'image/jpeg',
|
|
254
|
+
'png': 'image/png',
|
|
255
|
+
'gif': 'image/gif',
|
|
256
|
+
'webp': 'image/webp',
|
|
257
|
+
'mp4': 'video/mp4',
|
|
258
|
+
'webm': 'video/webm',
|
|
259
|
+
'ogv': 'video/ogg',
|
|
260
|
+
'zip': 'application/zip',
|
|
261
|
+
'rar': 'application/x-rar-compressed',
|
|
262
|
+
'7z': 'application/x-7z-compressed',
|
|
263
|
+
};
|
|
264
|
+
return mimeMap[ext] || 'application/octet-stream';
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
@@ -12,17 +12,9 @@
|
|
|
12
12
|
<v-row no-gutters class="px-6 pt-4">
|
|
13
13
|
<v-col cols="12" class="mt-2">
|
|
14
14
|
<v-row no-gutters>
|
|
15
|
-
<InputLabel
|
|
16
|
-
class="text-capitalize font-weight-bold"
|
|
17
|
-
title="Name"
|
|
18
|
-
required
|
|
19
|
-
/>
|
|
15
|
+
<InputLabel class="text-capitalize font-weight-bold" title="Name" required />
|
|
20
16
|
<v-col cols="12">
|
|
21
|
-
<v-text-field
|
|
22
|
-
v-model="building.name"
|
|
23
|
-
density="comfortable"
|
|
24
|
-
:rules="[requiredRule]"
|
|
25
|
-
></v-text-field>
|
|
17
|
+
<v-text-field v-model="building.name" density="comfortable" :rules="[requiredRule]"></v-text-field>
|
|
26
18
|
</v-col>
|
|
27
19
|
</v-row>
|
|
28
20
|
</v-col>
|
|
@@ -31,51 +23,30 @@
|
|
|
31
23
|
<v-row>
|
|
32
24
|
<v-col cols="6" class="mt-2">
|
|
33
25
|
<v-row no-gutters>
|
|
34
|
-
<InputLabel
|
|
35
|
-
class="text-capitalize font-weight-bold"
|
|
36
|
-
title="Block"
|
|
37
|
-
required
|
|
38
|
-
/>
|
|
26
|
+
<InputLabel class="text-capitalize font-weight-bold" title="Block" required />
|
|
39
27
|
<v-col cols="12">
|
|
40
|
-
<v-select
|
|
41
|
-
|
|
42
|
-
:items="blocks"
|
|
43
|
-
item-title="label"
|
|
44
|
-
item-value="value"
|
|
45
|
-
density="comfortable"
|
|
46
|
-
:disabled="prop.mode === 'edit'"
|
|
47
|
-
:rules="[
|
|
28
|
+
<v-select v-model.number="building.block" :items="blocks" item-title="label" item-value="value"
|
|
29
|
+
density="comfortable" :disabled="prop.mode === 'edit'" :rules="[
|
|
48
30
|
requiredRule,
|
|
49
31
|
() =>
|
|
50
32
|
(building.block && building.block > 0) ||
|
|
51
33
|
'Block is required',
|
|
52
|
-
]"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</v-select>
|
|
34
|
+
]" type="number">
|
|
35
|
+
<template v-slot:item="{ props: itemProps, item }">
|
|
36
|
+
<v-list-item v-bind="itemProps" :disabled="item?.raw?.disabled"></v-list-item>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
</v-select>
|
|
60
40
|
</v-col>
|
|
61
41
|
</v-row>
|
|
62
42
|
</v-col>
|
|
63
43
|
|
|
64
44
|
<v-col cols="6" class="mt-2">
|
|
65
45
|
<v-row no-gutters>
|
|
66
|
-
<InputLabel
|
|
67
|
-
class="text-capitalize font-weight-bold"
|
|
68
|
-
title="Levels"
|
|
69
|
-
required
|
|
70
|
-
/>
|
|
46
|
+
<InputLabel class="text-capitalize font-weight-bold" title="Levels" required />
|
|
71
47
|
<v-col cols="12">
|
|
72
|
-
<v-text-field
|
|
73
|
-
|
|
74
|
-
density="comfortable"
|
|
75
|
-
:rules="[requiredRule]"
|
|
76
|
-
type="number"
|
|
77
|
-
@update:model-value="setBuildingLevels()"
|
|
78
|
-
></v-text-field>
|
|
48
|
+
<v-text-field v-model.number="buildingLevels" density="comfortable" :rules="[requiredRule]"
|
|
49
|
+
type="number" @update:model-value="setBuildingLevels()"></v-text-field>
|
|
79
50
|
</v-col>
|
|
80
51
|
</v-row>
|
|
81
52
|
</v-col>
|
|
@@ -85,14 +56,8 @@
|
|
|
85
56
|
<v-col v-if="buildingLevels" cols="12">
|
|
86
57
|
<v-row justify="center">
|
|
87
58
|
<v-col cols="6">
|
|
88
|
-
<v-btn
|
|
89
|
-
|
|
90
|
-
color="primary"
|
|
91
|
-
variant="text"
|
|
92
|
-
class="text-none font-weight-bold"
|
|
93
|
-
text="Set level labels"
|
|
94
|
-
@click="show = !show"
|
|
95
|
-
></v-btn>
|
|
59
|
+
<v-btn block color="primary" variant="text" class="text-none font-weight-bold" text="Set level labels"
|
|
60
|
+
@click="show = !show"></v-btn>
|
|
96
61
|
</v-col>
|
|
97
62
|
</v-row>
|
|
98
63
|
</v-col>
|
|
@@ -100,21 +65,12 @@
|
|
|
100
65
|
|
|
101
66
|
<v-expand-transition>
|
|
102
67
|
<v-row v-show="show" no-gutters class="px-6">
|
|
103
|
-
<template
|
|
104
|
-
v-for="(level, levelIndex) in building.levels"
|
|
105
|
-
:key="levelIndex"
|
|
106
|
-
>
|
|
68
|
+
<template v-for="(level, levelIndex) in building.levels" :key="levelIndex">
|
|
107
69
|
<v-col cols="12">
|
|
108
70
|
<v-row no-gutters>
|
|
109
|
-
<InputLabel
|
|
110
|
-
class="text-capitalize font-weight-bold"
|
|
111
|
-
:title="`Level ${levelIndex + 1}`"
|
|
112
|
-
/>
|
|
71
|
+
<InputLabel class="text-capitalize font-weight-bold" :title="`Level ${levelIndex + 1}`" />
|
|
113
72
|
<v-col cols="12">
|
|
114
|
-
<v-text-field
|
|
115
|
-
v-model.trim="building.levels[levelIndex]"
|
|
116
|
-
density="comfortable"
|
|
117
|
-
></v-text-field>
|
|
73
|
+
<v-text-field v-model.trim="building.levels[levelIndex]" density="comfortable"></v-text-field>
|
|
118
74
|
</v-col>
|
|
119
75
|
</v-row>
|
|
120
76
|
</v-col>
|
|
@@ -122,10 +78,10 @@
|
|
|
122
78
|
</v-row>
|
|
123
79
|
</v-expand-transition>
|
|
124
80
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
81
|
+
<v-col cols="12" class="px-6">
|
|
82
|
+
<InputLabel class="text-capitalize" title="Building Floor plan" />
|
|
83
|
+
<InputFileV2 v-model="building.buildingFiles" :multiple="true" accept="*" :max-length="10" title="Upload Files" />
|
|
84
|
+
</v-col>
|
|
129
85
|
|
|
130
86
|
<v-col cols="12" class="mt-2">
|
|
131
87
|
<v-checkbox v-model="createMore" density="comfortable" hide-details>
|
|
@@ -140,9 +96,7 @@
|
|
|
140
96
|
<v-col cols="12">
|
|
141
97
|
<v-row no-gutters>
|
|
142
98
|
<v-col cols="12" class="text-center">
|
|
143
|
-
<span
|
|
144
|
-
class="text-none text-subtitle-2 font-weight-medium text-error"
|
|
145
|
-
>
|
|
99
|
+
<span class="text-none text-subtitle-2 font-weight-medium text-error">
|
|
146
100
|
{{ message }}
|
|
147
101
|
</span>
|
|
148
102
|
</v-col>
|
|
@@ -154,31 +108,14 @@
|
|
|
154
108
|
<v-toolbar density="compact">
|
|
155
109
|
<v-row no-gutters>
|
|
156
110
|
<v-col cols="6">
|
|
157
|
-
<v-btn
|
|
158
|
-
tile
|
|
159
|
-
block
|
|
160
|
-
variant="text"
|
|
161
|
-
class="text-none"
|
|
162
|
-
size="48"
|
|
163
|
-
@click="cancel"
|
|
164
|
-
:disabled="disable"
|
|
165
|
-
>
|
|
111
|
+
<v-btn tile block variant="text" class="text-none" size="48" @click="cancel" :disabled="disable">
|
|
166
112
|
Cancel
|
|
167
113
|
</v-btn>
|
|
168
114
|
</v-col>
|
|
169
115
|
|
|
170
116
|
<v-col cols="6">
|
|
171
|
-
<v-btn
|
|
172
|
-
|
|
173
|
-
block
|
|
174
|
-
variant="flat"
|
|
175
|
-
color="black"
|
|
176
|
-
class="text-none"
|
|
177
|
-
size="48"
|
|
178
|
-
:disabled="!validForm || disable"
|
|
179
|
-
@click="submit"
|
|
180
|
-
:loading="disable"
|
|
181
|
-
>
|
|
117
|
+
<v-btn tile block variant="flat" color="black" class="text-none" size="48" :disabled="!validForm || disable"
|
|
118
|
+
@click="submit" :loading="disable">
|
|
182
119
|
Submit
|
|
183
120
|
</v-btn>
|
|
184
121
|
</v-col>
|
|
@@ -204,7 +141,7 @@ const prop = defineProps({
|
|
|
204
141
|
name: "",
|
|
205
142
|
block: null,
|
|
206
143
|
levels: [],
|
|
207
|
-
|
|
144
|
+
buildingFiles: []
|
|
208
145
|
}),
|
|
209
146
|
},
|
|
210
147
|
blocks: {
|
|
@@ -214,6 +151,7 @@ const prop = defineProps({
|
|
|
214
151
|
});
|
|
215
152
|
|
|
216
153
|
const { getSiteById } = useSite();
|
|
154
|
+
const { getFileById } = useFile();
|
|
217
155
|
|
|
218
156
|
const site = ref<TSite | null>(null);
|
|
219
157
|
|
|
@@ -235,7 +173,7 @@ const blocks = computed(() => {
|
|
|
235
173
|
const array = Array.from({ length: site.value?.metadata?.block || 0 }, (_, i) => i + 1);
|
|
236
174
|
|
|
237
175
|
const formattedArray = array.map((num) => {
|
|
238
|
-
const isDisabled = prop.blocks.some((block) => block.block === num)
|
|
176
|
+
const isDisabled = prop.blocks.some((block) => block.block === num)
|
|
239
177
|
return {
|
|
240
178
|
label: num.toString(),
|
|
241
179
|
value: num,
|
|
@@ -266,15 +204,43 @@ const building = ref<TBuilding>({
|
|
|
266
204
|
name: "",
|
|
267
205
|
block: null,
|
|
268
206
|
levels: [],
|
|
269
|
-
|
|
207
|
+
buildingFiles: []
|
|
270
208
|
});
|
|
271
209
|
|
|
210
|
+
// Normalize buildingFiles: extract IDs for InputFileV2, keep names in a separate map
|
|
211
|
+
const buildingFilesNames = ref<Record<string, string>>({});
|
|
212
|
+
|
|
272
213
|
building.value.site = prop.site;
|
|
273
214
|
if (prop.mode === "edit") {
|
|
274
215
|
building.value = JSON.parse(JSON.stringify(prop.building));
|
|
275
216
|
buildingLevels.value = building.value.levels.length;
|
|
217
|
+
|
|
218
|
+
const rawFiles = building.value.buildingFiles as any[];
|
|
219
|
+
if (rawFiles?.length && typeof rawFiles[0] === 'object') {
|
|
220
|
+
rawFiles.forEach((f: { id: string; name: string }) => {
|
|
221
|
+
buildingFilesNames.value[f.id] = f.name ?? "";
|
|
222
|
+
});
|
|
223
|
+
building.value.buildingFiles = rawFiles.map((f: { id: string }) => f.id);
|
|
224
|
+
}
|
|
276
225
|
}
|
|
277
226
|
|
|
227
|
+
watch(
|
|
228
|
+
() => building.value.buildingFiles as string[],
|
|
229
|
+
async (ids) => {
|
|
230
|
+
for (const id of ids) {
|
|
231
|
+
if (!buildingFilesNames.value[id]) {
|
|
232
|
+
try {
|
|
233
|
+
const meta = await getFileById(id) as any;
|
|
234
|
+
buildingFilesNames.value[id] = meta?.data?.name ?? meta?.name ?? "";
|
|
235
|
+
} catch {
|
|
236
|
+
buildingFilesNames.value[id] = "";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{ deep: true }
|
|
242
|
+
);
|
|
243
|
+
|
|
278
244
|
const createMore = ref(false);
|
|
279
245
|
const disable = ref(false);
|
|
280
246
|
|
|
@@ -289,7 +255,9 @@ async function submit() {
|
|
|
289
255
|
|
|
290
256
|
try {
|
|
291
257
|
if (prop.mode === "add") {
|
|
292
|
-
|
|
258
|
+
const buildingPayload = { ...building.value };
|
|
259
|
+
buildingPayload.buildingFiles = (building.value.buildingFiles as string[] || []).map((id) => ({ id, name: buildingFilesNames.value[id] ?? "" }));
|
|
260
|
+
await createBuilding(buildingPayload);
|
|
293
261
|
}
|
|
294
262
|
|
|
295
263
|
if (prop.mode === "edit") {
|
|
@@ -297,7 +265,7 @@ async function submit() {
|
|
|
297
265
|
name: building.value.name,
|
|
298
266
|
block: building.value.block,
|
|
299
267
|
levels: building.value.levels,
|
|
300
|
-
|
|
268
|
+
buildingFiles: (building.value.buildingFiles as string[] || []).map((id) => ({ id, name: buildingFilesNames.value[id] ?? "" }))
|
|
301
269
|
});
|
|
302
270
|
}
|
|
303
271
|
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
</v-toolbar>
|
|
29
29
|
|
|
30
30
|
<v-data-table :headers="headers" :items="items" item-value="_id" items-per-page="20" fixed-header
|
|
31
|
-
hide-default-footer
|
|
32
|
-
style="max-height: calc(100vh - (200px))">
|
|
31
|
+
hide-default-footer @click:row="tableRowClickHandler" style="max-height: calc(100vh - (200px))">
|
|
33
32
|
<template #item.createdAt="{ value }">
|
|
34
33
|
{{ new Date(value).toLocaleDateString() }}
|
|
35
34
|
</template>
|
|
@@ -70,14 +69,13 @@
|
|
|
70
69
|
<strong>Levels:</strong>
|
|
71
70
|
{{ selectedBuilding?.levels.length ?? "N/A" }}
|
|
72
71
|
</v-col>
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</template> -->
|
|
72
|
+
<v-col cols="12">
|
|
73
|
+
<strong>Files:</strong>
|
|
74
|
+
<div v-if="selectedBuilding?.buildingFiles?.length" class="">
|
|
75
|
+
<AttachmentsViewer :files="selectedBuilding.buildingFiles" title="" show-file-type-title />
|
|
76
|
+
</div>
|
|
77
|
+
<span v-else> None</span>
|
|
78
|
+
</v-col>
|
|
81
79
|
</v-row>
|
|
82
80
|
</v-card-text>
|
|
83
81
|
|
|
@@ -153,6 +151,7 @@
|
|
|
153
151
|
</v-col>
|
|
154
152
|
</v-row>
|
|
155
153
|
</v-toolbar>
|
|
154
|
+
|
|
156
155
|
</v-card>
|
|
157
156
|
</v-dialog>
|
|
158
157
|
|
|
@@ -270,7 +269,7 @@ const selectedBuilding = ref<TBuilding>({
|
|
|
270
269
|
name: "",
|
|
271
270
|
block: null,
|
|
272
271
|
levels: [],
|
|
273
|
-
|
|
272
|
+
buildingFiles: [],
|
|
274
273
|
});
|
|
275
274
|
|
|
276
275
|
function tableRowClickHandler(_: any, data: any) {
|
|
@@ -343,7 +342,7 @@ function setBuilding({
|
|
|
343
342
|
}
|
|
344
343
|
</script>
|
|
345
344
|
<style scoped>
|
|
346
|
-
|
|
345
|
+
.v-data-table :deep(thead th) {
|
|
347
346
|
font-weight: bold !important;
|
|
348
347
|
}
|
|
349
348
|
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog :model-value="modelValue" :max-width="fileType === 'image' ? '1200' : '900'" scrollable
|
|
3
|
+
@update:model-value="emit('update:modelValue', $event)">
|
|
4
|
+
<v-card>
|
|
5
|
+
<v-toolbar density="compact">
|
|
6
|
+
<v-toolbar-title class="text-subtitle-2 text-truncate">{{ fileName }}</v-toolbar-title>
|
|
7
|
+
<v-spacer />
|
|
8
|
+
<v-btn icon="mdi-open-in-new" variant="text" size="small" @click="openInNewTab()" />
|
|
9
|
+
<v-btn icon="mdi-close" variant="text" size="small" @click="emit('update:modelValue', false)" />
|
|
10
|
+
</v-toolbar>
|
|
11
|
+
<v-card-text class="pa-0"
|
|
12
|
+
:style="{ height: fileType === 'image' ? 'auto' : '75vh', 'overflow-y': fileType === 'image' ? 'auto' : 'auto' }">
|
|
13
|
+
<!-- Image Viewer -->
|
|
14
|
+
<v-img v-if="fileType === 'image'" :src="getFileUrl(fileId)" :alt="fileName" class="w-100" />
|
|
15
|
+
|
|
16
|
+
<!-- Video Viewer -->
|
|
17
|
+
<video v-else-if="fileType === 'video'" width="100%" height="100%" controls
|
|
18
|
+
style="background-color: #000;">
|
|
19
|
+
<source :src="getFileUrl(fileId)" :type="mimeType" />
|
|
20
|
+
Your browser does not support the video tag.
|
|
21
|
+
</video>
|
|
22
|
+
|
|
23
|
+
<!-- Document Viewer (PDF, Word, etc.) -->
|
|
24
|
+
<iframe v-else :src="getFileUrl(fileId)" width="100%" height="100%" style="border: none;"
|
|
25
|
+
:title="fileName" />
|
|
26
|
+
</v-card-text>
|
|
27
|
+
<v-card-actions>
|
|
28
|
+
<v-spacer />
|
|
29
|
+
<v-btn variant="text" class="text-none" @click="openInNewTab()">
|
|
30
|
+
<v-icon start>mdi-download</v-icon>
|
|
31
|
+
Open / Download
|
|
32
|
+
</v-btn>
|
|
33
|
+
</v-card-actions>
|
|
34
|
+
</v-card>
|
|
35
|
+
</v-dialog>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
const props = defineProps({
|
|
40
|
+
modelValue: {
|
|
41
|
+
type: Boolean,
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
44
|
+
fileId: {
|
|
45
|
+
type: String,
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
fileName: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: "Document",
|
|
51
|
+
},
|
|
52
|
+
mimeType: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: "application/octet-stream",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const emit = defineEmits(['update:modelValue']);
|
|
59
|
+
|
|
60
|
+
const { getFileUrl } = useFile();
|
|
61
|
+
|
|
62
|
+
const fileType = computed(() => {
|
|
63
|
+
if (props.mimeType.startsWith('image/')) {
|
|
64
|
+
return 'image';
|
|
65
|
+
} else if (props.mimeType.startsWith('video/')) {
|
|
66
|
+
return 'video';
|
|
67
|
+
} else {
|
|
68
|
+
return 'document';
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function openInNewTab() {
|
|
73
|
+
window.open(getFileUrl(props.fileId), '_blank', 'noopener,noreferrer');
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
@@ -109,6 +109,22 @@
|
|
|
109
109
|
>
|
|
110
110
|
</template>
|
|
111
111
|
|
|
112
|
+
<template v-slot:item.email="{ value }">
|
|
113
|
+
{{ value ?? "N/A" }}
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template v-slot:item.invited-by="{ item }">
|
|
117
|
+
<span class="d-flex align-center ga-2">
|
|
118
|
+
<AvatarMain
|
|
119
|
+
v-if="item?.inviterName"
|
|
120
|
+
:name="item?.inviterName"
|
|
121
|
+
:size="20"
|
|
122
|
+
:id="item?._id"
|
|
123
|
+
/>
|
|
124
|
+
<span class="text-capitalize">{{ item?.inviterName ?? "N/A" }}</span>
|
|
125
|
+
</span>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
112
128
|
<template v-slot:item.type-company="{ item }">
|
|
113
129
|
<span class="d-flex align-center ga-2">
|
|
114
130
|
<v-icon icon="mdi-user" size="15" />
|
|
@@ -301,11 +317,11 @@
|
|
|
301
317
|
<v-row no-gutters class="d-flex flex-column py-2">
|
|
302
318
|
<span class="d-flex align-center ga-2">
|
|
303
319
|
<v-icon icon="mdi-calendar" size="20" />
|
|
304
|
-
{{ standardFormatDate(item.expectedCheckIn)
|
|
320
|
+
{{ standardFormatDate(item.expectedCheckIn) }}
|
|
305
321
|
</span>
|
|
306
322
|
<span class="d-flex align-center ga-2">
|
|
307
323
|
<v-icon icon="mdi-clock-time-four-outline" size="20" />
|
|
308
|
-
{{
|
|
324
|
+
{{ item.arrivalTime ?? "N/A" }}
|
|
309
325
|
</span>
|
|
310
326
|
|
|
311
327
|
<span v-if="!item.checkIn && canUpdateVisitor">
|
|
@@ -320,13 +336,26 @@
|
|
|
320
336
|
class="text-capitalize"
|
|
321
337
|
color="success"
|
|
322
338
|
text="Checkin"
|
|
323
|
-
@click.stop="
|
|
339
|
+
@click.stop="handleCheckin(item)"
|
|
324
340
|
/>
|
|
325
341
|
</span>
|
|
326
342
|
</span>
|
|
327
343
|
</v-row>
|
|
328
344
|
</template>
|
|
329
345
|
|
|
346
|
+
<template v-slot:item.status="{ item }">
|
|
347
|
+
<v-card
|
|
348
|
+
v-if="item?.overnightParking?.status"
|
|
349
|
+
size="small"
|
|
350
|
+
class="rounded-xl text-capitalize text-center elevation-0 py-1 px-2"
|
|
351
|
+
:color="getOvernightParkingRequestStatusColor(item?.overnightParking?.status as string)"
|
|
352
|
+
style="max-width: 150px; margin: auto"
|
|
353
|
+
>
|
|
354
|
+
{{ item?.overnightParking?.status }}
|
|
355
|
+
</v-card>
|
|
356
|
+
<span v-else> N/A </span>
|
|
357
|
+
</template>
|
|
358
|
+
|
|
330
359
|
<template v-slot:item.action="{ item }">
|
|
331
360
|
<v-btn
|
|
332
361
|
v-if="activeTab === 'unregistered' && canAddVisitor"
|
|
@@ -335,6 +364,52 @@
|
|
|
335
364
|
text="Register"
|
|
336
365
|
@click.stop="handleRegistrationUnregisteredVisitor(item)"
|
|
337
366
|
/>
|
|
367
|
+
<v-menu v-else-if="activeTab === 'overnight-parking'">
|
|
368
|
+
<template #activator="{ props }">
|
|
369
|
+
<v-avatar class="rounded-xl border-md">
|
|
370
|
+
<v-icon v-bind="props" icon="mdi-dots-vertical" />
|
|
371
|
+
</v-avatar>
|
|
372
|
+
</template>
|
|
373
|
+
|
|
374
|
+
<v-card>
|
|
375
|
+
<v-list-item
|
|
376
|
+
@click.stop="
|
|
377
|
+
openApproveRejectOverNightParkingRequestDialog(
|
|
378
|
+
item,
|
|
379
|
+
overNightParkingListActions[0].status
|
|
380
|
+
)
|
|
381
|
+
"
|
|
382
|
+
:disabled="
|
|
383
|
+
item?.overnightParking?.status == 'approved' ||
|
|
384
|
+
!overNightParkingListActions[0].disabled
|
|
385
|
+
"
|
|
386
|
+
>
|
|
387
|
+
<template #title>
|
|
388
|
+
<span class="text-caption">
|
|
389
|
+
{{ overNightParkingListActions[0].text }}
|
|
390
|
+
</span>
|
|
391
|
+
</template>
|
|
392
|
+
</v-list-item>
|
|
393
|
+
<v-divider />
|
|
394
|
+
<v-list-item
|
|
395
|
+
v-if="item?.overnightParking?.status !== 'approved'"
|
|
396
|
+
@click="
|
|
397
|
+
openApproveRejectOverNightParkingRequestDialog(
|
|
398
|
+
item,
|
|
399
|
+
overNightParkingListActions[1].status
|
|
400
|
+
)
|
|
401
|
+
"
|
|
402
|
+
:disabled="!overNightParkingListActions[1].disabled"
|
|
403
|
+
>
|
|
404
|
+
<template #title>
|
|
405
|
+
<span class="text-caption">
|
|
406
|
+
{{ overNightParkingListActions[1].text }}
|
|
407
|
+
</span>
|
|
408
|
+
</template>
|
|
409
|
+
</v-list-item>
|
|
410
|
+
<v-divider />
|
|
411
|
+
</v-card>
|
|
412
|
+
</v-menu>
|
|
338
413
|
</template>
|
|
339
414
|
|
|
340
415
|
<template v-slot:item.checkInRemarks="{ item }">
|
|
@@ -401,9 +476,29 @@
|
|
|
401
476
|
<template v-slot:content>
|
|
402
477
|
<v-row no-gutters class="mb-4">
|
|
403
478
|
<v-col v-for="(label, key) in formattedFields" :key="key" cols="12">
|
|
479
|
+
<span
|
|
480
|
+
v-if="
|
|
481
|
+
key === 'checkIn' &&
|
|
482
|
+
!selectedVisitorObject[key] &&
|
|
483
|
+
canUpdateVisitor
|
|
484
|
+
"
|
|
485
|
+
class="d-flex align-center"
|
|
486
|
+
>
|
|
487
|
+
<strong>{{ label }}:</strong>
|
|
488
|
+
<v-btn
|
|
489
|
+
size="x-small"
|
|
490
|
+
class="ml-3 text-capitalize"
|
|
491
|
+
color="success"
|
|
492
|
+
text="Checkin"
|
|
493
|
+
:disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
|
|
494
|
+
@click="handleCheckin(selectedVisitorObject)"
|
|
495
|
+
:loading="isVisitorDataFromScannedQRCodeCheckingInOut"
|
|
496
|
+
/>
|
|
497
|
+
</span>
|
|
404
498
|
<span
|
|
405
499
|
v-if="
|
|
406
500
|
key === 'checkOut' &&
|
|
501
|
+
selectedVisitorObject['checkIn'] &&
|
|
407
502
|
!selectedVisitorObject[key] &&
|
|
408
503
|
canCheckoutVisitor
|
|
409
504
|
"
|
|
@@ -716,12 +811,90 @@
|
|
|
716
811
|
/>
|
|
717
812
|
</v-dialog>
|
|
718
813
|
|
|
814
|
+
<v-dialog
|
|
815
|
+
v-model="dialog.approveRejectOvernightParkingRequestDialog"
|
|
816
|
+
transition="dialog-bottom-transition"
|
|
817
|
+
persistent
|
|
818
|
+
width="60vh"
|
|
819
|
+
>
|
|
820
|
+
<v-card>
|
|
821
|
+
<v-toolbar density="compact">
|
|
822
|
+
<v-row
|
|
823
|
+
no-gutters
|
|
824
|
+
class="d-flex fill-height justify-space-between align-center px-4"
|
|
825
|
+
>
|
|
826
|
+
<span class="font-weight-bold"> Overnight Parking Request </span>
|
|
827
|
+
<v-btn
|
|
828
|
+
icon="mdi-close"
|
|
829
|
+
variant="text"
|
|
830
|
+
@click="dialog.approveRejectOvernightParkingRequestDialog = false"
|
|
831
|
+
/>
|
|
832
|
+
</v-row>
|
|
833
|
+
</v-toolbar>
|
|
834
|
+
<v-card-text class="px-4 pb-2">
|
|
835
|
+
<v-row no-gutters justify="center" align-content="center">
|
|
836
|
+
<v-col
|
|
837
|
+
v-if="
|
|
838
|
+
approveRejectOvernightParkingRequestDialog.status !== 'remarks'
|
|
839
|
+
"
|
|
840
|
+
cols="12"
|
|
841
|
+
class="text-h6 text-center"
|
|
842
|
+
>
|
|
843
|
+
{{
|
|
844
|
+
`Are you sure you want to ${approveRejectOvernightParkingRequestDialog.text} the visitor's overnight parking request?`
|
|
845
|
+
}}
|
|
846
|
+
</v-col>
|
|
847
|
+
|
|
848
|
+
<v-col cols="12" md="8" class="mt-4">
|
|
849
|
+
<v-textarea
|
|
850
|
+
v-model="overNightParkingRequestApproveRejectRemarks"
|
|
851
|
+
label="Remarks"
|
|
852
|
+
placeholder="Enter remarks"
|
|
853
|
+
outlined
|
|
854
|
+
rows="3"
|
|
855
|
+
clearable
|
|
856
|
+
/>
|
|
857
|
+
</v-col>
|
|
858
|
+
</v-row>
|
|
859
|
+
</v-card-text>
|
|
860
|
+
<v-toolbar class="pa-0" density="compact">
|
|
861
|
+
<v-row no-gutters>
|
|
862
|
+
<v-col cols="6">
|
|
863
|
+
<v-btn
|
|
864
|
+
text="No"
|
|
865
|
+
variant="text"
|
|
866
|
+
block
|
|
867
|
+
:disabled="isApprovingRejectingOvernightParkingRequest"
|
|
868
|
+
@click="closeApproveRejectOvernightParkingDialog"
|
|
869
|
+
/>
|
|
870
|
+
</v-col>
|
|
871
|
+
<v-col cols="6">
|
|
872
|
+
<v-btn
|
|
873
|
+
text="Yes"
|
|
874
|
+
color="success"
|
|
875
|
+
variant="flat"
|
|
876
|
+
height="48"
|
|
877
|
+
rounded="0"
|
|
878
|
+
block
|
|
879
|
+
:disabled="
|
|
880
|
+
!overNightParkingRequestApproveRejectRemarks ||
|
|
881
|
+
isApprovingRejectingOvernightParkingRequest
|
|
882
|
+
"
|
|
883
|
+
@click="updateOvernightParkingRequestStatus"
|
|
884
|
+
:loading="isApprovingRejectingOvernightParkingRequest"
|
|
885
|
+
/>
|
|
886
|
+
</v-col>
|
|
887
|
+
</v-row>
|
|
888
|
+
</v-toolbar>
|
|
889
|
+
</v-card>
|
|
890
|
+
</v-dialog>
|
|
891
|
+
|
|
719
892
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
720
893
|
</v-row>
|
|
721
894
|
</template>
|
|
722
895
|
|
|
723
896
|
<script setup lang="ts">
|
|
724
|
-
import
|
|
897
|
+
import moment from "moment-timezone";
|
|
725
898
|
|
|
726
899
|
definePageMeta({
|
|
727
900
|
middleware: ["01-auth", "02-org"],
|
|
@@ -764,7 +937,6 @@ const {
|
|
|
764
937
|
debounce,
|
|
765
938
|
formatCamelCaseToWords,
|
|
766
939
|
formatDate,
|
|
767
|
-
formatTime,
|
|
768
940
|
UTCToLocalTIme,
|
|
769
941
|
standardFormatDate,
|
|
770
942
|
} = useUtils();
|
|
@@ -802,6 +974,7 @@ const messageSnackbar = ref(false);
|
|
|
802
974
|
|
|
803
975
|
const loading = reactive({
|
|
804
976
|
fetchingVisitors: false,
|
|
977
|
+
checkingIn: false,
|
|
805
978
|
checkingOut: false,
|
|
806
979
|
savingRemarks: false,
|
|
807
980
|
updatingPassKeys: false,
|
|
@@ -818,6 +991,7 @@ const dialog = reactive({
|
|
|
818
991
|
editPassKey: false,
|
|
819
992
|
scanVisitorQRCode: false,
|
|
820
993
|
showVisitorDataFromScannedQRCode: false,
|
|
994
|
+
approveRejectOvernightParkingRequestDialog: false,
|
|
821
995
|
});
|
|
822
996
|
|
|
823
997
|
const snapshotImageUrl = ref("");
|
|
@@ -825,32 +999,141 @@ const remarksType = ref<"checkIn" | "checkOut">("checkIn");
|
|
|
825
999
|
const remarksInput = ref("");
|
|
826
1000
|
const remarksViewOnly = ref(false);
|
|
827
1001
|
|
|
828
|
-
const headers = computed(() =>
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
]
|
|
1002
|
+
const headers = computed(() => {
|
|
1003
|
+
const tab = activeTab.value;
|
|
1004
|
+
|
|
1005
|
+
// 1. Define the specific headers for Overnight Parking
|
|
1006
|
+
if (tab === "overnight-parking") {
|
|
1007
|
+
return [
|
|
1008
|
+
{ title: "Name", value: "name" },
|
|
1009
|
+
{ title: "Email", value: "email" },
|
|
1010
|
+
{ title: "Invited By", value: "invited-by" },
|
|
1011
|
+
{ title: "Arrival Date/Time", value: "arrival-date-time" },
|
|
1012
|
+
{ title: "Check In/Out", value: "checkin-out" },
|
|
1013
|
+
{ title: "Status", value: "status" },
|
|
1014
|
+
{ title: "Action", value: "action" },
|
|
1015
|
+
];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// 2. Define logic for all other tabs
|
|
1019
|
+
const list = [{ title: "Name", value: "name" }];
|
|
1020
|
+
|
|
1021
|
+
// Standard info for non-parking tabs
|
|
1022
|
+
list.push(
|
|
1023
|
+
{ title: "Type/Company", value: "type-company" },
|
|
1024
|
+
{ title: "Location", value: "location" },
|
|
1025
|
+
{ title: "Contact/Vehicle No.", value: "contact-vehicleNumber" }
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
// Conditional logic for Guests vs Others
|
|
1029
|
+
if (tab === "guests") {
|
|
1030
|
+
list.push({ title: "Arrival Date/Time", value: "arrival-date-time" });
|
|
1031
|
+
} else {
|
|
1032
|
+
list.push({ title: "Check In/Out", value: "checkin-out" });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Specific additions
|
|
1036
|
+
if (tab === "resident-transactions") {
|
|
1037
|
+
list.push(
|
|
1038
|
+
{ title: "Check-in Remarks", value: "checkInRemarks" },
|
|
1039
|
+
{ title: "Check-out Remarks", value: "checkOutRemarks" }
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (tab === "unregistered") {
|
|
1044
|
+
list.push({ title: "Action", value: "action" });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return list;
|
|
1048
|
+
});
|
|
846
1049
|
|
|
847
1050
|
const tabOptions = [
|
|
848
1051
|
{ name: "Registered", value: "registered" },
|
|
849
1052
|
{ name: "Unregistered", value: "unregistered" },
|
|
850
1053
|
{ name: "Guests", value: "guests" },
|
|
851
1054
|
{ name: "Resident Transactions", value: "resident-transactions" },
|
|
1055
|
+
{ name: "Overnight Parking", value: "overnight-parking" },
|
|
852
1056
|
];
|
|
853
1057
|
|
|
1058
|
+
function getOvernightParkingRequestStatusColor(status: string) {
|
|
1059
|
+
switch (status) {
|
|
1060
|
+
case "approved":
|
|
1061
|
+
return "success";
|
|
1062
|
+
case "rejected":
|
|
1063
|
+
return "error";
|
|
1064
|
+
default:
|
|
1065
|
+
return "grey";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const overNightParkingListActions = [
|
|
1070
|
+
{
|
|
1071
|
+
text: "Approve",
|
|
1072
|
+
status: "approved",
|
|
1073
|
+
disabled: props.canUpdateVisitor,
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
text: "Reject",
|
|
1077
|
+
status: "rejected",
|
|
1078
|
+
disabled: props.canUpdateVisitor,
|
|
1079
|
+
},
|
|
1080
|
+
];
|
|
1081
|
+
const overNightParkingRequestApproveRejectRemarks = ref("");
|
|
1082
|
+
const approveRejectOvernightParkingRequestDialog = ref({
|
|
1083
|
+
text: "",
|
|
1084
|
+
status: "",
|
|
1085
|
+
});
|
|
1086
|
+
const selectedOvernightParkingRequest = ref<Record<string, any>>({});
|
|
1087
|
+
|
|
1088
|
+
function openApproveRejectOverNightParkingRequestDialog(
|
|
1089
|
+
visitor: Record<string, any>,
|
|
1090
|
+
status: string
|
|
1091
|
+
) {
|
|
1092
|
+
selectedOvernightParkingRequest.value = visitor;
|
|
1093
|
+
approveRejectOvernightParkingRequestDialog.value = {
|
|
1094
|
+
text: status === "approved" ? "Approve" : "Reject",
|
|
1095
|
+
status,
|
|
1096
|
+
};
|
|
1097
|
+
dialog.approveRejectOvernightParkingRequestDialog = true;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const isApprovingRejectingOvernightParkingRequest = ref(false);
|
|
1101
|
+
|
|
1102
|
+
async function updateOvernightParkingRequestStatus() {
|
|
1103
|
+
isApprovingRejectingOvernightParkingRequest.value = true;
|
|
1104
|
+
try {
|
|
1105
|
+
await visitorDataFromScannedQRCodeCheckIn({
|
|
1106
|
+
type: "overnight-parking",
|
|
1107
|
+
filter: {
|
|
1108
|
+
_id: selectedOvernightParkingRequest.value._id,
|
|
1109
|
+
},
|
|
1110
|
+
update: {
|
|
1111
|
+
status: approveRejectOvernightParkingRequestDialog.value.status,
|
|
1112
|
+
remarks: overNightParkingRequestApproveRejectRemarks.value ?? "",
|
|
1113
|
+
updatedBy: currentUser.value?._id,
|
|
1114
|
+
},
|
|
1115
|
+
userType: "site",
|
|
1116
|
+
});
|
|
1117
|
+
showMessage(
|
|
1118
|
+
`Visitor's overnight parking request successfully ${approveRejectOvernightParkingRequestDialog.value.status}`,
|
|
1119
|
+
"success"
|
|
1120
|
+
);
|
|
1121
|
+
await getVisitorRefresh();
|
|
1122
|
+
closeApproveRejectOvernightParkingDialog();
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
showMessage(errorConverter(error), "error");
|
|
1125
|
+
} finally {
|
|
1126
|
+
isApprovingRejectingOvernightParkingRequest.value = false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function closeApproveRejectOvernightParkingDialog() {
|
|
1131
|
+
dialog.approveRejectOvernightParkingRequestDialog = false;
|
|
1132
|
+
selectedVisitorId.value = "";
|
|
1133
|
+
approveRejectOvernightParkingRequestDialog.value = { text: "", status: "" };
|
|
1134
|
+
overNightParkingRequestApproveRejectRemarks.value = "";
|
|
1135
|
+
}
|
|
1136
|
+
|
|
854
1137
|
function normalizeDateOnly(value: string) {
|
|
855
1138
|
if (!value) return "";
|
|
856
1139
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
|
@@ -905,15 +1188,16 @@ function toRoute(tab: any) {
|
|
|
905
1188
|
const obj = tabOptions.find((x) => x.value === tab);
|
|
906
1189
|
if (!obj) return;
|
|
907
1190
|
page.value = 1;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
1191
|
+
getVisitorRefresh();
|
|
1192
|
+
// navigateTo({
|
|
1193
|
+
// name: routeName,
|
|
1194
|
+
// params: {
|
|
1195
|
+
// org: orgId,
|
|
1196
|
+
// },
|
|
1197
|
+
// // query: {
|
|
1198
|
+
// // tab
|
|
1199
|
+
// // },
|
|
1200
|
+
// });
|
|
917
1201
|
}
|
|
918
1202
|
const {
|
|
919
1203
|
data: getVisitorReq,
|
|
@@ -926,8 +1210,8 @@ const {
|
|
|
926
1210
|
page: page.value,
|
|
927
1211
|
site: siteId,
|
|
928
1212
|
search: searchInput.value,
|
|
929
|
-
dateTo:
|
|
930
|
-
dateFrom:
|
|
1213
|
+
dateTo: moment(dateTo.value).endOf("day").toISOString(),
|
|
1214
|
+
dateFrom: moment(dateFrom.value).startOf("day").toISOString(),
|
|
931
1215
|
type:
|
|
932
1216
|
filterTypes.value.length === 0 && activeTab.value === "registered"
|
|
933
1217
|
? "contractor,delivery,walk-in,pick-up,drop-off,guest"
|
|
@@ -941,6 +1225,8 @@ const {
|
|
|
941
1225
|
} else if (activeTab.value === "resident-transactions") {
|
|
942
1226
|
params.status = "registered";
|
|
943
1227
|
params.type = "resident,tenant";
|
|
1228
|
+
} else if (activeTab.value === "overnight-parking") {
|
|
1229
|
+
params.tab = "Overnight Parking";
|
|
944
1230
|
} else {
|
|
945
1231
|
params.status = activeTab.value;
|
|
946
1232
|
}
|
|
@@ -970,7 +1256,7 @@ watch(getVisitorReq, (newData: any) => {
|
|
|
970
1256
|
}
|
|
971
1257
|
});
|
|
972
1258
|
|
|
973
|
-
const selectedVisitorObject = computed(() => {
|
|
1259
|
+
const selectedVisitorObject = computed<Record<string, any>>(() => {
|
|
974
1260
|
const obj = items.value.find((x: any) => x?._id === selectedVisitorId.value);
|
|
975
1261
|
if (!obj) return {};
|
|
976
1262
|
|
|
@@ -1070,6 +1356,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
|
|
|
1070
1356
|
}
|
|
1071
1357
|
handleCloseVisitorDataFromScannedQRCodeDialog();
|
|
1072
1358
|
getVisitorRefresh();
|
|
1359
|
+
dialog.viewVisitor = false;
|
|
1073
1360
|
} catch (error) {
|
|
1074
1361
|
showMessage(errorConverter(error), "error");
|
|
1075
1362
|
} finally {
|
|
@@ -1262,7 +1549,7 @@ const canConfirmCheckout = computed(() => {
|
|
|
1262
1549
|
});
|
|
1263
1550
|
});
|
|
1264
1551
|
|
|
1265
|
-
|
|
1552
|
+
function handleCheckin(user: Record<string, any>) {
|
|
1266
1553
|
visitorDataFromScannedQRCode.value = user;
|
|
1267
1554
|
dialog.showVisitorDataFromScannedQRCode = true;
|
|
1268
1555
|
}
|
|
@@ -90,6 +90,7 @@ export default function () {
|
|
|
90
90
|
status?: string;
|
|
91
91
|
checkedOut?: boolean;
|
|
92
92
|
plateNumber?: string;
|
|
93
|
+
tab?: string;
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
async function getVisitors({
|
|
@@ -106,6 +107,7 @@ export default function () {
|
|
|
106
107
|
// status = "registered"
|
|
107
108
|
plateNumber = "",
|
|
108
109
|
checkedOut,
|
|
110
|
+
tab,
|
|
109
111
|
}: GetVisitorsParams = {}) {
|
|
110
112
|
return await useNuxtApp().$api<Record<string, any>>(
|
|
111
113
|
"/api/visitor-transactions",
|
|
@@ -124,6 +126,7 @@ export default function () {
|
|
|
124
126
|
checkedOut,
|
|
125
127
|
plateNumber,
|
|
126
128
|
order,
|
|
129
|
+
tab,
|
|
127
130
|
},
|
|
128
131
|
}
|
|
129
132
|
);
|
package/package.json
CHANGED