@7365admin1/layer-common 1.11.17 → 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 +12 -0
- package/components/AddPassKeyToVisitor.vue +207 -0
- package/components/BuildingUnitFormEdit.vue +41 -1
- package/components/Card/MemberInfoSummary.vue +5 -1
- package/components/Input/FileV2.vue +4 -3
- package/components/MemberInformation.vue +98 -20
- package/components/PassInformation.vue +75 -30
- package/components/VehicleManagement.vue +107 -34
- package/components/VisitorForm.vue +72 -30
- package/components/VisitorManagement.vue +278 -27
- package/composables/useKey.ts +4 -0
- package/composables/useLocalAuth.ts +2 -4
- package/composables/useSiteSettings.ts +14 -0
- package/composables/useTemplateReusable.ts +5 -1
- package/composables/useVehicle.ts +3 -3
- package/package.json +1 -1
- package/types/vehicle.d.ts +2 -1
- package/types/visitor.d.ts +13 -2
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,9 +19,9 @@
|
|
|
19
19
|
</v-col>
|
|
20
20
|
|
|
21
21
|
<v-col cols="12">
|
|
22
|
-
<v-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
<v-autocomplete v-model="selectedPass" v-model:search="passInput"
|
|
23
|
+
:hide-no-data="false" class="mt-3" :items="passItemsFilteredFinal"
|
|
24
|
+
item-title="prefixAndName" item-value="_id" label="Pass (optional)" variant="outlined"
|
|
25
25
|
hide-details density="compact" persistent-hint small-chips>
|
|
26
26
|
<template v-slot:no-data>
|
|
27
27
|
<v-list-item density="compact">
|
|
@@ -33,12 +33,19 @@
|
|
|
33
33
|
<v-list-item v-else>No data available</v-list-item>
|
|
34
34
|
</v-list-item>
|
|
35
35
|
</template>
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
<template v-slot:chip="{ props, item }">
|
|
38
|
+
<v-chip v-if="selectedPass" v-bind="props"
|
|
39
|
+
prepend-icon="mdi-card-bulleted-outline"
|
|
40
|
+
:text="item.raw?.prefixAndName"></v-chip>
|
|
41
|
+
</template>
|
|
42
|
+
</v-autocomplete>
|
|
37
43
|
</v-col>
|
|
38
44
|
|
|
39
45
|
<v-col cols="12">
|
|
40
46
|
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
41
|
-
|
|
47
|
+
<InputPhoneNumberV2 v-model="memberForm.contact" density="compact" default-country="SG"
|
|
48
|
+
hide-details />
|
|
42
49
|
</v-col>
|
|
43
50
|
|
|
44
51
|
<v-row class="pt-3" justify="space-between">
|
|
@@ -56,20 +63,41 @@
|
|
|
56
63
|
</v-card-text>
|
|
57
64
|
</v-card>
|
|
58
65
|
<v-divider class="my-3" />
|
|
59
|
-
<v-row v-if="
|
|
60
|
-
<template v-for="member, index in
|
|
61
|
-
<CardMemberInfoSummary :member="
|
|
66
|
+
<v-row v-if="committedMembers.length > 0" no-gutters class="w-100 mt-5 ga-3">
|
|
67
|
+
<template v-for="member, index in committedMembers" :key="member.nric">
|
|
68
|
+
<CardMemberInfoSummary :member="membersDisplayed[index]" @remove="handleRemoveMember(index)" />
|
|
62
69
|
</template>
|
|
63
70
|
</v-row>
|
|
64
71
|
</v-row>
|
|
65
72
|
</template>
|
|
66
73
|
|
|
67
74
|
<script setup lang="ts">
|
|
75
|
+
import type { PropType } from 'vue'
|
|
76
|
+
import usePassKey from '../composables/usePassKey'
|
|
77
|
+
import useUtils from '../composables/useUtils'
|
|
78
|
+
|
|
68
79
|
|
|
69
80
|
const props = defineProps({
|
|
81
|
+
type: {
|
|
82
|
+
type: String,
|
|
83
|
+
required: true
|
|
84
|
+
},
|
|
85
|
+
contractorType: {
|
|
86
|
+
type: String,
|
|
87
|
+
required: false
|
|
88
|
+
},
|
|
89
|
+
site: {
|
|
90
|
+
type: String,
|
|
91
|
+
required: true
|
|
92
|
+
},
|
|
93
|
+
selectedVisitorPass: {
|
|
94
|
+
type: Array as PropType<TPassKeyPayload[]>,
|
|
95
|
+
default: () => []
|
|
96
|
+
}
|
|
70
97
|
})
|
|
71
98
|
|
|
72
99
|
const { requiredRule } = useUtils()
|
|
100
|
+
const { getPassKeysByPageSearch } = usePassKey()
|
|
73
101
|
|
|
74
102
|
|
|
75
103
|
const memberForm = reactive<TMemberInfo>({
|
|
@@ -85,33 +113,83 @@ const processing = ref(false);
|
|
|
85
113
|
const errorMessage = ref('')
|
|
86
114
|
|
|
87
115
|
// pass
|
|
88
|
-
const pass = ref()
|
|
89
116
|
const passInput = ref('')
|
|
90
|
-
const passItems = ref([
|
|
117
|
+
const passItems = ref<TPassKey[]>([])
|
|
118
|
+
|
|
119
|
+
const selectedPass = ref<string>('')
|
|
120
|
+
|
|
121
|
+
const members = defineModel<TMemberInfo[]>({ required: true, default: [] })
|
|
122
|
+
const committedMembers = ref<TMemberInfo[]>([])
|
|
123
|
+
|
|
124
|
+
// Always keep the model in sync: committed members + current form draft
|
|
125
|
+
watch(() => memberForm, (newForm) => {
|
|
126
|
+
if (memberForm.name && memberForm.nric && memberForm.contact) {
|
|
127
|
+
members.value = [...committedMembers.value, { ...newForm }]
|
|
128
|
+
} else {
|
|
129
|
+
members.value = [...committedMembers.value]
|
|
130
|
+
}
|
|
131
|
+
}, { deep: true })
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
const passTypesComputed = computed(() => {
|
|
135
|
+
if (props.type === 'contractor') {
|
|
136
|
+
if (props.contractorType === 'property-agent') {
|
|
137
|
+
return ["agent-pass"]
|
|
138
|
+
} else {
|
|
139
|
+
return ["contractor-pass"]
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
return ["visitor-pass"]
|
|
143
|
+
}
|
|
144
|
+
})
|
|
91
145
|
|
|
92
|
-
const
|
|
146
|
+
const { data: passesData, refresh: refreshPassesData, pending: fetchPassesPending } = await useLazyAsyncData('get-pass-keys', () => {
|
|
147
|
+
return getPassKeysByPageSearch({
|
|
148
|
+
search: passInput.value,
|
|
149
|
+
page: 1,
|
|
150
|
+
limit: 500,
|
|
151
|
+
passTypes: passTypesComputed.value,
|
|
152
|
+
sites: [props.site],
|
|
153
|
+
statuses: ["Available"]
|
|
154
|
+
})
|
|
155
|
+
})
|
|
93
156
|
|
|
157
|
+
watch(passesData, (data: any) => {
|
|
158
|
+
passItems.value = Array.isArray(data?.items) ? data.items : []
|
|
159
|
+
})
|
|
94
160
|
|
|
95
|
-
|
|
161
|
+
const passItemsFilteredFinal = computed(() => {
|
|
162
|
+
return passItems.value.filter((item: TPassKey) => {
|
|
163
|
+
return !props.selectedVisitorPass.some((pass: TPassKeyPayload) => pass.keyId === item._id) && !committedMembers.value.some((member: TMemberInfo) => member.visitorPass === item._id)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const membersDisplayed = computed(() => {
|
|
168
|
+
return committedMembers.value.map((member: TMemberInfo) => {
|
|
169
|
+
return {
|
|
170
|
+
...member,
|
|
171
|
+
visitorPass: passItems.value.find((item: TPassKey) => item._id === member.visitorPass)?.prefixAndName || ""
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
watch(selectedPass, (newVal) => {
|
|
177
|
+
memberForm.visitorPass = [{ keyId: newVal }]
|
|
178
|
+
})
|
|
96
179
|
|
|
97
|
-
}
|
|
98
180
|
|
|
99
|
-
function handleSelectPass(val: any) {
|
|
100
|
-
memberForm.visitorPass = val
|
|
101
|
-
}
|
|
102
181
|
|
|
103
182
|
function handleClearForm() {
|
|
104
183
|
formRef.value?.reset()
|
|
105
184
|
}
|
|
106
185
|
|
|
107
186
|
async function handleAddMember() {
|
|
108
|
-
|
|
187
|
+
committedMembers.value.push({ ...memberForm })
|
|
109
188
|
handleClearForm()
|
|
110
189
|
}
|
|
111
190
|
|
|
112
|
-
function handleRemoveMember(index: number){
|
|
113
|
-
|
|
114
|
-
members.value = filtered
|
|
191
|
+
function handleRemoveMember(index: number) {
|
|
192
|
+
committedMembers.value = committedMembers.value.filter((_, i) => i !== index)
|
|
115
193
|
}
|
|
116
194
|
|
|
117
195
|
|
|
@@ -4,27 +4,36 @@
|
|
|
4
4
|
<v-card-text>
|
|
5
5
|
<v-btn block color="primary-button" :height="40" text="Scan QR Code" class="text-capitalize" disabled
|
|
6
6
|
prepend-icon="mdi-qrcode" />
|
|
7
|
-
<v-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<template v-slot:
|
|
12
|
-
<v-
|
|
13
|
-
|
|
14
|
-
No results matching "<strong>{{ passInput }}</strong>". This value will be
|
|
15
|
-
added as new
|
|
16
|
-
option.
|
|
17
|
-
</v-list-item-title>
|
|
18
|
-
<v-list-item v-else>No data available</v-list-item>
|
|
19
|
-
</v-list-item>
|
|
7
|
+
<v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false" class="mt-3"
|
|
8
|
+
:items="passItems" :rules="props.passRules" item-title="prefixAndName" item-value="_id" label="Pass"
|
|
9
|
+
variant="outlined" hide-details density="compact" persistent-hint small-chips>
|
|
10
|
+
|
|
11
|
+
<template v-slot:chip="{ props, item }">
|
|
12
|
+
<v-chip v-if="selectedPass" v-bind="props" prepend-icon="mdi-card-bulleted-outline"
|
|
13
|
+
:text="item.raw?.prefixAndName"></v-chip>
|
|
20
14
|
</template>
|
|
21
|
-
</v-
|
|
15
|
+
</v-autocomplete>
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
<template v-if="!props.hideKeys">
|
|
19
|
+
<v-autocomplete v-model="selectedKeys" v-model:search="keyInput" :hide-no-data="false" class="mt-3"
|
|
20
|
+
:items="keyItems" :rules="props.passRules" item-title="prefixAndName" item-value="_id"
|
|
21
|
+
label="Keys" multiple variant="outlined" hide-details density="compact" persistent-hint
|
|
22
|
+
small-chips>
|
|
23
|
+
|
|
24
|
+
<template v-slot:chip="{ props, item }">
|
|
25
|
+
<v-chip v-if="selectedKeys.length > 0" v-bind="props" prepend-icon="mdi-key"
|
|
26
|
+
:text="item.raw?.prefixAndName"></v-chip>
|
|
27
|
+
</template>
|
|
28
|
+
</v-autocomplete>
|
|
29
|
+
</template>
|
|
22
30
|
|
|
23
31
|
<v-divider class="my-4 w-100" />
|
|
24
|
-
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
<template v-if="selectedType">
|
|
27
|
-
<v-number-input v-model="count" variant="outlined" :min="1" :precision="0" density="compact"
|
|
35
|
+
<v-number-input v-model="count" variant="outlined" :min="1" :precision="0" density="compact"
|
|
36
|
+
:rules="countRules" />
|
|
28
37
|
</template>
|
|
29
38
|
</v-card-text>
|
|
30
39
|
</v-card>
|
|
@@ -35,6 +44,7 @@
|
|
|
35
44
|
import type { PropType } from 'vue'
|
|
36
45
|
import type { ValidationRule } from 'vuetify/lib/types.mjs'
|
|
37
46
|
import usePassKey from '../composables/usePassKey'
|
|
47
|
+
import useKey from '../composables/useKey'
|
|
38
48
|
|
|
39
49
|
const props = defineProps({
|
|
40
50
|
passRules: {
|
|
@@ -56,12 +66,21 @@ const props = defineProps({
|
|
|
56
66
|
contractorType: {
|
|
57
67
|
type: String,
|
|
58
68
|
default: ""
|
|
69
|
+
},
|
|
70
|
+
hideKeys: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false
|
|
59
73
|
}
|
|
60
74
|
})
|
|
61
75
|
|
|
62
|
-
const pass =
|
|
76
|
+
const pass = defineModel<TPassKeyPayload[]>("pass", { default: [] })
|
|
77
|
+
const keys = defineModel<TPassKeyPayload[]>("keys", { default: [] })
|
|
78
|
+
const selectedPass = ref<string>('')
|
|
79
|
+
const selectedKeys = ref<string[]>([])
|
|
63
80
|
const passInput = ref('')
|
|
64
|
-
const
|
|
81
|
+
const keyInput = ref('')
|
|
82
|
+
const passItems = ref<TPassKey[]>([])
|
|
83
|
+
const keyItems = ref<any[]>([])
|
|
65
84
|
const selectedType = ref<'qr-pass' | 'nfc-card'>()
|
|
66
85
|
const count = ref(1)
|
|
67
86
|
|
|
@@ -79,15 +98,15 @@ const typeItems = [
|
|
|
79
98
|
]
|
|
80
99
|
|
|
81
100
|
const passTypesComputed = computed(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
if (props.type === 'contractor') {
|
|
102
|
+
if (props.contractorType === 'property-agent') {
|
|
103
|
+
return ["agent-pass"]
|
|
104
|
+
} else {
|
|
105
|
+
return ["contractor-pass"]
|
|
106
|
+
}
|
|
85
107
|
} else {
|
|
86
|
-
return ["
|
|
108
|
+
return ["visitor-pass"]
|
|
87
109
|
}
|
|
88
|
-
} else {
|
|
89
|
-
return ["visitor-pass"]
|
|
90
|
-
}
|
|
91
110
|
})
|
|
92
111
|
|
|
93
112
|
const { data: passesData, refresh: refreshPassesData, pending: fetchPassesPending } = await useLazyAsyncData('get-pass-keys', () => {
|
|
@@ -105,22 +124,48 @@ watch(passesData, (data: any) => {
|
|
|
105
124
|
passItems.value = data?.items || []
|
|
106
125
|
})
|
|
107
126
|
|
|
127
|
+
const { data: keysData, refresh: refreshKeysData, pending: fetchKeysPending } = await useLazyAsyncData('get-keys', () => {
|
|
128
|
+
return getPassKeysByPageSearch({
|
|
129
|
+
search: keyInput.value,
|
|
130
|
+
statuses: ["Available"],
|
|
131
|
+
passTypes: ['pass-key'],
|
|
132
|
+
page: 1,
|
|
133
|
+
limit: 500,
|
|
134
|
+
sites: [props.site],
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
watch(keysData, (data: any) => {
|
|
139
|
+
keyItems.value = data?.items || []
|
|
140
|
+
})
|
|
108
141
|
|
|
109
|
-
|
|
142
|
+
watch(selectedPass, (newVal) => {
|
|
143
|
+
pass.value = [{ keyId: newVal }]
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
watch(selectedKeys, (newVal) => {
|
|
147
|
+
keys.value = newVal.map(key => ({ keyId: key }))
|
|
148
|
+
})
|
|
110
149
|
|
|
111
|
-
}
|
|
112
150
|
|
|
113
|
-
function handleSelectPass() {
|
|
114
151
|
|
|
115
|
-
}
|
|
116
152
|
|
|
117
153
|
//prevent negative value;
|
|
118
154
|
watch(count, (newCount) => {
|
|
119
|
-
if(newCount < 1){
|
|
155
|
+
if (newCount < 1) {
|
|
120
156
|
count.value = 1
|
|
121
157
|
}
|
|
122
158
|
})
|
|
123
159
|
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
if (pass.value.length > 0) {
|
|
162
|
+
selectedPass.value = pass.value[0].keyId
|
|
163
|
+
}
|
|
164
|
+
if (keys.value.length > 0) {
|
|
165
|
+
selectedKeys.value = keys.value.map(k => k.keyId)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
124
169
|
</script>
|
|
125
170
|
|
|
126
171
|
<style scoped></style>
|