@7365admin1/layer-common 1.11.18 → 1.11.19
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 +6 -0
- package/components/AddPassKeyToVisitor.vue +207 -0
- package/components/BuildingUnitFormEdit.vue +41 -1
- package/components/Input/FileV2.vue +4 -3
- package/components/MemberInformation.vue +8 -2
- package/components/VisitorManagement.vue +56 -20
- package/composables/useSiteSettings.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" :loading="processing">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
|
|
5
|
+
<span class="font-weight-bold text-subtitle-1">Assign Pass & Keys</span>
|
|
6
|
+
<ButtonClose @click="emit('close')" />
|
|
7
|
+
</v-row>
|
|
8
|
+
</v-toolbar>
|
|
9
|
+
|
|
10
|
+
<v-card-text>
|
|
11
|
+
<v-row no-gutters class="ga-1">
|
|
12
|
+
<v-col cols="12">
|
|
13
|
+
<span class="text-subtitle-2 text-medium-emphasis">
|
|
14
|
+
Name: {{ prop.visitor.name }}
|
|
15
|
+
</span>
|
|
16
|
+
</v-col>
|
|
17
|
+
|
|
18
|
+
<v-col cols="12" class="mt-3">
|
|
19
|
+
<InputLabel title="Pass" />
|
|
20
|
+
<v-autocomplete
|
|
21
|
+
v-model="selectedPass"
|
|
22
|
+
v-model:search="passInput"
|
|
23
|
+
:hide-no-data="false"
|
|
24
|
+
:items="passItems"
|
|
25
|
+
item-title="prefixAndName"
|
|
26
|
+
item-value="_id"
|
|
27
|
+
variant="outlined"
|
|
28
|
+
hide-details
|
|
29
|
+
density="compact"
|
|
30
|
+
small-chips
|
|
31
|
+
:loading="fetchPassesPending"
|
|
32
|
+
>
|
|
33
|
+
<template v-slot:chip="{ props: chipProps, item }">
|
|
34
|
+
<v-chip v-if="selectedPass" v-bind="chipProps" prepend-icon="mdi-card-bulleted-outline"
|
|
35
|
+
:text="item.raw?.prefixAndName" />
|
|
36
|
+
</template>
|
|
37
|
+
<template v-slot:no-data>
|
|
38
|
+
<v-list-item density="compact">
|
|
39
|
+
<v-list-item-title>No available passes</v-list-item-title>
|
|
40
|
+
</v-list-item>
|
|
41
|
+
</template>
|
|
42
|
+
</v-autocomplete>
|
|
43
|
+
</v-col>
|
|
44
|
+
|
|
45
|
+
<v-col v-if="showKeys" cols="12" class="mt-3">
|
|
46
|
+
<InputLabel title="Keys" />
|
|
47
|
+
<v-autocomplete
|
|
48
|
+
v-model="selectedKeys"
|
|
49
|
+
v-model:search="keyInput"
|
|
50
|
+
:hide-no-data="false"
|
|
51
|
+
:items="keyItems"
|
|
52
|
+
item-title="prefixAndName"
|
|
53
|
+
item-value="_id"
|
|
54
|
+
multiple
|
|
55
|
+
variant="outlined"
|
|
56
|
+
hide-details
|
|
57
|
+
density="compact"
|
|
58
|
+
small-chips
|
|
59
|
+
:loading="fetchKeysPending"
|
|
60
|
+
>
|
|
61
|
+
<template v-slot:chip="{ props: chipProps, item }">
|
|
62
|
+
<v-chip v-if="selectedKeys.length > 0" v-bind="chipProps" prepend-icon="mdi-key"
|
|
63
|
+
:text="item.raw?.prefixAndName" />
|
|
64
|
+
</template>
|
|
65
|
+
<template v-slot:no-data>
|
|
66
|
+
<v-list-item density="compact">
|
|
67
|
+
<v-list-item-title>No available keys</v-list-item-title>
|
|
68
|
+
</v-list-item>
|
|
69
|
+
</template>
|
|
70
|
+
</v-autocomplete>
|
|
71
|
+
</v-col>
|
|
72
|
+
</v-row>
|
|
73
|
+
|
|
74
|
+
<v-row v-if="errorMessage" no-gutters class="mt-2">
|
|
75
|
+
<p class="text-error text-subtitle-2 w-100 text-center">{{ errorMessage }}</p>
|
|
76
|
+
</v-row>
|
|
77
|
+
</v-card-text>
|
|
78
|
+
|
|
79
|
+
<v-toolbar density="compact">
|
|
80
|
+
<v-row no-gutters>
|
|
81
|
+
<v-col cols="6">
|
|
82
|
+
<v-btn tile block variant="text" class="text-none" size="48" text="Cancel" @click="emit('close')" />
|
|
83
|
+
</v-col>
|
|
84
|
+
<v-col cols="6">
|
|
85
|
+
<v-btn
|
|
86
|
+
tile block variant="flat" color="black" class="text-none" size="48"
|
|
87
|
+
text="Assign"
|
|
88
|
+
:disabled="!selectedPass && selectedKeys.length === 0"
|
|
89
|
+
:loading="processing"
|
|
90
|
+
@click="handleSubmit"
|
|
91
|
+
/>
|
|
92
|
+
</v-col>
|
|
93
|
+
</v-row>
|
|
94
|
+
</v-toolbar>
|
|
95
|
+
</v-card>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
import type { PropType } from 'vue'
|
|
100
|
+
import usePassKey from '../composables/usePassKey'
|
|
101
|
+
import useVisitor from '../composables/useVisitor'
|
|
102
|
+
|
|
103
|
+
const prop = defineProps({
|
|
104
|
+
visitor: {
|
|
105
|
+
type: Object as PropType<TVisitor>,
|
|
106
|
+
required: true,
|
|
107
|
+
},
|
|
108
|
+
site: {
|
|
109
|
+
type: String,
|
|
110
|
+
required: true,
|
|
111
|
+
},
|
|
112
|
+
type: {
|
|
113
|
+
type: String as PropType<TVisitorType>,
|
|
114
|
+
required: true,
|
|
115
|
+
},
|
|
116
|
+
contractorType: {
|
|
117
|
+
type: String,
|
|
118
|
+
default: '',
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const emit = defineEmits<{
|
|
123
|
+
(e: 'done'): void
|
|
124
|
+
(e: 'close'): void
|
|
125
|
+
}>()
|
|
126
|
+
|
|
127
|
+
const { getPassKeysByPageSearch } = usePassKey()
|
|
128
|
+
const { updateVisitor } = useVisitor()
|
|
129
|
+
|
|
130
|
+
const processing = ref(false)
|
|
131
|
+
const errorMessage = ref('')
|
|
132
|
+
|
|
133
|
+
const selectedPass = ref<string>('')
|
|
134
|
+
const selectedKeys = ref<string[]>([])
|
|
135
|
+
const passInput = ref('')
|
|
136
|
+
const keyInput = ref('')
|
|
137
|
+
const passItems = ref<TPassKey[]>([])
|
|
138
|
+
const keyItems = ref<TPassKey[]>([])
|
|
139
|
+
|
|
140
|
+
const showKeys = computed(() => prop.visitor.type === 'contractor')
|
|
141
|
+
|
|
142
|
+
const passTypesComputed = computed(() => {
|
|
143
|
+
if (prop.type === 'contractor') {
|
|
144
|
+
return prop.contractorType === 'property-agent' ? ['agent-pass'] : ['contractor-pass']
|
|
145
|
+
}
|
|
146
|
+
return ['visitor-pass']
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const { data: passesData, pending: fetchPassesPending } = await useLazyAsyncData(
|
|
150
|
+
`add-pass-key-visitor-passes-${prop.visitor._id}`,
|
|
151
|
+
() => getPassKeysByPageSearch({
|
|
152
|
+
search: passInput.value,
|
|
153
|
+
page: 1,
|
|
154
|
+
limit: 500,
|
|
155
|
+
passTypes: passTypesComputed.value,
|
|
156
|
+
sites: [prop.site],
|
|
157
|
+
statuses: ['Available'],
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const { data: keysData, pending: fetchKeysPending } = await useLazyAsyncData(
|
|
162
|
+
`add-pass-key-visitor-keys-${prop.visitor._id}`,
|
|
163
|
+
() => getPassKeysByPageSearch({
|
|
164
|
+
search: keyInput.value,
|
|
165
|
+
page: 1,
|
|
166
|
+
limit: 500,
|
|
167
|
+
passTypes: ['pass-key'],
|
|
168
|
+
sites: [prop.site],
|
|
169
|
+
statuses: ['Available'],
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
watch(passesData, (data: any) => {
|
|
174
|
+
passItems.value = Array.isArray(data?.items) ? data.items : []
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
watch(keysData, (data: any) => {
|
|
178
|
+
keyItems.value = Array.isArray(data?.items) ? data.items : []
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
async function handleSubmit() {
|
|
182
|
+
if (!prop.visitor._id) return
|
|
183
|
+
errorMessage.value = ''
|
|
184
|
+
processing.value = true
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const payload: Partial<TVisitorPayload> = {}
|
|
188
|
+
|
|
189
|
+
if (selectedPass.value) {
|
|
190
|
+
payload.visitorPass = [{ keyId: selectedPass.value, status: "In Use" }]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (selectedKeys.value.length > 0) {
|
|
194
|
+
payload.passKeys = selectedKeys.value.map(keyId => ({ keyId, status: "In Use" }))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await updateVisitor(prop.visitor._id, payload)
|
|
198
|
+
emit('done')
|
|
199
|
+
} catch (error: any) {
|
|
200
|
+
errorMessage.value = error?.data?.message || 'Failed to assign pass & keys. Please try again.'
|
|
201
|
+
} finally {
|
|
202
|
+
processing.value = false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<style scoped></style>
|
|
@@ -227,6 +227,17 @@
|
|
|
227
227
|
|
|
228
228
|
</v-row>
|
|
229
229
|
</v-col>
|
|
230
|
+
|
|
231
|
+
<v-col cols="12" class="mt-2">
|
|
232
|
+
<InputLabel class="text-capitalize d-block mb-1" title="Unit Documents" />
|
|
233
|
+
<InputFileV2
|
|
234
|
+
v-model="buildingUnit.buildingUnitFiles"
|
|
235
|
+
:multiple="true"
|
|
236
|
+
accept="*/*"
|
|
237
|
+
title="Upload documents"
|
|
238
|
+
:height="104"
|
|
239
|
+
/>
|
|
240
|
+
</v-col>
|
|
230
241
|
</v-row>
|
|
231
242
|
</template>
|
|
232
243
|
</v-card-text>
|
|
@@ -331,6 +342,35 @@ const buildingUnit = ref({
|
|
|
331
342
|
|
|
332
343
|
buildingUnit.value = JSON.parse(JSON.stringify(prop.roomFacility));
|
|
333
344
|
|
|
345
|
+
// Normalize buildingUnitFiles: extract IDs for InputFileV2, keep names in a separate map
|
|
346
|
+
const buildingUnitFilesNames = ref<Record<string, string>>({});
|
|
347
|
+
const rawFiles = buildingUnit.value.buildingUnitFiles as any[];
|
|
348
|
+
if (rawFiles?.length && typeof rawFiles[0] === 'object') {
|
|
349
|
+
rawFiles.forEach((f: { id: string; name: string }) => {
|
|
350
|
+
buildingUnitFilesNames.value[f.id] = f.name ?? "";
|
|
351
|
+
});
|
|
352
|
+
buildingUnit.value.buildingUnitFiles = rawFiles.map((f: { id: string }) => f.id);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { getFileById } = useFile();
|
|
356
|
+
|
|
357
|
+
watch(
|
|
358
|
+
() => buildingUnit.value.buildingUnitFiles as string[],
|
|
359
|
+
async (ids) => {
|
|
360
|
+
for (const id of ids) {
|
|
361
|
+
if (!buildingUnitFilesNames.value[id]) {
|
|
362
|
+
try {
|
|
363
|
+
const meta = await getFileById(id) as any;
|
|
364
|
+
buildingUnitFilesNames.value[id] = meta?.data?.name ?? meta?.name ?? "";
|
|
365
|
+
} catch {
|
|
366
|
+
buildingUnitFilesNames.value[id] = "";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
{ deep: true }
|
|
372
|
+
);
|
|
373
|
+
|
|
334
374
|
const emit = defineEmits(["cancel", "success", "success:create-more", "delete-unit"]);
|
|
335
375
|
|
|
336
376
|
|
|
@@ -462,7 +502,7 @@ async function submit() {
|
|
|
462
502
|
name: buildingUnit.value.name,
|
|
463
503
|
level: buildingUnit.value.level,
|
|
464
504
|
// category: buildingUnit.value.category,
|
|
465
|
-
buildingUnitFiles: buildingUnit.value.buildingUnitFiles || [],
|
|
505
|
+
buildingUnitFiles: (buildingUnit.value.buildingUnitFiles as string[] || []).map((id) => ({ id, name: buildingUnitFilesNames.value[id] ?? "" })),
|
|
466
506
|
companyName: buildingUnit.value.companyName,
|
|
467
507
|
companyRegistrationNumber: buildingUnit.value.companyRegistrationNumber || "",
|
|
468
508
|
leaseStart: buildingUnit.value.leaseStart,
|
|
@@ -77,7 +77,7 @@ const props = defineProps({
|
|
|
77
77
|
}
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
|
|
80
|
+
const { addFile, deleteFile, getFileUrl, getFileById, urlToFile } = useFile()
|
|
81
81
|
|
|
82
82
|
const showImageCarousel = ref(false)
|
|
83
83
|
const activeImageId = ref("")
|
|
@@ -187,8 +187,9 @@ async function downloadFile(id: string, filename: string) {
|
|
|
187
187
|
const result: { file: File; id: string }[] = []
|
|
188
188
|
for (const id of ids) {
|
|
189
189
|
try {
|
|
190
|
-
const
|
|
191
|
-
const name =
|
|
190
|
+
const meta = await getFileById(id) as any
|
|
191
|
+
const name: string = meta?.data?.name || meta?.name || `file_${id}`
|
|
192
|
+
const url = getFileUrl(id)
|
|
192
193
|
const file = await urlToFile(url, name)
|
|
193
194
|
result.push({ file, id })
|
|
194
195
|
} catch (err) {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</v-col>
|
|
20
20
|
|
|
21
21
|
<v-col cols="12">
|
|
22
|
-
<v-autocomplete v-model="
|
|
22
|
+
<v-autocomplete v-model="selectedPass" v-model:search="passInput"
|
|
23
23
|
:hide-no-data="false" class="mt-3" :items="passItemsFilteredFinal"
|
|
24
24
|
item-title="prefixAndName" item-value="_id" label="Pass (optional)" variant="outlined"
|
|
25
25
|
hide-details density="compact" persistent-hint small-chips>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</template>
|
|
36
36
|
|
|
37
37
|
<template v-slot:chip="{ props, item }">
|
|
38
|
-
<v-chip v-if="
|
|
38
|
+
<v-chip v-if="selectedPass" v-bind="props"
|
|
39
39
|
prepend-icon="mdi-card-bulleted-outline"
|
|
40
40
|
:text="item.raw?.prefixAndName"></v-chip>
|
|
41
41
|
</template>
|
|
@@ -116,6 +116,8 @@ const errorMessage = ref('')
|
|
|
116
116
|
const passInput = ref('')
|
|
117
117
|
const passItems = ref<TPassKey[]>([])
|
|
118
118
|
|
|
119
|
+
const selectedPass = ref<string>('')
|
|
120
|
+
|
|
119
121
|
const members = defineModel<TMemberInfo[]>({ required: true, default: [] })
|
|
120
122
|
const committedMembers = ref<TMemberInfo[]>([])
|
|
121
123
|
|
|
@@ -171,6 +173,10 @@ const membersDisplayed = computed(() => {
|
|
|
171
173
|
})
|
|
172
174
|
})
|
|
173
175
|
|
|
176
|
+
watch(selectedPass, (newVal) => {
|
|
177
|
+
memberForm.visitorPass = [{ keyId: newVal }]
|
|
178
|
+
})
|
|
179
|
+
|
|
174
180
|
|
|
175
181
|
|
|
176
182
|
function handleClearForm() {
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
</span>
|
|
44
44
|
<span class="text-capitalize">{{ item?.name }}</span>
|
|
45
45
|
</span>
|
|
46
|
-
<span class="text-grey text-caption" v-if="item?.members?.length > 0">( +{{ item?.members?.length }}
|
|
46
|
+
<span class="text-grey text-caption" v-if="item?.members?.length > 0">( +{{ item?.members?.length }}
|
|
47
|
+
members)</span>
|
|
47
48
|
</template>
|
|
48
49
|
|
|
49
50
|
<template v-slot:item.type-company="{ item }">
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
<v-icon icon="mdi-user" size="15" />
|
|
52
53
|
<span v-if="item.type === 'contractor'" class="text-capitalize">{{
|
|
53
54
|
formatCamelCaseToWords(item.contractorType)
|
|
54
|
-
|
|
55
|
+
}}</span>
|
|
55
56
|
<span v-else class="text-capitalize">{{ formatType(item) }}</span>
|
|
56
57
|
</span>
|
|
57
58
|
<span class="d-flex align-center ga-2">
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
<v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
|
|
91
92
|
<span class="text-capitalize">{{
|
|
92
93
|
UTCToLocalTIme(item.checkIn) || "-"
|
|
93
|
-
|
|
94
|
+
}}</span>
|
|
94
95
|
<span>
|
|
95
96
|
<v-icon v-if="item?.snapshotEntryImage" size="17" icon="mdi-image"
|
|
96
97
|
@click.stop="handleViewImage(item.snapshotEntryImage)" />
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
<template v-if="item.checkOut">
|
|
102
103
|
<span class="text-capitalize">{{
|
|
103
104
|
UTCToLocalTIme(item.checkOut) || "-"
|
|
104
|
-
|
|
105
|
+
}}</span>
|
|
105
106
|
<span>
|
|
106
107
|
<v-icon v-if="item?.snapshotExitImage" size="17" icon="mdi-image"
|
|
107
108
|
@click.stop="handleViewImage(item.snapshotExitImage)" />
|
|
@@ -127,6 +128,13 @@
|
|
|
127
128
|
{{ (key as any)?.prefixAndName }}
|
|
128
129
|
</v-chip>
|
|
129
130
|
</div>
|
|
131
|
+
<div v-if="showAddPassKeyButton(item)" class="d-flex flex-wrap ga-1 mt-1 mt-2"
|
|
132
|
+
@click.stop="handleCheckout(item._id)">
|
|
133
|
+
<v-chip size="x-small" variant="tonal" prepend-icon="mdi-key" @click.stop="handleOpenAddPassKey(item)">
|
|
134
|
+
Add Pass/Key
|
|
135
|
+
</v-chip>
|
|
136
|
+
|
|
137
|
+
</div>
|
|
130
138
|
</v-row>
|
|
131
139
|
</template>
|
|
132
140
|
|
|
@@ -180,7 +188,7 @@
|
|
|
180
188
|
</span>
|
|
181
189
|
|
|
182
190
|
<span v-else-if="selectedVisitorObject[key]" class="d-flex ga-3 align-center"><strong>{{ label
|
|
183
|
-
|
|
191
|
+
}}:</strong>
|
|
184
192
|
{{ formatValues(key, selectedVisitorObject[key]) }}
|
|
185
193
|
<TooltipInfo v-if="key === 'checkOut'" text="Manual Checkout" density="compact" size="x-small" />
|
|
186
194
|
</span>
|
|
@@ -251,9 +259,9 @@
|
|
|
251
259
|
{{ (pass as any)?.prefixAndName }}
|
|
252
260
|
</v-chip>
|
|
253
261
|
<v-select hide-details max-width="200px" density="compact" :items="passStatusOptions" item-title="label"
|
|
254
|
-
item-value="value" v-model="pass.status"></v-select>
|
|
262
|
+
item-value="value" v-model="pass.status" :disabled="selectedVisitorObject.checkOut"></v-select>
|
|
255
263
|
<v-textarea v-if="pass.status === 'Lost' || pass.status === 'Damaged'" no-resize rows="3" class="w-100"
|
|
256
|
-
density="compact" v-model="pass.remarks"></v-textarea>
|
|
264
|
+
density="compact" v-model="pass.remarks" :disabled="selectedVisitorObject.checkOut"></v-textarea>
|
|
257
265
|
</v-row>
|
|
258
266
|
</div>
|
|
259
267
|
<div v-if="(keyReturnStatuses.length > 0)" class="mb-2">
|
|
@@ -264,32 +272,37 @@
|
|
|
264
272
|
{{ (key as any)?.prefixAndName }}
|
|
265
273
|
</v-chip>
|
|
266
274
|
<v-select hide-details max-width="200px" density="compact" :items="passStatusOptions" item-title="label"
|
|
267
|
-
item-value="value" v-model="key.status">
|
|
268
|
-
</v-select>
|
|
275
|
+
item-value="value" v-model="key.status" :disabled="selectedVisitorObject.checkOut"></v-select>
|
|
269
276
|
<v-textarea v-if="key.status === 'Lost' || key.status === 'Damaged'" no-resize rows="3" class="w-100"
|
|
270
|
-
density="compact" v-model="key.remarks"></v-textarea>
|
|
277
|
+
density="compact" v-model="key.remarks" :disabled="selectedVisitorObject.checkOut"></v-textarea>
|
|
271
278
|
</v-row>
|
|
272
279
|
</div>
|
|
273
280
|
|
|
274
281
|
<v-row no-gutters class="my-5">
|
|
275
|
-
<v-btn variant="flat" color="blue" density="comfortable" class="text-capitalize" :
|
|
282
|
+
<v-btn variant="flat" color="blue" density="comfortable" class="text-capitalize" :disabled="selectedVisitorObject.checkOut"
|
|
283
|
+
:loading="loading.updatingPassKeys" @click.stop="handleUpdatePassKeys" >Update Pass/Keys</v-btn>
|
|
276
284
|
</v-row>
|
|
277
285
|
</v-card-text>
|
|
278
286
|
<v-toolbar class="pa-0" density="compact">
|
|
279
287
|
<v-row no-gutters>
|
|
280
288
|
<v-col cols="6">
|
|
281
|
-
<v-btn variant="text" block
|
|
289
|
+
<v-btn variant="text" block
|
|
282
290
|
@click="dialog.returnPassesKeys = false">Close</v-btn>
|
|
283
291
|
</v-col>
|
|
284
292
|
<v-col cols="6">
|
|
285
293
|
<v-btn color="red" variant="flat" height="48" rounded="0" block :loading="loading.checkingOut"
|
|
286
|
-
:disabled="!canConfirmCheckout" @click="proceedCheckout">Confirm Checkout</v-btn>
|
|
294
|
+
:disabled="!canConfirmCheckout || selectedVisitorObject.checkOut" @click="proceedCheckout">Confirm Checkout</v-btn>
|
|
287
295
|
</v-col>
|
|
288
296
|
</v-row>
|
|
289
297
|
</v-toolbar>
|
|
290
298
|
</v-card>
|
|
291
299
|
</v-dialog>
|
|
292
300
|
|
|
301
|
+
<v-dialog v-model="dialog.addPassKey" width="450" persistent>
|
|
302
|
+
<AddPassKeyToVisitor :visitor="selectedVisitorDataObject" :site="siteId" @close="dialog.addPassKey = false"
|
|
303
|
+
@update="getVisitorRefresh" />
|
|
304
|
+
</v-dialog>
|
|
305
|
+
|
|
293
306
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
294
307
|
</v-row>
|
|
295
308
|
</template>
|
|
@@ -379,6 +392,7 @@ const dialog = reactive({
|
|
|
379
392
|
snapshotImage: false,
|
|
380
393
|
remarks: false,
|
|
381
394
|
returnPassesKeys: false,
|
|
395
|
+
addPassKey: false
|
|
382
396
|
});
|
|
383
397
|
|
|
384
398
|
const snapshotImageUrl = ref("");
|
|
@@ -507,8 +521,8 @@ const {
|
|
|
507
521
|
|
|
508
522
|
watch(getVisitorReq, (newData: any) => {
|
|
509
523
|
if (newData) {
|
|
510
|
-
|
|
511
|
-
items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
|
|
524
|
+
items.value = newData.items ?? [];
|
|
525
|
+
// items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
|
|
512
526
|
pages.value = newData.pages ?? 0;
|
|
513
527
|
pageRange.value = newData?.pageRange ?? "-- - -- of --";
|
|
514
528
|
}
|
|
@@ -540,6 +554,8 @@ function formatValues(key: string, value: any) {
|
|
|
540
554
|
return value;
|
|
541
555
|
}
|
|
542
556
|
|
|
557
|
+
|
|
558
|
+
|
|
543
559
|
function handleAddNew() {
|
|
544
560
|
dialog.showSelection = true;
|
|
545
561
|
activeVisitorFormType.value = null;
|
|
@@ -555,6 +571,15 @@ function handleUpdatePage(newPageNum: number) {
|
|
|
555
571
|
page.value = newPageNum;
|
|
556
572
|
}
|
|
557
573
|
|
|
574
|
+
function showAddPassKeyButton(item: any) {
|
|
575
|
+
const hasPasses = (item?.visitorPass?.length ?? 0) > 0;
|
|
576
|
+
const hasKeys = (item?.passKeys?.length ?? 0) > 0;
|
|
577
|
+
|
|
578
|
+
const isTypeWithPassKey = ["contractor", "guest", "walk-in"].includes(item?.type);
|
|
579
|
+
|
|
580
|
+
return !hasPasses && !hasKeys && isTypeWithPassKey && item?.status === "registered" && !item?.checkOut;
|
|
581
|
+
}
|
|
582
|
+
|
|
558
583
|
function handleSelectVisitorType(type: TVisitorType) {
|
|
559
584
|
dialog.showSelection = false;
|
|
560
585
|
dialog.showForm = true;
|
|
@@ -607,6 +632,11 @@ function handleViewImage(imageId: string) {
|
|
|
607
632
|
dialog.snapshotImage = true;
|
|
608
633
|
}
|
|
609
634
|
|
|
635
|
+
function handleOpenAddPassKey(item: any) {
|
|
636
|
+
selectedVisitorId.value = item?._id;
|
|
637
|
+
dialog.addPassKey = true;
|
|
638
|
+
}
|
|
639
|
+
|
|
610
640
|
function handleOpenRemarks(item: any, type: 'checkIn' | 'checkOut') {
|
|
611
641
|
selectedVisitorId.value = item?._id;
|
|
612
642
|
remarksType.value = type;
|
|
@@ -635,16 +665,18 @@ async function handleSaveRemarks() {
|
|
|
635
665
|
}
|
|
636
666
|
}
|
|
637
667
|
|
|
638
|
-
async function handleUpdatePassKeys(){
|
|
668
|
+
async function handleUpdatePassKeys() {
|
|
639
669
|
if (!selectedVisitorId.value) return;
|
|
640
670
|
try {
|
|
641
671
|
loading.updatingPassKeys = true;
|
|
642
672
|
const payload: any = {};
|
|
643
673
|
if (passReturnStatuses.value.length > 0) {
|
|
644
|
-
|
|
674
|
+
const passReturnStatusesPayload = passReturnStatuses.value.map(p => ({ keyId: p.keyId, status: p.status, remarks: p.remarks }));
|
|
675
|
+
payload.visitorPass = passReturnStatusesPayload;
|
|
645
676
|
}
|
|
646
677
|
if (keyReturnStatuses.value.length > 0) {
|
|
647
|
-
|
|
678
|
+
const keyReturnStatusesPayload = keyReturnStatuses.value.map(k => ({ keyId: k.keyId, status: k.status, remarks: k.remarks }));
|
|
679
|
+
payload.passKeys = keyReturnStatusesPayload;
|
|
648
680
|
}
|
|
649
681
|
await updateVisitor(selectedVisitorId.value, payload);
|
|
650
682
|
showMessage("Pass/Key statuses updated successfully!", "info");
|
|
@@ -723,10 +755,14 @@ async function proceedCheckout() {
|
|
|
723
755
|
|
|
724
756
|
try {
|
|
725
757
|
loading.checkingOut = true;
|
|
758
|
+
|
|
759
|
+
const passReturnStatusesPayload = passReturnStatuses.value.map(p => ({ keyId: p.keyId, status: p.status, remarks: p.remarks }));
|
|
760
|
+
const keyReturnStatusesPayload = keyReturnStatuses.value.map(k => ({ keyId: k.keyId, status: k.status, remarks: k.remarks }));
|
|
761
|
+
|
|
726
762
|
const res = await updateVisitor(userId as string, {
|
|
727
763
|
checkOut: new Date().toISOString(),
|
|
728
|
-
...(passReturnStatuses.value.length > 0 ? { visitorPass:
|
|
729
|
-
...(keyReturnStatuses.value.length > 0 ? { passKeys:
|
|
764
|
+
...(passReturnStatuses.value.length > 0 ? { visitorPass: passReturnStatusesPayload } : {}),
|
|
765
|
+
...(keyReturnStatuses.value.length > 0 ? { passKeys: keyReturnStatusesPayload } : {}),
|
|
730
766
|
});
|
|
731
767
|
if (res) {
|
|
732
768
|
showMessage("Visitor successfully checked-out!", "info");
|
|
@@ -98,6 +98,19 @@ export default function () {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
async function updateSiteInformation(
|
|
102
|
+
siteId: string,
|
|
103
|
+
payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
|
|
104
|
+
) {
|
|
105
|
+
return await useNuxtApp().$api<Record<string, any>>(
|
|
106
|
+
`/api/sites/information/id/${siteId}`,
|
|
107
|
+
{
|
|
108
|
+
method: "PATCH",
|
|
109
|
+
body: payload,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
async function setSiteGuardPosts(siteId: string, value: number) {
|
|
102
115
|
return await useNuxtApp().$api<Record<string, any>>(
|
|
103
116
|
`/api/sites/guard-post/id/${siteId}`,
|
|
@@ -138,6 +151,7 @@ export default function () {
|
|
|
138
151
|
updateSiteCamera,
|
|
139
152
|
deleteSiteCameraById,
|
|
140
153
|
updateSitebyId,
|
|
154
|
+
updateSiteInformation,
|
|
141
155
|
getOvernightParkingAvailability,
|
|
142
156
|
updateOvernightParkingAvailability
|
|
143
157
|
};
|