@7365admin1/layer-common 3.0.18 → 3.0.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/BuildingManagement/buildings.vue +7 -4
- package/components/BuildingManagement/units.vue +187 -9
- package/components/HidAccessLogDashboard.vue +184 -21
- package/components/HidReaderForm.vue +20 -12
- package/components/HidReaderManagement.vue +113 -80
- package/components/MemberInformation.vue +240 -163
- package/components/PassInformation.vue +6 -4
- package/components/VisitorForm.vue +41 -18
- package/composables/useBuilding.ts +14 -0
- package/composables/useVisitor.ts +19 -15
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +3 -1
- package/types/visitor.d.ts +1 -1
|
@@ -1,164 +1,234 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
2
|
+
<v-row no-gutters class="w-100">
|
|
3
|
+
<v-card class="w-100">
|
|
4
|
+
<v-card-text>
|
|
5
|
+
<v-btn
|
|
6
|
+
block
|
|
7
|
+
color="primary-button"
|
|
8
|
+
:height="40"
|
|
9
|
+
text="Scan QR Code"
|
|
10
|
+
class="text-capitalize"
|
|
11
|
+
prepend-icon="mdi-qrcode"
|
|
12
|
+
@click="handleScanQRPass"
|
|
13
|
+
/>
|
|
14
|
+
<v-form
|
|
15
|
+
ref="formRef"
|
|
16
|
+
v-model="validForm"
|
|
17
|
+
:disabled="processing"
|
|
18
|
+
@click="errorScanPassMessage = ''"
|
|
19
|
+
>
|
|
20
|
+
<v-row no-gutters class="pt-5 ga-2">
|
|
21
|
+
<v-col cols="12">
|
|
22
|
+
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
23
|
+
<v-text-field
|
|
24
|
+
v-model.trim="memberForm.name"
|
|
25
|
+
density="compact"
|
|
26
|
+
hide-details
|
|
27
|
+
:rules="[requiredRule]"
|
|
28
|
+
/>
|
|
29
|
+
</v-col>
|
|
30
|
+
|
|
31
|
+
<v-col cols="12">
|
|
32
|
+
<InputLabel class="text-capitalize" title="NRIC" required />
|
|
33
|
+
<InputNRICNumber
|
|
34
|
+
v-model.trim="memberForm.nric"
|
|
35
|
+
density="compact"
|
|
36
|
+
hide-details
|
|
37
|
+
:rules="[requiredRule]"
|
|
38
|
+
/>
|
|
39
|
+
</v-col>
|
|
40
|
+
|
|
41
|
+
<v-col cols="12">
|
|
42
|
+
<v-autocomplete
|
|
43
|
+
v-model="selectedPass"
|
|
44
|
+
v-model:search="passInput"
|
|
45
|
+
:hide-no-data="false"
|
|
46
|
+
class="mt-3"
|
|
47
|
+
:items="passItemsFilteredFinal"
|
|
48
|
+
item-title="prefixAndName"
|
|
49
|
+
item-value="_id"
|
|
50
|
+
label="Pass (optional)"
|
|
51
|
+
variant="outlined"
|
|
52
|
+
density="compact"
|
|
53
|
+
:error-messages="errorScanPassMessage"
|
|
54
|
+
persistent-hint
|
|
55
|
+
small-chips
|
|
56
|
+
:clearable="clearable"
|
|
57
|
+
@click="fetchPasses"
|
|
58
|
+
>
|
|
59
|
+
<template v-slot:no-data>
|
|
60
|
+
<v-list-item density="compact">
|
|
61
|
+
<v-list-item-title v-if="passInput">
|
|
62
|
+
No results matching "<strong>{{ passInput }}</strong
|
|
63
|
+
>". This value will be added as new option.
|
|
64
|
+
</v-list-item-title>
|
|
65
|
+
<v-list-item v-else>No data available</v-list-item>
|
|
66
|
+
</v-list-item>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<template v-slot:chip="{ props, item }">
|
|
70
|
+
<v-chip
|
|
71
|
+
v-if="selectedPass"
|
|
72
|
+
v-bind="props"
|
|
73
|
+
prepend-icon="mdi-card-bulleted-outline"
|
|
74
|
+
:text="item.raw?.prefixAndName"
|
|
75
|
+
></v-chip>
|
|
76
|
+
</template>
|
|
77
|
+
</v-autocomplete>
|
|
78
|
+
</v-col>
|
|
79
|
+
|
|
80
|
+
<v-col cols="12">
|
|
81
|
+
<InputLabel
|
|
82
|
+
class="text-capitalize"
|
|
83
|
+
title="Phone Number"
|
|
84
|
+
required
|
|
85
|
+
/>
|
|
86
|
+
<InputPhoneNumberV2
|
|
87
|
+
v-model="memberForm.contact"
|
|
88
|
+
density="compact"
|
|
89
|
+
default-country="SG"
|
|
90
|
+
hide-details
|
|
91
|
+
/>
|
|
92
|
+
</v-col>
|
|
93
|
+
|
|
94
|
+
<v-row class="pt-3" justify="space-between">
|
|
95
|
+
<v-col cols="6">
|
|
96
|
+
<v-btn
|
|
97
|
+
text="Add Member"
|
|
98
|
+
:disabled="!validForm"
|
|
99
|
+
prepend-icon="mdi-plus"
|
|
100
|
+
color="primary-button"
|
|
101
|
+
class="text-capitalize"
|
|
102
|
+
@click="handleAddMember"
|
|
103
|
+
/>
|
|
104
|
+
</v-col>
|
|
105
|
+
<v-col cols="6" align="end">
|
|
106
|
+
<v-btn
|
|
107
|
+
text="Clear Form"
|
|
108
|
+
prepend-icon="mdi-refresh"
|
|
109
|
+
color="primary-button"
|
|
110
|
+
class="text-capitalize"
|
|
111
|
+
@click.stop="handleClearForm"
|
|
112
|
+
/>
|
|
113
|
+
</v-col>
|
|
114
|
+
</v-row>
|
|
115
|
+
</v-row>
|
|
116
|
+
</v-form>
|
|
117
|
+
</v-card-text>
|
|
118
|
+
</v-card>
|
|
119
|
+
<visitor-pass-key-q-r-scanner
|
|
120
|
+
:dialog="dialog.scanQRCode"
|
|
121
|
+
v-model:scannedValue="scannedPassValue"
|
|
122
|
+
@close-dialog="dialog.scanQRCode = false"
|
|
123
|
+
/>
|
|
124
|
+
<v-divider class="my-3" />
|
|
125
|
+
<v-row
|
|
126
|
+
v-if="committedMembers.length > 0"
|
|
127
|
+
no-gutters
|
|
128
|
+
class="w-100 mt-5 ga-3"
|
|
129
|
+
>
|
|
130
|
+
<template v-for="(member, index) in committedMembers" :key="member.nric">
|
|
131
|
+
<CardMemberInfoSummary
|
|
132
|
+
:member="membersDisplayed[index]"
|
|
133
|
+
@remove="handleRemoveMember(index)"
|
|
134
|
+
/>
|
|
135
|
+
</template>
|
|
73
136
|
</v-row>
|
|
137
|
+
<!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
|
|
138
|
+
</v-row>
|
|
74
139
|
</template>
|
|
75
140
|
|
|
76
141
|
<script setup lang="ts">
|
|
77
|
-
import type { PropType } from
|
|
78
|
-
import usePassKey from
|
|
79
|
-
import useUtils from
|
|
80
|
-
|
|
142
|
+
import type { PropType } from "vue";
|
|
143
|
+
import usePassKey from "../composables/usePassKey";
|
|
144
|
+
import useUtils from "../composables/useUtils";
|
|
81
145
|
|
|
82
146
|
const props = defineProps({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
type: {
|
|
148
|
+
type: String,
|
|
149
|
+
required: true,
|
|
150
|
+
},
|
|
151
|
+
contractorType: {
|
|
152
|
+
type: String,
|
|
153
|
+
required: false,
|
|
154
|
+
},
|
|
155
|
+
site: {
|
|
156
|
+
type: String,
|
|
157
|
+
required: true,
|
|
158
|
+
},
|
|
159
|
+
selectedVisitorPass: {
|
|
160
|
+
type: Array as PropType<TPassKeyPayload[]>,
|
|
161
|
+
default: () => [],
|
|
162
|
+
},
|
|
163
|
+
clearable: {
|
|
164
|
+
type: Boolean,
|
|
165
|
+
default: false,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const { requiredRule } = useUtils();
|
|
170
|
+
const { getPassKeysByPageSearch } = usePassKey();
|
|
104
171
|
|
|
105
172
|
const memberForm = reactive<TMemberInfo>({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const validForm = ref(false)
|
|
113
|
-
const formRef = ref<HTMLFormElement>()
|
|
173
|
+
name: "",
|
|
174
|
+
nric: "",
|
|
175
|
+
visitorPass: [] as { keyId: string }[],
|
|
176
|
+
contact: "",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const validForm = ref(false);
|
|
180
|
+
const formRef = ref<HTMLFormElement>();
|
|
114
181
|
const processing = ref(false);
|
|
115
|
-
const errorScanPassMessage = ref(
|
|
182
|
+
const errorScanPassMessage = ref("");
|
|
116
183
|
|
|
117
184
|
const message = ref("");
|
|
118
185
|
const messageColor = ref("");
|
|
119
186
|
const messageSnackbar = ref(false);
|
|
120
187
|
|
|
121
188
|
function showMessage(msg: string, color: string) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
189
|
+
message.value = msg;
|
|
190
|
+
messageColor.value = color;
|
|
191
|
+
messageSnackbar.value = true;
|
|
125
192
|
}
|
|
126
193
|
|
|
127
194
|
// pass
|
|
128
|
-
const passInput = ref(
|
|
129
|
-
const passItems = ref<TPassKey[]>([])
|
|
130
|
-
const scannedPassValue = ref<string>("")
|
|
195
|
+
const passInput = ref("");
|
|
196
|
+
const passItems = ref<TPassKey[]>([]);
|
|
197
|
+
const scannedPassValue = ref<string>("");
|
|
131
198
|
|
|
132
199
|
const dialog = reactive({
|
|
133
|
-
|
|
134
|
-
})
|
|
200
|
+
scanQRCode: false,
|
|
201
|
+
});
|
|
135
202
|
|
|
136
|
-
const selectedPass = ref<string>(
|
|
203
|
+
const selectedPass = ref<string>("");
|
|
137
204
|
|
|
138
|
-
const members = defineModel<TMemberInfo[]>({ required: true, default: [] })
|
|
139
|
-
const committedMembers = ref<TMemberInfo[]>([])
|
|
205
|
+
const members = defineModel<TMemberInfo[]>({ required: true, default: [] });
|
|
206
|
+
const committedMembers = ref<TMemberInfo[]>([]);
|
|
140
207
|
|
|
141
208
|
// Always keep the model in sync: committed members + current form draft
|
|
142
|
-
watch(
|
|
209
|
+
watch(
|
|
210
|
+
() => memberForm,
|
|
211
|
+
(newForm) => {
|
|
143
212
|
if (memberForm.name && memberForm.nric && memberForm.contact) {
|
|
144
|
-
|
|
213
|
+
members.value = [...committedMembers.value, { ...newForm }];
|
|
145
214
|
} else {
|
|
146
|
-
|
|
215
|
+
members.value = [...committedMembers.value];
|
|
147
216
|
}
|
|
148
|
-
},
|
|
149
|
-
|
|
217
|
+
},
|
|
218
|
+
{ deep: true }
|
|
219
|
+
);
|
|
150
220
|
|
|
151
221
|
const passTypesComputed = computed(() => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} else {
|
|
156
|
-
return ["contractor-pass"]
|
|
157
|
-
}
|
|
222
|
+
if (props.type === "contractor") {
|
|
223
|
+
if (props.contractorType === "property-agent") {
|
|
224
|
+
return ["agent-pass"];
|
|
158
225
|
} else {
|
|
159
|
-
|
|
226
|
+
return ["contractor-pass"];
|
|
160
227
|
}
|
|
161
|
-
}
|
|
228
|
+
} else {
|
|
229
|
+
return ["visitor-pass"];
|
|
230
|
+
}
|
|
231
|
+
});
|
|
162
232
|
|
|
163
233
|
const isPassListLoading = ref(false);
|
|
164
234
|
|
|
@@ -184,60 +254,67 @@ async function fetchPasses() {
|
|
|
184
254
|
}
|
|
185
255
|
|
|
186
256
|
const passItemsFilteredFinal = computed(() => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
257
|
+
return passItems.value.filter((item: TPassKey) => {
|
|
258
|
+
return (
|
|
259
|
+
!props.selectedVisitorPass.some(
|
|
260
|
+
(pass: TPassKeyPayload) => pass.keyId === item._id
|
|
261
|
+
) &&
|
|
262
|
+
!committedMembers.value.some((member: TMemberInfo) =>
|
|
263
|
+
member.visitorPass?.some((pass) => pass.keyId === item._id)
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
191
268
|
|
|
192
269
|
const membersDisplayed = computed(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
270
|
+
return committedMembers.value.map((member: TMemberInfo) => {
|
|
271
|
+
return {
|
|
272
|
+
...member,
|
|
273
|
+
visitorPass:
|
|
274
|
+
passItems.value.find(
|
|
275
|
+
(item: TPassKey) => item._id === member.visitorPass?.[0]?.keyId
|
|
276
|
+
)?.prefixAndName || "",
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
});
|
|
200
280
|
|
|
201
281
|
function handleScanQRPass() {
|
|
202
|
-
|
|
203
|
-
|
|
282
|
+
errorScanPassMessage.value = "";
|
|
283
|
+
dialog.scanQRCode = true;
|
|
204
284
|
}
|
|
205
285
|
|
|
206
286
|
watch(selectedPass, (newVal) => {
|
|
207
|
-
|
|
208
|
-
})
|
|
287
|
+
memberForm.visitorPass = [...(newVal ? [{ keyId: newVal }] : [])];
|
|
288
|
+
});
|
|
209
289
|
|
|
210
290
|
watch(scannedPassValue, (newVal) => {
|
|
211
|
-
|
|
291
|
+
if (!newVal) return;
|
|
212
292
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
selectedPass.value = matchedPass._id ?? ""
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
errorScanPassMessage.value = "Scanned pass not available"
|
|
221
|
-
showMessage("Scanned pass not available", "error")
|
|
222
|
-
})
|
|
293
|
+
const matchedPass = passItemsFilteredFinal.value.find(
|
|
294
|
+
(p) => p.prefixAndName === newVal
|
|
295
|
+
);
|
|
223
296
|
|
|
297
|
+
if (matchedPass) {
|
|
298
|
+
selectedPass.value = matchedPass._id ?? "";
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
224
301
|
|
|
302
|
+
errorScanPassMessage.value = "Scanned pass not available";
|
|
303
|
+
showMessage("Scanned pass not available", "error");
|
|
304
|
+
});
|
|
225
305
|
|
|
226
306
|
function handleClearForm() {
|
|
227
|
-
|
|
307
|
+
formRef.value?.reset();
|
|
228
308
|
}
|
|
229
309
|
|
|
230
310
|
async function handleAddMember() {
|
|
231
|
-
|
|
232
|
-
|
|
311
|
+
committedMembers.value.push({ ...memberForm });
|
|
312
|
+
handleClearForm();
|
|
233
313
|
}
|
|
234
314
|
|
|
235
315
|
function handleRemoveMember(index: number) {
|
|
236
|
-
|
|
316
|
+
committedMembers.value = committedMembers.value.filter((_, i) => i !== index);
|
|
237
317
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
318
|
</script>
|
|
242
319
|
|
|
243
|
-
<style scoped></style>
|
|
320
|
+
<style scoped></style>
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
density="compact"
|
|
58
58
|
persistent-hint
|
|
59
59
|
small-chips
|
|
60
|
+
closable-chips
|
|
60
61
|
:clearable="props.clearable"
|
|
61
62
|
@click="fetchKeys"
|
|
62
63
|
:loading="isKeyListLoading"
|
|
@@ -182,7 +183,7 @@ const isPassListLoading = ref(false);
|
|
|
182
183
|
|
|
183
184
|
async function fetchPasses() {
|
|
184
185
|
// load pass list
|
|
185
|
-
|
|
186
|
+
|
|
186
187
|
isPassListLoading.value = true;
|
|
187
188
|
try {
|
|
188
189
|
const result = await getPassKeysByPageSearch({
|
|
@@ -204,7 +205,6 @@ async function fetchPasses() {
|
|
|
204
205
|
const isKeyListLoading = ref(false);
|
|
205
206
|
|
|
206
207
|
async function fetchKeys() {
|
|
207
|
-
keyItems.value = [];
|
|
208
208
|
isKeyListLoading.value = true;
|
|
209
209
|
try {
|
|
210
210
|
const keys: any = await getPassKeysByPageSearch({
|
|
@@ -215,19 +215,21 @@ async function fetchKeys() {
|
|
|
215
215
|
statuses: ["Available"],
|
|
216
216
|
sortBy: { column: "prefixAndName", order: "1" },
|
|
217
217
|
});
|
|
218
|
-
|
|
218
|
+
|
|
219
|
+
const previouslySelectedKeys = keyItems.value.filter((item: any) => {
|
|
219
220
|
if (Array.isArray(props?.passKeys) && props.passKeys.length) {
|
|
220
221
|
return props.passKeys.includes(item._id);
|
|
221
222
|
}
|
|
222
223
|
return false;
|
|
223
224
|
});
|
|
225
|
+
|
|
224
226
|
const filteredArray = keys.items.filter((item: any) => {
|
|
225
227
|
if (Array.isArray(props?.passKeys) && props.passKeys.length) {
|
|
226
228
|
return !props.passKeys.includes(item._id);
|
|
227
229
|
}
|
|
228
230
|
return true;
|
|
229
231
|
});
|
|
230
|
-
keyItems.value = [...
|
|
232
|
+
keyItems.value = [...previouslySelectedKeys, ...filteredArray];
|
|
231
233
|
} catch (error) {
|
|
232
234
|
showMessage(errorConverter(error as string), "error");
|
|
233
235
|
} finally {
|