@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 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
- v-model.number="building.block"
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
- type="number"
54
- >
55
- <template v-slot:item="{ props: itemProps, item}">
56
- <v-list-item v-bind="itemProps" :disabled="item?.raw?.disabled" ></v-list-item>
57
- </template>
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
- v-model.number="buildingLevels"
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
- block
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
- <!-- <v-col cols="12" class="px-6">
126
- <InputLabel class="text-capitalize" title="Building Floor plan" />
127
- <InputFileV2 v-model="building.buildingFloorPlan" :multiple="true" :max-length="10" title="Upload Images" />
128
- </v-col> -->
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
- tile
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
- buildingFloorPlan: []
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
- buildingFloorPlan: []
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
- await createBuilding(building.value);
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
- buildingFloorPlan: building.value.buildingFloorPlan
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 @click:row="tableRowClickHandler"
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
- <!-- <template v-if="selectedBuilding?.buildingFloorPlan.length > 0">
74
- <v-col cols="12">
75
- <strong>Building Floor Plan:</strong>
76
- </v-col>
77
- <v-col cols="12">
78
- <InputFileV2 :model-value="selectedBuilding?.buildingFloorPlan" view-mode />
79
- </v-col>
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
- buildingFloorPlan: [],
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
- .v-data-table :deep(thead th) {
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/DD/YYYY'
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
- const options: Intl.DateTimeFormatOptions = {
51
- year: 'numeric',
52
- month: '2-digit',
53
- day: '2-digit'
54
- }
55
- return dateObj.toLocaleDateString('en-US', options)
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
- {{ formatTime(item.expectedCheckIn) || "-" }}
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="handleCheckIn(item)"
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 { errorConverter } from "../utils/data";
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
- { title: "Name", value: "name" },
830
- { title: "Type/Company", value: "type-company" },
831
- { title: "Location", value: "location" },
832
- { title: "Contact/Vehicle No.", value: "contact-vehicleNumber" },
833
- ...(activeTab.value != "guests"
834
- ? [{ title: "Check In/Out", value: "checkin-out" }]
835
- : [{ title: "Arrival Date / Time", value: "arrival-date-time" }]),
836
- ...(activeTab.value === "resident-transactions"
837
- ? [
838
- { title: "Check-in Remarks", value: "checkInRemarks" },
839
- { title: "Check-out Remarks", value: "checkOutRemarks" },
840
- ]
841
- : []),
842
- ...(activeTab.value === "unregistered"
843
- ? [{ title: "Action", value: "action" }]
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
- navigateTo({
909
- name: routeName,
910
- params: {
911
- org: orgId,
912
- },
913
- // query: {
914
- // tab
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: normalizeDateOnly(dateTo.value),
930
- dateFrom: normalizeDateOnly(dateFrom.value),
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
- async function handleCheckIn(user: Record<string, any>) {
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/24/2025, 09:05 format
72
+ // 24/11/2025, 09:05 format
73
73
  function toLocalTimeNumeric(utcString: string) {
74
- return new Date(utcString).toLocaleString("en-US", {
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
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.11.24",
5
+ "version": "1.11.27",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -4,7 +4,7 @@ declare type TBuilding = {
4
4
  name: string;
5
5
  block: number | null;
6
6
  levels: string[];
7
- buildingFloorPlan: string[]
7
+ buildingFiles: { id: string, name: string }[] | string[];
8
8
  };
9
9
 
10
10
  declare type TBuildingUnit = {