@7365admin1/layer-common 1.11.24 → 1.11.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.26
4
+
5
+ ### Patch Changes
6
+
7
+ - d418c67: Update builing management image upload
8
+
9
+ ## 1.11.25
10
+
11
+ ### Patch Changes
12
+
13
+ - 949ad2b: Update Layer-common version
14
+
3
15
  ## 1.11.24
4
16
 
5
17
  ### 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>
@@ -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,52 @@
335
364
  text="Register"
336
365
  @click.stop="handleRegistrationUnregisteredVisitor(item)"
337
366
  />
367
+ <v-menu v-else-if="activeTab === 'overnight-parking'">
368
+ <template #activator="{ props }">
369
+ <v-avatar class="rounded-xl border-md">
370
+ <v-icon v-bind="props" icon="mdi-dots-vertical" />
371
+ </v-avatar>
372
+ </template>
373
+
374
+ <v-card>
375
+ <v-list-item
376
+ @click.stop="
377
+ openApproveRejectOverNightParkingRequestDialog(
378
+ item,
379
+ overNightParkingListActions[0].status
380
+ )
381
+ "
382
+ :disabled="
383
+ item?.overnightParking?.status == 'approved' ||
384
+ !overNightParkingListActions[0].disabled
385
+ "
386
+ >
387
+ <template #title>
388
+ <span class="text-caption">
389
+ {{ overNightParkingListActions[0].text }}
390
+ </span>
391
+ </template>
392
+ </v-list-item>
393
+ <v-divider />
394
+ <v-list-item
395
+ v-if="item?.overnightParking?.status !== 'approved'"
396
+ @click="
397
+ openApproveRejectOverNightParkingRequestDialog(
398
+ item,
399
+ overNightParkingListActions[1].status
400
+ )
401
+ "
402
+ :disabled="!overNightParkingListActions[1].disabled"
403
+ >
404
+ <template #title>
405
+ <span class="text-caption">
406
+ {{ overNightParkingListActions[1].text }}
407
+ </span>
408
+ </template>
409
+ </v-list-item>
410
+ <v-divider />
411
+ </v-card>
412
+ </v-menu>
338
413
  </template>
339
414
 
340
415
  <template v-slot:item.checkInRemarks="{ item }">
@@ -401,9 +476,29 @@
401
476
  <template v-slot:content>
402
477
  <v-row no-gutters class="mb-4">
403
478
  <v-col v-for="(label, key) in formattedFields" :key="key" cols="12">
479
+ <span
480
+ v-if="
481
+ key === 'checkIn' &&
482
+ !selectedVisitorObject[key] &&
483
+ canUpdateVisitor
484
+ "
485
+ class="d-flex align-center"
486
+ >
487
+ <strong>{{ label }}:</strong>
488
+ <v-btn
489
+ size="x-small"
490
+ class="ml-3 text-capitalize"
491
+ color="success"
492
+ text="Checkin"
493
+ :disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
494
+ @click="handleCheckin(selectedVisitorObject)"
495
+ :loading="isVisitorDataFromScannedQRCodeCheckingInOut"
496
+ />
497
+ </span>
404
498
  <span
405
499
  v-if="
406
500
  key === 'checkOut' &&
501
+ selectedVisitorObject['checkIn'] &&
407
502
  !selectedVisitorObject[key] &&
408
503
  canCheckoutVisitor
409
504
  "
@@ -716,12 +811,90 @@
716
811
  />
717
812
  </v-dialog>
718
813
 
814
+ <v-dialog
815
+ v-model="dialog.approveRejectOvernightParkingRequestDialog"
816
+ transition="dialog-bottom-transition"
817
+ persistent
818
+ width="60vh"
819
+ >
820
+ <v-card>
821
+ <v-toolbar density="compact">
822
+ <v-row
823
+ no-gutters
824
+ class="d-flex fill-height justify-space-between align-center px-4"
825
+ >
826
+ <span class="font-weight-bold"> Overnight Parking Request </span>
827
+ <v-btn
828
+ icon="mdi-close"
829
+ variant="text"
830
+ @click="dialog.approveRejectOvernightParkingRequestDialog = false"
831
+ />
832
+ </v-row>
833
+ </v-toolbar>
834
+ <v-card-text class="px-4 pb-2">
835
+ <v-row no-gutters justify="center" align-content="center">
836
+ <v-col
837
+ v-if="
838
+ approveRejectOvernightParkingRequestDialog.status !== 'remarks'
839
+ "
840
+ cols="12"
841
+ class="text-h6 text-center"
842
+ >
843
+ {{
844
+ `Are you sure you want to ${approveRejectOvernightParkingRequestDialog.text} the visitor's overnight parking request?`
845
+ }}
846
+ </v-col>
847
+
848
+ <v-col cols="12" md="8" class="mt-4">
849
+ <v-textarea
850
+ v-model="overNightParkingRequestApproveRejectRemarks"
851
+ label="Remarks"
852
+ placeholder="Enter remarks"
853
+ outlined
854
+ rows="3"
855
+ clearable
856
+ />
857
+ </v-col>
858
+ </v-row>
859
+ </v-card-text>
860
+ <v-toolbar class="pa-0" density="compact">
861
+ <v-row no-gutters>
862
+ <v-col cols="6">
863
+ <v-btn
864
+ text="No"
865
+ variant="text"
866
+ block
867
+ :disabled="isApprovingRejectingOvernightParkingRequest"
868
+ @click="closeApproveRejectOvernightParkingDialog"
869
+ />
870
+ </v-col>
871
+ <v-col cols="6">
872
+ <v-btn
873
+ text="Yes"
874
+ color="success"
875
+ variant="flat"
876
+ height="48"
877
+ rounded="0"
878
+ block
879
+ :disabled="
880
+ !overNightParkingRequestApproveRejectRemarks ||
881
+ isApprovingRejectingOvernightParkingRequest
882
+ "
883
+ @click="updateOvernightParkingRequestStatus"
884
+ :loading="isApprovingRejectingOvernightParkingRequest"
885
+ />
886
+ </v-col>
887
+ </v-row>
888
+ </v-toolbar>
889
+ </v-card>
890
+ </v-dialog>
891
+
719
892
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
720
893
  </v-row>
721
894
  </template>
722
895
 
723
896
  <script setup lang="ts">
724
- import { errorConverter } from "../utils/data";
897
+ import moment from "moment-timezone";
725
898
 
726
899
  definePageMeta({
727
900
  middleware: ["01-auth", "02-org"],
@@ -764,7 +937,6 @@ const {
764
937
  debounce,
765
938
  formatCamelCaseToWords,
766
939
  formatDate,
767
- formatTime,
768
940
  UTCToLocalTIme,
769
941
  standardFormatDate,
770
942
  } = useUtils();
@@ -802,6 +974,7 @@ const messageSnackbar = ref(false);
802
974
 
803
975
  const loading = reactive({
804
976
  fetchingVisitors: false,
977
+ checkingIn: false,
805
978
  checkingOut: false,
806
979
  savingRemarks: false,
807
980
  updatingPassKeys: false,
@@ -818,6 +991,7 @@ const dialog = reactive({
818
991
  editPassKey: false,
819
992
  scanVisitorQRCode: false,
820
993
  showVisitorDataFromScannedQRCode: false,
994
+ approveRejectOvernightParkingRequestDialog: false,
821
995
  });
822
996
 
823
997
  const snapshotImageUrl = ref("");
@@ -825,32 +999,141 @@ const remarksType = ref<"checkIn" | "checkOut">("checkIn");
825
999
  const remarksInput = ref("");
826
1000
  const remarksViewOnly = ref(false);
827
1001
 
828
- const headers = computed(() => [
829
- { 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
- ]);
1002
+ const headers = computed(() => {
1003
+ const tab = activeTab.value;
1004
+
1005
+ // 1. Define the specific headers for Overnight Parking
1006
+ if (tab === "overnight-parking") {
1007
+ return [
1008
+ { title: "Name", value: "name" },
1009
+ { title: "Email", value: "email" },
1010
+ { title: "Invited By", value: "invited-by" },
1011
+ { title: "Arrival Date/Time", value: "arrival-date-time" },
1012
+ { title: "Check In/Out", value: "checkin-out" },
1013
+ { title: "Status", value: "status" },
1014
+ { title: "Action", value: "action" },
1015
+ ];
1016
+ }
1017
+
1018
+ // 2. Define logic for all other tabs
1019
+ const list = [{ title: "Name", value: "name" }];
1020
+
1021
+ // Standard info for non-parking tabs
1022
+ list.push(
1023
+ { title: "Type/Company", value: "type-company" },
1024
+ { title: "Location", value: "location" },
1025
+ { title: "Contact/Vehicle No.", value: "contact-vehicleNumber" }
1026
+ );
1027
+
1028
+ // Conditional logic for Guests vs Others
1029
+ if (tab === "guests") {
1030
+ list.push({ title: "Arrival Date/Time", value: "arrival-date-time" });
1031
+ } else {
1032
+ list.push({ title: "Check In/Out", value: "checkin-out" });
1033
+ }
1034
+
1035
+ // Specific additions
1036
+ if (tab === "resident-transactions") {
1037
+ list.push(
1038
+ { title: "Check-in Remarks", value: "checkInRemarks" },
1039
+ { title: "Check-out Remarks", value: "checkOutRemarks" }
1040
+ );
1041
+ }
1042
+
1043
+ if (tab === "unregistered") {
1044
+ list.push({ title: "Action", value: "action" });
1045
+ }
1046
+
1047
+ return list;
1048
+ });
846
1049
 
847
1050
  const tabOptions = [
848
1051
  { name: "Registered", value: "registered" },
849
1052
  { name: "Unregistered", value: "unregistered" },
850
1053
  { name: "Guests", value: "guests" },
851
1054
  { name: "Resident Transactions", value: "resident-transactions" },
1055
+ { name: "Overnight Parking", value: "overnight-parking" },
852
1056
  ];
853
1057
 
1058
+ function getOvernightParkingRequestStatusColor(status: string) {
1059
+ switch (status) {
1060
+ case "approved":
1061
+ return "success";
1062
+ case "rejected":
1063
+ return "error";
1064
+ default:
1065
+ return "grey";
1066
+ }
1067
+ }
1068
+
1069
+ const overNightParkingListActions = [
1070
+ {
1071
+ text: "Approve",
1072
+ status: "approved",
1073
+ disabled: props.canUpdateVisitor,
1074
+ },
1075
+ {
1076
+ text: "Reject",
1077
+ status: "rejected",
1078
+ disabled: props.canUpdateVisitor,
1079
+ },
1080
+ ];
1081
+ const overNightParkingRequestApproveRejectRemarks = ref("");
1082
+ const approveRejectOvernightParkingRequestDialog = ref({
1083
+ text: "",
1084
+ status: "",
1085
+ });
1086
+ const selectedOvernightParkingRequest = ref<Record<string, any>>({});
1087
+
1088
+ function openApproveRejectOverNightParkingRequestDialog(
1089
+ visitor: Record<string, any>,
1090
+ status: string
1091
+ ) {
1092
+ selectedOvernightParkingRequest.value = visitor;
1093
+ approveRejectOvernightParkingRequestDialog.value = {
1094
+ text: status === "approved" ? "Approve" : "Reject",
1095
+ status,
1096
+ };
1097
+ dialog.approveRejectOvernightParkingRequestDialog = true;
1098
+ }
1099
+
1100
+ const isApprovingRejectingOvernightParkingRequest = ref(false);
1101
+
1102
+ async function updateOvernightParkingRequestStatus() {
1103
+ isApprovingRejectingOvernightParkingRequest.value = true;
1104
+ try {
1105
+ await visitorDataFromScannedQRCodeCheckIn({
1106
+ type: "overnight-parking",
1107
+ filter: {
1108
+ _id: selectedOvernightParkingRequest.value._id,
1109
+ },
1110
+ update: {
1111
+ status: approveRejectOvernightParkingRequestDialog.value.status,
1112
+ remarks: overNightParkingRequestApproveRejectRemarks.value ?? "",
1113
+ updatedBy: currentUser.value?._id,
1114
+ },
1115
+ userType: "site",
1116
+ });
1117
+ showMessage(
1118
+ `Visitor's overnight parking request successfully ${approveRejectOvernightParkingRequestDialog.value.status}`,
1119
+ "success"
1120
+ );
1121
+ await getVisitorRefresh();
1122
+ closeApproveRejectOvernightParkingDialog();
1123
+ } catch (error) {
1124
+ showMessage(errorConverter(error), "error");
1125
+ } finally {
1126
+ isApprovingRejectingOvernightParkingRequest.value = false;
1127
+ }
1128
+ }
1129
+
1130
+ function closeApproveRejectOvernightParkingDialog() {
1131
+ dialog.approveRejectOvernightParkingRequestDialog = false;
1132
+ selectedVisitorId.value = "";
1133
+ approveRejectOvernightParkingRequestDialog.value = { text: "", status: "" };
1134
+ overNightParkingRequestApproveRejectRemarks.value = "";
1135
+ }
1136
+
854
1137
  function normalizeDateOnly(value: string) {
855
1138
  if (!value) return "";
856
1139
  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
@@ -905,15 +1188,16 @@ function toRoute(tab: any) {
905
1188
  const obj = tabOptions.find((x) => x.value === tab);
906
1189
  if (!obj) return;
907
1190
  page.value = 1;
908
- navigateTo({
909
- name: routeName,
910
- params: {
911
- org: orgId,
912
- },
913
- // query: {
914
- // tab
915
- // },
916
- });
1191
+ getVisitorRefresh();
1192
+ // navigateTo({
1193
+ // name: routeName,
1194
+ // params: {
1195
+ // org: orgId,
1196
+ // },
1197
+ // // query: {
1198
+ // // tab
1199
+ // // },
1200
+ // });
917
1201
  }
918
1202
  const {
919
1203
  data: getVisitorReq,
@@ -926,8 +1210,8 @@ const {
926
1210
  page: page.value,
927
1211
  site: siteId,
928
1212
  search: searchInput.value,
929
- dateTo: normalizeDateOnly(dateTo.value),
930
- dateFrom: normalizeDateOnly(dateFrom.value),
1213
+ dateTo: moment(dateTo.value).endOf("day").toISOString(),
1214
+ dateFrom: moment(dateFrom.value).startOf("day").toISOString(),
931
1215
  type:
932
1216
  filterTypes.value.length === 0 && activeTab.value === "registered"
933
1217
  ? "contractor,delivery,walk-in,pick-up,drop-off,guest"
@@ -941,6 +1225,8 @@ const {
941
1225
  } else if (activeTab.value === "resident-transactions") {
942
1226
  params.status = "registered";
943
1227
  params.type = "resident,tenant";
1228
+ } else if (activeTab.value === "overnight-parking") {
1229
+ params.tab = "Overnight Parking";
944
1230
  } else {
945
1231
  params.status = activeTab.value;
946
1232
  }
@@ -970,7 +1256,7 @@ watch(getVisitorReq, (newData: any) => {
970
1256
  }
971
1257
  });
972
1258
 
973
- const selectedVisitorObject = computed(() => {
1259
+ const selectedVisitorObject = computed<Record<string, any>>(() => {
974
1260
  const obj = items.value.find((x: any) => x?._id === selectedVisitorId.value);
975
1261
  if (!obj) return {};
976
1262
 
@@ -1070,6 +1356,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
1070
1356
  }
1071
1357
  handleCloseVisitorDataFromScannedQRCodeDialog();
1072
1358
  getVisitorRefresh();
1359
+ dialog.viewVisitor = false;
1073
1360
  } catch (error) {
1074
1361
  showMessage(errorConverter(error), "error");
1075
1362
  } finally {
@@ -1262,7 +1549,7 @@ const canConfirmCheckout = computed(() => {
1262
1549
  });
1263
1550
  });
1264
1551
 
1265
- async function handleCheckIn(user: Record<string, any>) {
1552
+ function handleCheckin(user: Record<string, any>) {
1266
1553
  visitorDataFromScannedQRCode.value = user;
1267
1554
  dialog.showVisitorDataFromScannedQRCode = true;
1268
1555
  }
@@ -90,6 +90,7 @@ export default function () {
90
90
  status?: string;
91
91
  checkedOut?: boolean;
92
92
  plateNumber?: string;
93
+ tab?: string;
93
94
  };
94
95
 
95
96
  async function getVisitors({
@@ -106,6 +107,7 @@ export default function () {
106
107
  // status = "registered"
107
108
  plateNumber = "",
108
109
  checkedOut,
110
+ tab,
109
111
  }: GetVisitorsParams = {}) {
110
112
  return await useNuxtApp().$api<Record<string, any>>(
111
113
  "/api/visitor-transactions",
@@ -124,6 +126,7 @@ export default function () {
124
126
  checkedOut,
125
127
  plateNumber,
126
128
  order,
129
+ tab,
127
130
  },
128
131
  }
129
132
  );
package/package.json CHANGED
@@ -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.26",
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 = {