@7365admin1/layer-common 1.11.24 → 1.11.27
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 +18 -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/Input/DatePicker.vue +7 -7
- package/components/VisitorManagement.vue +366 -36
- package/composables/useFacilityUtils.ts +3 -3
- package/composables/useVisitor.ts +3 -0
- package/package.json +1 -1
- package/types/building.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 1.11.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2797c03: updates and fixes
|
|
8
|
+
|
|
9
|
+
## 1.11.26
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d418c67: Update builing management image upload
|
|
14
|
+
|
|
15
|
+
## 1.11.25
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 949ad2b: Update Layer-common version
|
|
20
|
+
|
|
3
21
|
## 1.11.24
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -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>
|
|
@@ -21,7 +21,7 @@ const prop = defineProps({
|
|
|
21
21
|
},
|
|
22
22
|
placeholder: {
|
|
23
23
|
type: String,
|
|
24
|
-
default: 'MM/
|
|
24
|
+
default: 'DD/MM/YYYY'
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
27
|
|
|
@@ -47,12 +47,12 @@ function validate() {
|
|
|
47
47
|
function convertToReadableFormat(dateStr: string): string {
|
|
48
48
|
if (!dateStr) return ""
|
|
49
49
|
const dateObj = new Date(dateStr + "T00:00:00")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
50
|
+
if (Number.isNaN(dateObj.getTime())) return ""
|
|
51
|
+
|
|
52
|
+
const day = String(dateObj.getDate()).padStart(2, "0")
|
|
53
|
+
const month = String(dateObj.getMonth() + 1).padStart(2, "0")
|
|
54
|
+
const year = String(dateObj.getFullYear())
|
|
55
|
+
return `${day}/${month}/${year}`
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function handleInitialDate() {
|
|
@@ -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,74 @@
|
|
|
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 v-bind="props" class="rounded-xl border-md">
|
|
370
|
+
<v-icon icon="mdi-dots-vertical" />
|
|
371
|
+
</v-avatar>
|
|
372
|
+
</template>
|
|
373
|
+
|
|
374
|
+
<v-card>
|
|
375
|
+
<v-list-item
|
|
376
|
+
v-if="
|
|
377
|
+
!overNightParkingListActions[0].disabled ||
|
|
378
|
+
!['approved', 'rejected'].includes(
|
|
379
|
+
item?.overnightParking?.status
|
|
380
|
+
)
|
|
381
|
+
"
|
|
382
|
+
@click.stop="
|
|
383
|
+
openApproveRejectOverNightParkingRequestDialog(
|
|
384
|
+
item,
|
|
385
|
+
overNightParkingListActions[0].status
|
|
386
|
+
)
|
|
387
|
+
"
|
|
388
|
+
>
|
|
389
|
+
<template #title>
|
|
390
|
+
<span class="text-caption">
|
|
391
|
+
{{ overNightParkingListActions[0].text }}
|
|
392
|
+
</span>
|
|
393
|
+
</template>
|
|
394
|
+
</v-list-item>
|
|
395
|
+
<v-divider />
|
|
396
|
+
<v-list-item
|
|
397
|
+
v-if="
|
|
398
|
+
!overNightParkingListActions[1].disabled ||
|
|
399
|
+
!['approved', 'rejected'].includes(
|
|
400
|
+
item?.overnightParking?.status
|
|
401
|
+
)
|
|
402
|
+
"
|
|
403
|
+
@click="
|
|
404
|
+
openApproveRejectOverNightParkingRequestDialog(
|
|
405
|
+
item,
|
|
406
|
+
overNightParkingListActions[1].status
|
|
407
|
+
)
|
|
408
|
+
"
|
|
409
|
+
>
|
|
410
|
+
<template #title>
|
|
411
|
+
<span class="text-caption">
|
|
412
|
+
{{ overNightParkingListActions[1].text }}
|
|
413
|
+
</span>
|
|
414
|
+
</template>
|
|
415
|
+
</v-list-item>
|
|
416
|
+
<v-list-item
|
|
417
|
+
v-if="item?.overnightParking?.remarks"
|
|
418
|
+
@click="
|
|
419
|
+
openApproveRejectOverNightParkingRequestDialog(
|
|
420
|
+
item,
|
|
421
|
+
overNightParkingListActions[2].status,
|
|
422
|
+
item?.overnightParking?.remarks
|
|
423
|
+
)
|
|
424
|
+
"
|
|
425
|
+
>
|
|
426
|
+
<template #title>
|
|
427
|
+
<span class="text-caption">
|
|
428
|
+
{{ overNightParkingListActions[2].text }}
|
|
429
|
+
</span>
|
|
430
|
+
</template>
|
|
431
|
+
</v-list-item>
|
|
432
|
+
<v-divider />
|
|
433
|
+
</v-card>
|
|
434
|
+
</v-menu>
|
|
338
435
|
</template>
|
|
339
436
|
|
|
340
437
|
<template v-slot:item.checkInRemarks="{ item }">
|
|
@@ -401,9 +498,29 @@
|
|
|
401
498
|
<template v-slot:content>
|
|
402
499
|
<v-row no-gutters class="mb-4">
|
|
403
500
|
<v-col v-for="(label, key) in formattedFields" :key="key" cols="12">
|
|
501
|
+
<span
|
|
502
|
+
v-if="
|
|
503
|
+
key === 'checkIn' &&
|
|
504
|
+
!selectedVisitorObject[key] &&
|
|
505
|
+
canUpdateVisitor
|
|
506
|
+
"
|
|
507
|
+
class="d-flex align-center"
|
|
508
|
+
>
|
|
509
|
+
<strong>{{ label }}:</strong>
|
|
510
|
+
<v-btn
|
|
511
|
+
size="x-small"
|
|
512
|
+
class="ml-3 text-capitalize"
|
|
513
|
+
color="success"
|
|
514
|
+
text="Checkin"
|
|
515
|
+
:disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
|
|
516
|
+
@click="handleCheckin(selectedVisitorObject)"
|
|
517
|
+
:loading="isVisitorDataFromScannedQRCodeCheckingInOut"
|
|
518
|
+
/>
|
|
519
|
+
</span>
|
|
404
520
|
<span
|
|
405
521
|
v-if="
|
|
406
522
|
key === 'checkOut' &&
|
|
523
|
+
selectedVisitorObject['checkIn'] &&
|
|
407
524
|
!selectedVisitorObject[key] &&
|
|
408
525
|
canCheckoutVisitor
|
|
409
526
|
"
|
|
@@ -716,12 +833,105 @@
|
|
|
716
833
|
/>
|
|
717
834
|
</v-dialog>
|
|
718
835
|
|
|
836
|
+
<v-dialog
|
|
837
|
+
v-model="dialog.approveRejectOvernightParkingRequestDialog"
|
|
838
|
+
transition="dialog-bottom-transition"
|
|
839
|
+
persistent
|
|
840
|
+
width="60vh"
|
|
841
|
+
>
|
|
842
|
+
<v-card>
|
|
843
|
+
<v-toolbar density="compact">
|
|
844
|
+
<v-row
|
|
845
|
+
no-gutters
|
|
846
|
+
class="d-flex fill-height justify-space-between align-center px-4"
|
|
847
|
+
>
|
|
848
|
+
<span class="font-weight-bold"> Overnight Parking Request </span>
|
|
849
|
+
<v-btn
|
|
850
|
+
icon="mdi-close"
|
|
851
|
+
variant="text"
|
|
852
|
+
@click="dialog.approveRejectOvernightParkingRequestDialog = false"
|
|
853
|
+
/>
|
|
854
|
+
</v-row>
|
|
855
|
+
</v-toolbar>
|
|
856
|
+
<v-card-text class="px-4 pb-6">
|
|
857
|
+
<v-row no-gutters justify="center" align-content="center">
|
|
858
|
+
<v-col
|
|
859
|
+
v-if="
|
|
860
|
+
approveRejectOvernightParkingRequestDialog.status !== 'remarks'
|
|
861
|
+
"
|
|
862
|
+
cols="12"
|
|
863
|
+
class="text-h6 text-center"
|
|
864
|
+
>
|
|
865
|
+
{{
|
|
866
|
+
`Are you sure you want to ${approveRejectOvernightParkingRequestDialog.text} the visitor's overnight parking request?`
|
|
867
|
+
}}
|
|
868
|
+
</v-col>
|
|
869
|
+
|
|
870
|
+
<v-col cols="12" md="8" class="mt-4">
|
|
871
|
+
<v-textarea
|
|
872
|
+
v-model="overNightParkingRequestApproveRejectRemarks"
|
|
873
|
+
label="Remarks"
|
|
874
|
+
placeholder="Enter remarks"
|
|
875
|
+
outlined
|
|
876
|
+
rows="3"
|
|
877
|
+
clearable
|
|
878
|
+
:hide-details="
|
|
879
|
+
approveRejectOvernightParkingRequestDialog.status ===
|
|
880
|
+
'remarks'
|
|
881
|
+
"
|
|
882
|
+
:readonly="
|
|
883
|
+
approveRejectOvernightParkingRequestDialog.status ===
|
|
884
|
+
'remarks'
|
|
885
|
+
"
|
|
886
|
+
/>
|
|
887
|
+
</v-col>
|
|
888
|
+
</v-row>
|
|
889
|
+
</v-card-text>
|
|
890
|
+
<v-toolbar
|
|
891
|
+
v-if="approveRejectOvernightParkingRequestDialog.status !== 'remarks'"
|
|
892
|
+
class="pa-0"
|
|
893
|
+
density="compact"
|
|
894
|
+
>
|
|
895
|
+
<v-row no-gutters>
|
|
896
|
+
<v-col cols="6">
|
|
897
|
+
<v-btn
|
|
898
|
+
text="No"
|
|
899
|
+
color="grey-lighten-3"
|
|
900
|
+
variant="flat"
|
|
901
|
+
height="48"
|
|
902
|
+
tile
|
|
903
|
+
block
|
|
904
|
+
:disabled="isApprovingRejectingOvernightParkingRequest"
|
|
905
|
+
@click="closeApproveRejectOvernightParkingDialog"
|
|
906
|
+
/>
|
|
907
|
+
</v-col>
|
|
908
|
+
<v-col cols="6">
|
|
909
|
+
<v-btn
|
|
910
|
+
text="Yes"
|
|
911
|
+
color="success"
|
|
912
|
+
variant="flat"
|
|
913
|
+
height="48"
|
|
914
|
+
tile
|
|
915
|
+
block
|
|
916
|
+
:disabled="
|
|
917
|
+
!overNightParkingRequestApproveRejectRemarks ||
|
|
918
|
+
isApprovingRejectingOvernightParkingRequest
|
|
919
|
+
"
|
|
920
|
+
@click="updateOvernightParkingRequestStatus"
|
|
921
|
+
:loading="isApprovingRejectingOvernightParkingRequest"
|
|
922
|
+
/>
|
|
923
|
+
</v-col>
|
|
924
|
+
</v-row>
|
|
925
|
+
</v-toolbar>
|
|
926
|
+
</v-card>
|
|
927
|
+
</v-dialog>
|
|
928
|
+
|
|
719
929
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
720
930
|
</v-row>
|
|
721
931
|
</template>
|
|
722
932
|
|
|
723
933
|
<script setup lang="ts">
|
|
724
|
-
import
|
|
934
|
+
import moment from "moment-timezone";
|
|
725
935
|
|
|
726
936
|
definePageMeta({
|
|
727
937
|
middleware: ["01-auth", "02-org"],
|
|
@@ -764,7 +974,6 @@ const {
|
|
|
764
974
|
debounce,
|
|
765
975
|
formatCamelCaseToWords,
|
|
766
976
|
formatDate,
|
|
767
|
-
formatTime,
|
|
768
977
|
UTCToLocalTIme,
|
|
769
978
|
standardFormatDate,
|
|
770
979
|
} = useUtils();
|
|
@@ -802,6 +1011,7 @@ const messageSnackbar = ref(false);
|
|
|
802
1011
|
|
|
803
1012
|
const loading = reactive({
|
|
804
1013
|
fetchingVisitors: false,
|
|
1014
|
+
checkingIn: false,
|
|
805
1015
|
checkingOut: false,
|
|
806
1016
|
savingRemarks: false,
|
|
807
1017
|
updatingPassKeys: false,
|
|
@@ -818,6 +1028,7 @@ const dialog = reactive({
|
|
|
818
1028
|
editPassKey: false,
|
|
819
1029
|
scanVisitorQRCode: false,
|
|
820
1030
|
showVisitorDataFromScannedQRCode: false,
|
|
1031
|
+
approveRejectOvernightParkingRequestDialog: false,
|
|
821
1032
|
});
|
|
822
1033
|
|
|
823
1034
|
const snapshotImageUrl = ref("");
|
|
@@ -825,32 +1036,147 @@ const remarksType = ref<"checkIn" | "checkOut">("checkIn");
|
|
|
825
1036
|
const remarksInput = ref("");
|
|
826
1037
|
const remarksViewOnly = ref(false);
|
|
827
1038
|
|
|
828
|
-
const headers = computed(() =>
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
]
|
|
1039
|
+
const headers = computed(() => {
|
|
1040
|
+
const tab = activeTab.value;
|
|
1041
|
+
|
|
1042
|
+
// 1. Define the specific headers for Overnight Parking
|
|
1043
|
+
if (tab === "overnight-parking") {
|
|
1044
|
+
return [
|
|
1045
|
+
{ title: "Name", value: "name" },
|
|
1046
|
+
{ title: "Email", value: "email" },
|
|
1047
|
+
{ title: "Invited By", value: "invited-by" },
|
|
1048
|
+
{ title: "Arrival Date/Time", value: "arrival-date-time" },
|
|
1049
|
+
{ title: "Check In/Out", value: "checkin-out" },
|
|
1050
|
+
{ title: "Status", value: "status" },
|
|
1051
|
+
{ title: "Action", value: "action" },
|
|
1052
|
+
];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 2. Define logic for all other tabs
|
|
1056
|
+
const list = [{ title: "Name", value: "name" }];
|
|
1057
|
+
|
|
1058
|
+
// Standard info for non-parking tabs
|
|
1059
|
+
list.push(
|
|
1060
|
+
{ title: "Type/Company", value: "type-company" },
|
|
1061
|
+
{ title: "Location", value: "location" },
|
|
1062
|
+
{ title: "Contact/Vehicle No.", value: "contact-vehicleNumber" }
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
// Conditional logic for Guests vs Others
|
|
1066
|
+
if (tab === "guests") {
|
|
1067
|
+
list.push({ title: "Arrival Date/Time", value: "arrival-date-time" });
|
|
1068
|
+
} else {
|
|
1069
|
+
list.push({ title: "Check In/Out", value: "checkin-out" });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Specific additions
|
|
1073
|
+
if (tab === "resident-transactions") {
|
|
1074
|
+
list.push(
|
|
1075
|
+
{ title: "Check-in Remarks", value: "checkInRemarks" },
|
|
1076
|
+
{ title: "Check-out Remarks", value: "checkOutRemarks" }
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (tab === "unregistered") {
|
|
1081
|
+
list.push({ title: "Action", value: "action" });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return list;
|
|
1085
|
+
});
|
|
846
1086
|
|
|
847
1087
|
const tabOptions = [
|
|
848
1088
|
{ name: "Registered", value: "registered" },
|
|
849
1089
|
{ name: "Unregistered", value: "unregistered" },
|
|
850
1090
|
{ name: "Guests", value: "guests" },
|
|
851
1091
|
{ name: "Resident Transactions", value: "resident-transactions" },
|
|
1092
|
+
{ name: "Overnight Parking", value: "overnight-parking" },
|
|
852
1093
|
];
|
|
853
1094
|
|
|
1095
|
+
function getOvernightParkingRequestStatusColor(status: string) {
|
|
1096
|
+
switch (status) {
|
|
1097
|
+
case "approved":
|
|
1098
|
+
return "success";
|
|
1099
|
+
case "rejected":
|
|
1100
|
+
return "error";
|
|
1101
|
+
default:
|
|
1102
|
+
return "grey";
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const overNightParkingListActions = [
|
|
1107
|
+
{
|
|
1108
|
+
text: "Approve",
|
|
1109
|
+
status: "approved",
|
|
1110
|
+
disabled: props.canUpdateVisitor,
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
text: "Reject",
|
|
1114
|
+
status: "rejected",
|
|
1115
|
+
disabled: props.canUpdateVisitor,
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
text: "View Remarks",
|
|
1119
|
+
status: "remarks",
|
|
1120
|
+
},
|
|
1121
|
+
];
|
|
1122
|
+
const overNightParkingRequestApproveRejectRemarks = ref("");
|
|
1123
|
+
const approveRejectOvernightParkingRequestDialog = ref({
|
|
1124
|
+
text: "",
|
|
1125
|
+
status: "",
|
|
1126
|
+
});
|
|
1127
|
+
const selectedOvernightParkingRequest = ref<Record<string, any>>({});
|
|
1128
|
+
|
|
1129
|
+
function openApproveRejectOverNightParkingRequestDialog(
|
|
1130
|
+
visitor: Record<string, any>,
|
|
1131
|
+
status: string,
|
|
1132
|
+
remarks?: string
|
|
1133
|
+
) {
|
|
1134
|
+
selectedOvernightParkingRequest.value = visitor;
|
|
1135
|
+
approveRejectOvernightParkingRequestDialog.value = {
|
|
1136
|
+
text: status === "approved" ? "Approve" : "Reject",
|
|
1137
|
+
status,
|
|
1138
|
+
};
|
|
1139
|
+
dialog.approveRejectOvernightParkingRequestDialog = true;
|
|
1140
|
+
overNightParkingRequestApproveRejectRemarks.value = remarks ?? "";
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const isApprovingRejectingOvernightParkingRequest = ref(false);
|
|
1144
|
+
|
|
1145
|
+
async function updateOvernightParkingRequestStatus() {
|
|
1146
|
+
isApprovingRejectingOvernightParkingRequest.value = true;
|
|
1147
|
+
try {
|
|
1148
|
+
await visitorDataFromScannedQRCodeCheckIn({
|
|
1149
|
+
type: "overnight-parking",
|
|
1150
|
+
filter: {
|
|
1151
|
+
_id: selectedOvernightParkingRequest.value._id,
|
|
1152
|
+
},
|
|
1153
|
+
update: {
|
|
1154
|
+
status: approveRejectOvernightParkingRequestDialog.value.status,
|
|
1155
|
+
remarks: overNightParkingRequestApproveRejectRemarks.value ?? "",
|
|
1156
|
+
updatedBy: currentUser.value?._id,
|
|
1157
|
+
},
|
|
1158
|
+
userType: "site",
|
|
1159
|
+
});
|
|
1160
|
+
showMessage(
|
|
1161
|
+
`Visitor's overnight parking request successfully ${approveRejectOvernightParkingRequestDialog.value.status}`,
|
|
1162
|
+
"success"
|
|
1163
|
+
);
|
|
1164
|
+
await getVisitorRefresh();
|
|
1165
|
+
closeApproveRejectOvernightParkingDialog();
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
showMessage(errorConverter(error), "error");
|
|
1168
|
+
} finally {
|
|
1169
|
+
isApprovingRejectingOvernightParkingRequest.value = false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function closeApproveRejectOvernightParkingDialog() {
|
|
1174
|
+
dialog.approveRejectOvernightParkingRequestDialog = false;
|
|
1175
|
+
selectedVisitorId.value = "";
|
|
1176
|
+
approveRejectOvernightParkingRequestDialog.value = { text: "", status: "" };
|
|
1177
|
+
overNightParkingRequestApproveRejectRemarks.value = "";
|
|
1178
|
+
}
|
|
1179
|
+
|
|
854
1180
|
function normalizeDateOnly(value: string) {
|
|
855
1181
|
if (!value) return "";
|
|
856
1182
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
|
@@ -905,15 +1231,16 @@ function toRoute(tab: any) {
|
|
|
905
1231
|
const obj = tabOptions.find((x) => x.value === tab);
|
|
906
1232
|
if (!obj) return;
|
|
907
1233
|
page.value = 1;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
1234
|
+
getVisitorRefresh();
|
|
1235
|
+
// navigateTo({
|
|
1236
|
+
// name: routeName,
|
|
1237
|
+
// params: {
|
|
1238
|
+
// org: orgId,
|
|
1239
|
+
// },
|
|
1240
|
+
// // query: {
|
|
1241
|
+
// // tab
|
|
1242
|
+
// // },
|
|
1243
|
+
// });
|
|
917
1244
|
}
|
|
918
1245
|
const {
|
|
919
1246
|
data: getVisitorReq,
|
|
@@ -926,8 +1253,8 @@ const {
|
|
|
926
1253
|
page: page.value,
|
|
927
1254
|
site: siteId,
|
|
928
1255
|
search: searchInput.value,
|
|
929
|
-
dateTo:
|
|
930
|
-
dateFrom:
|
|
1256
|
+
dateTo: moment(dateTo.value).endOf("day").toISOString(),
|
|
1257
|
+
dateFrom: moment(dateFrom.value).startOf("day").toISOString(),
|
|
931
1258
|
type:
|
|
932
1259
|
filterTypes.value.length === 0 && activeTab.value === "registered"
|
|
933
1260
|
? "contractor,delivery,walk-in,pick-up,drop-off,guest"
|
|
@@ -941,6 +1268,8 @@ const {
|
|
|
941
1268
|
} else if (activeTab.value === "resident-transactions") {
|
|
942
1269
|
params.status = "registered";
|
|
943
1270
|
params.type = "resident,tenant";
|
|
1271
|
+
} else if (activeTab.value === "overnight-parking") {
|
|
1272
|
+
params.tab = "Overnight Parking";
|
|
944
1273
|
} else {
|
|
945
1274
|
params.status = activeTab.value;
|
|
946
1275
|
}
|
|
@@ -970,7 +1299,7 @@ watch(getVisitorReq, (newData: any) => {
|
|
|
970
1299
|
}
|
|
971
1300
|
});
|
|
972
1301
|
|
|
973
|
-
const selectedVisitorObject = computed(() => {
|
|
1302
|
+
const selectedVisitorObject = computed<Record<string, any>>(() => {
|
|
974
1303
|
const obj = items.value.find((x: any) => x?._id === selectedVisitorId.value);
|
|
975
1304
|
if (!obj) return {};
|
|
976
1305
|
|
|
@@ -1070,6 +1399,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
|
|
|
1070
1399
|
}
|
|
1071
1400
|
handleCloseVisitorDataFromScannedQRCodeDialog();
|
|
1072
1401
|
getVisitorRefresh();
|
|
1402
|
+
dialog.viewVisitor = false;
|
|
1073
1403
|
} catch (error) {
|
|
1074
1404
|
showMessage(errorConverter(error), "error");
|
|
1075
1405
|
} finally {
|
|
@@ -1262,7 +1592,7 @@ const canConfirmCheckout = computed(() => {
|
|
|
1262
1592
|
});
|
|
1263
1593
|
});
|
|
1264
1594
|
|
|
1265
|
-
|
|
1595
|
+
function handleCheckin(user: Record<string, any>) {
|
|
1266
1596
|
visitorDataFromScannedQRCode.value = user;
|
|
1267
1597
|
dialog.showVisitorDataFromScannedQRCode = true;
|
|
1268
1598
|
}
|
|
@@ -69,9 +69,9 @@ function generateTimeSlotsFromStart(
|
|
|
69
69
|
return slots;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// 11/
|
|
72
|
+
// 24/11/2025, 09:05 format
|
|
73
73
|
function toLocalTimeNumeric(utcString: string) {
|
|
74
|
-
return new Date(utcString).toLocaleString("en-
|
|
74
|
+
return new Date(utcString).toLocaleString("en-GB", {
|
|
75
75
|
year: "numeric",
|
|
76
76
|
month: "2-digit",
|
|
77
77
|
day: "2-digit",
|
|
@@ -137,4 +137,4 @@ return {
|
|
|
137
137
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
}
|
|
140
|
+
}
|
|
@@ -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