@7365admin1/layer-common 1.11.23 → 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 +18 -0
- package/components/AreaChecklistHistoryMain.vue +1 -1
- package/components/AttachmentsViewer.vue +266 -0
- package/components/BuildingForm.vue +63 -95
- package/components/BuildingManagement/buildings.vue +11 -12
- package/components/CleaningScheduleMain.vue +10 -1
- package/components/DocumentViewer.vue +75 -0
- package/components/EquipmentItemMain.vue +4 -2
- package/components/EquipmentManagementMain.vue +14 -10
- package/components/ManageChecklistMain.vue +13 -1
- package/components/ScanVisitorQRCode.vue +9 -2
- package/components/ScheduleAreaMain.vue +0 -1
- package/components/VisitorDataFromScannedQRCodeForm.vue +50 -20
- package/components/VisitorForm.vue +6 -8
- package/components/VisitorManagement.vue +438 -55
- package/composables/useVisitor.ts +5 -2
- 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.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
|
+
|
|
15
|
+
## 1.11.24
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 68d57fd: Update Enhancement in VMS
|
|
20
|
+
|
|
3
21
|
## 1.11.23
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -97,7 +97,7 @@ const props = defineProps({
|
|
|
97
97
|
site: { type: String, required: true, default: "" },
|
|
98
98
|
scheduleAreaId: { type: String, required: true, default: "" },
|
|
99
99
|
type: { type: String, required: true, default: "cleaner" },
|
|
100
|
-
scheduleRoute: { type: String, default: "
|
|
100
|
+
scheduleRoute: { type: String, default: "pest-schedule" },
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
const { formatDate, back, debounce } = useUtils();
|
|
@@ -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>
|
|
@@ -76,6 +76,15 @@
|
|
|
76
76
|
{{ value || "No Status" }}
|
|
77
77
|
</v-chip>
|
|
78
78
|
</template>
|
|
79
|
+
<template #item.completedAt="{ value }">
|
|
80
|
+
{{
|
|
81
|
+
formatDate
|
|
82
|
+
? formatDate(value)
|
|
83
|
+
: value
|
|
84
|
+
? new Date(value).toLocaleString()
|
|
85
|
+
: ""
|
|
86
|
+
}}
|
|
87
|
+
</template>
|
|
79
88
|
<template #item.download="{ item }">
|
|
80
89
|
<v-btn
|
|
81
90
|
v-if="canDownloadSchedule"
|
|
@@ -136,7 +145,7 @@ const headers = [
|
|
|
136
145
|
{ title: "Created Date", value: "createdAt" },
|
|
137
146
|
{ title: "Close in", value: "closeIn" },
|
|
138
147
|
{ title: "Status", value: "status" },
|
|
139
|
-
{ title: "Completion Date", value: "
|
|
148
|
+
{ title: "Completion Date", value: "completedAt" },
|
|
140
149
|
{ title: "", value: "download" },
|
|
141
150
|
];
|
|
142
151
|
const remainingTime = ref({} as Record<string, string>);
|