@7365admin1/layer-common 3.0.26 → 3.0.27
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 +7 -0
- package/components/AccessCardDetailsDialog.vue +98 -2
- package/components/AccessCardPreviewDialog.vue +31 -3
- package/components/AccessManagement.vue +10 -1
- package/components/EntryPassInformation.vue +55 -17
- package/components/HidReaderForm.vue +10 -16
- package/components/MemberInformation.vue +59 -1
- package/components/VisitorForm.vue +22 -6
- package/components/VisitorManagement.vue +790 -219
- package/composables/useAccessManagement.ts +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<div class="text-body-2 font-weight-medium">{{ details.type ?? "N/A" }}</div>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
<div>
|
|
33
|
+
<div v-if="details.status">
|
|
34
34
|
<div class="text-caption text-grey">Status</div>
|
|
35
35
|
<v-chip
|
|
36
36
|
:color="statusColor(details.status)"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
<div class="text-body-2 font-weight-medium">{{ details.isActivated ? "Yes" : "No" }}</div>
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
|
-
<div>
|
|
50
|
+
<div v-if="details.site?.name">
|
|
51
51
|
<div class="text-caption text-grey">Site</div>
|
|
52
52
|
<div class="text-body-2 font-weight-medium">{{ details.site?.name ?? "N/A" }}</div>
|
|
53
53
|
</div>
|
|
@@ -101,6 +101,81 @@
|
|
|
101
101
|
<div class="text-caption text-grey">Remarks</div>
|
|
102
102
|
<div class="text-body-2 font-weight-medium">{{ details.remarks }}</div>
|
|
103
103
|
</div>
|
|
104
|
+
|
|
105
|
+
<!-- History -->
|
|
106
|
+
<template v-if="details.history?.length > 0">
|
|
107
|
+
<v-divider />
|
|
108
|
+
<div
|
|
109
|
+
class="d-flex align-center justify-space-between cursor-pointer"
|
|
110
|
+
@click="historyOpen = !historyOpen"
|
|
111
|
+
>
|
|
112
|
+
<div class="text-caption font-weight-bold text-grey-darken-2 text-uppercase">
|
|
113
|
+
History ({{ details.history.length }})
|
|
114
|
+
</div>
|
|
115
|
+
<v-icon size="16" :icon="historyOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<template v-if="historyOpen">
|
|
119
|
+
<div
|
|
120
|
+
v-for="(entry, idx) in sortedHistory"
|
|
121
|
+
:key="entry._id ?? idx"
|
|
122
|
+
class="d-flex ga-2"
|
|
123
|
+
>
|
|
124
|
+
<!-- Timeline spine -->
|
|
125
|
+
<div class="d-flex flex-column align-center" style="width: 20px; flex-shrink: 0;">
|
|
126
|
+
<div
|
|
127
|
+
class="timeline-dot rounded-circle mt-1"
|
|
128
|
+
:class="entry.visitorId ? 'bg-blue' : 'bg-orange'"
|
|
129
|
+
/>
|
|
130
|
+
<div
|
|
131
|
+
v-if="idx < sortedHistory.length - 1"
|
|
132
|
+
class="timeline-line bg-grey-lighten-2 mt-1"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Entry content -->
|
|
137
|
+
<div class="d-flex flex-column ga-1 pb-4 flex-grow-1">
|
|
138
|
+
<div class="d-flex align-center justify-space-between">
|
|
139
|
+
<v-chip
|
|
140
|
+
size="x-small"
|
|
141
|
+
variant="tonal"
|
|
142
|
+
:color="entry.visitorId ? 'blue' : 'orange'"
|
|
143
|
+
:prepend-icon="entry.visitorId ? 'mdi-account-arrow-right' : 'mdi-arrow-u-left-top'"
|
|
144
|
+
>
|
|
145
|
+
{{ entry.visitorId ? "Assigned" : "Returned" }}
|
|
146
|
+
</v-chip>
|
|
147
|
+
<span class="text-caption text-grey">{{ formatDate(entry.createdAt) }}</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Assignment entry -->
|
|
151
|
+
<template v-if="entry.visitorId">
|
|
152
|
+
<div class="text-body-2 font-weight-medium">{{ entry.name ?? "N/A" }}</div>
|
|
153
|
+
<div class="text-caption text-grey-darken-1">
|
|
154
|
+
<span v-if="entry.nric">NRIC: {{ entry.nric }}</span>
|
|
155
|
+
<span v-if="entry.contact" class="ml-2">· {{ entry.contact }}</span>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="text-caption text-grey-darken-1">
|
|
158
|
+
<span v-if="entry.company">{{ entry.company }}</span>
|
|
159
|
+
<span v-if="entry.plateNumber" class="ml-2">· {{ entry.plateNumber }}</span>
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<!-- Return/checkout entry -->
|
|
164
|
+
<template v-else>
|
|
165
|
+
<div class="d-flex align-center ga-2">
|
|
166
|
+
<v-chip size="x-small" variant="flat" :color="statusColor(entry.status?.toLowerCase())">
|
|
167
|
+
{{ entry.status ?? "N/A" }}
|
|
168
|
+
</v-chip>
|
|
169
|
+
<span v-if="entry.damage" class="text-caption text-error">Damaged</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div v-if="entry.remarks" class="text-caption text-grey-darken-1">
|
|
172
|
+
Remarks: {{ entry.remarks }}
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</template>
|
|
178
|
+
</template>
|
|
104
179
|
</div>
|
|
105
180
|
</v-card-text>
|
|
106
181
|
</v-card>
|
|
@@ -131,6 +206,13 @@ const { getCardDetails } = useAccessManagement();
|
|
|
131
206
|
|
|
132
207
|
const details = ref<Record<string, any> | null>(null);
|
|
133
208
|
const pending = ref(false);
|
|
209
|
+
const historyOpen = ref(false);
|
|
210
|
+
|
|
211
|
+
const sortedHistory = computed(() =>
|
|
212
|
+
[...(details.value?.history ?? [])].sort(
|
|
213
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
214
|
+
)
|
|
215
|
+
);
|
|
134
216
|
|
|
135
217
|
watch(
|
|
136
218
|
() => props.modelValue,
|
|
@@ -145,6 +227,7 @@ watch(
|
|
|
145
227
|
}
|
|
146
228
|
} else {
|
|
147
229
|
details.value = null;
|
|
230
|
+
historyOpen.value = false;
|
|
148
231
|
}
|
|
149
232
|
}
|
|
150
233
|
);
|
|
@@ -172,3 +255,16 @@ function formatDate(date: string) {
|
|
|
172
255
|
}).format(new Date(date));
|
|
173
256
|
}
|
|
174
257
|
</script>
|
|
258
|
+
|
|
259
|
+
<style scoped>
|
|
260
|
+
.timeline-dot {
|
|
261
|
+
width: 10px;
|
|
262
|
+
height: 10px;
|
|
263
|
+
flex-shrink: 0;
|
|
264
|
+
}
|
|
265
|
+
.timeline-line {
|
|
266
|
+
width: 2px;
|
|
267
|
+
flex-grow: 1;
|
|
268
|
+
min-height: 16px;
|
|
269
|
+
}
|
|
270
|
+
</style>
|
|
@@ -116,9 +116,9 @@
|
|
|
116
116
|
<!-- Available Physical -->
|
|
117
117
|
<v-col cols="12" class="mt-3">
|
|
118
118
|
<strong>Available Physical</strong>
|
|
119
|
-
<div v-if="
|
|
119
|
+
<div v-if="availablePhysical.length" class="mt-1">
|
|
120
120
|
<v-chip
|
|
121
|
-
v-for="card in
|
|
121
|
+
v-for="card in availablePhysical"
|
|
122
122
|
:key="card._id"
|
|
123
123
|
size="small"
|
|
124
124
|
class="mr-1 mb-1"
|
|
@@ -133,6 +133,26 @@
|
|
|
133
133
|
<span v-else class="text-caption text-grey ml-1">None</span>
|
|
134
134
|
</v-col>
|
|
135
135
|
|
|
136
|
+
<!-- Damage Physical -->
|
|
137
|
+
<v-col cols="12" class="mt-2">
|
|
138
|
+
<strong>Damage</strong>
|
|
139
|
+
<div v-if="damagedPhysical.length" class="mt-1">
|
|
140
|
+
<v-chip
|
|
141
|
+
v-for="card in damagedPhysical"
|
|
142
|
+
:key="card._id"
|
|
143
|
+
size="small"
|
|
144
|
+
class="mr-1 mb-1"
|
|
145
|
+
color="deep-orange"
|
|
146
|
+
:variant="selectedCardInUnit?._id === card._id ? 'flat' : 'tonal'"
|
|
147
|
+
style="cursor: pointer"
|
|
148
|
+
@click="toggleCard(card)"
|
|
149
|
+
>
|
|
150
|
+
{{ card.cardNo }}
|
|
151
|
+
</v-chip>
|
|
152
|
+
</div>
|
|
153
|
+
<span v-else class="text-caption text-grey ml-1">None</span>
|
|
154
|
+
</v-col>
|
|
155
|
+
|
|
136
156
|
<!-- Available Non-Physical -->
|
|
137
157
|
<v-col cols="12" class="mt-2">
|
|
138
158
|
<strong>Available Non-Physical</strong>
|
|
@@ -424,11 +444,19 @@ function showToast(message: string, color: "success" | "error") {
|
|
|
424
444
|
snackbar.value = true;
|
|
425
445
|
}
|
|
426
446
|
|
|
447
|
+
const availablePhysical = computed(() =>
|
|
448
|
+
(props.unit?.available?.physical ?? []).filter((c: any) => !c.damage)
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const damagedPhysical = computed(() =>
|
|
452
|
+
(props.unit?.available?.physical ?? []).filter((c: any) => c.damage === true)
|
|
453
|
+
);
|
|
454
|
+
|
|
427
455
|
const isSelectedCardAvailable = computed(() => {
|
|
428
456
|
if (!props.selectedCardInUnit?._id) return false;
|
|
429
457
|
const id = props.selectedCardInUnit._id;
|
|
430
458
|
return [
|
|
431
|
-
...
|
|
459
|
+
...availablePhysical.value,
|
|
432
460
|
...(props.unit?.available?.non_physical ?? []),
|
|
433
461
|
].some((c) => c._id === id);
|
|
434
462
|
});
|
|
@@ -301,7 +301,16 @@ const loading = computed(() => getAllReqStatus.value === "pending");
|
|
|
301
301
|
|
|
302
302
|
watchEffect(() => {
|
|
303
303
|
if (getCardReq.value?.data) {
|
|
304
|
-
items.value = getCardReq.value.data.items
|
|
304
|
+
items.value = getCardReq.value.data.items.map((item: any) => ({
|
|
305
|
+
...item,
|
|
306
|
+
cardCounts: {
|
|
307
|
+
...item.cardCounts,
|
|
308
|
+
available: {
|
|
309
|
+
...item.cardCounts?.available,
|
|
310
|
+
physical: (item.available?.physical ?? []).filter((c: any) => !c.damage).length,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
}));
|
|
305
314
|
pages.value = getCardReq.value.data.pages;
|
|
306
315
|
pageRange.value = getCardReq.value.data.pageRange;
|
|
307
316
|
}
|
|
@@ -59,21 +59,39 @@
|
|
|
59
59
|
No printer is configured in the settings. QR Pass may not print.
|
|
60
60
|
</v-alert>
|
|
61
61
|
<template v-else>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</span>
|
|
67
|
-
<v-progress-circular v-else-if="cardsLoading" size="14" width="2" indeterminate color="grey" />
|
|
62
|
+
<!-- Generating / loading state -->
|
|
63
|
+
<div v-if="generating || cardsLoading" class="d-flex align-center ga-2 mt-3 text-caption text-grey-darken-1">
|
|
64
|
+
<v-progress-circular size="14" width="2" indeterminate color="grey" />
|
|
65
|
+
<span>{{ generating ? 'Generating QR passes...' : 'Checking availability...' }}</span>
|
|
68
66
|
</div>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
<!-- 0 available after all loading -->
|
|
68
|
+
<v-alert
|
|
69
|
+
v-else-if="availableCount === 0"
|
|
70
|
+
type="warning"
|
|
71
|
+
variant="tonal"
|
|
72
|
+
density="compact"
|
|
73
|
+
class="mt-3"
|
|
74
|
+
icon="mdi-information-outline"
|
|
75
|
+
>
|
|
76
|
+
No QR passes available. Add at least 1 non-physical card to this unit in Access Management to enable auto-generation.
|
|
77
|
+
</v-alert>
|
|
78
|
+
<!-- Normal: quantity input -->
|
|
79
|
+
<template v-else>
|
|
80
|
+
<div class="d-flex align-center justify-space-between mt-3">
|
|
81
|
+
<InputLabel title="Quantity" />
|
|
82
|
+
<span v-if="availableCount !== null" class="text-caption text-grey">
|
|
83
|
+
{{ availableCount }} available
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<v-text-field
|
|
87
|
+
v-model.number="quantity"
|
|
88
|
+
type="number"
|
|
89
|
+
density="comfortable"
|
|
90
|
+
:min="1"
|
|
91
|
+
:max="availableCount ?? undefined"
|
|
92
|
+
hide-details
|
|
93
|
+
/>
|
|
94
|
+
</template>
|
|
77
95
|
</template>
|
|
78
96
|
</template>
|
|
79
97
|
|
|
@@ -83,7 +101,7 @@
|
|
|
83
101
|
<InputLabel title="Select Cards" />
|
|
84
102
|
<v-select
|
|
85
103
|
v-model="selectedCards"
|
|
86
|
-
:items="
|
|
104
|
+
:items="filteredCardItems"
|
|
87
105
|
:loading="cardsLoading"
|
|
88
106
|
item-title="cardNo"
|
|
89
107
|
item-value="_id"
|
|
@@ -265,9 +283,13 @@ const props = defineProps({
|
|
|
265
283
|
type: Boolean,
|
|
266
284
|
default: false,
|
|
267
285
|
},
|
|
286
|
+
excludedCardIds: {
|
|
287
|
+
type: Array as PropType<string[]>,
|
|
288
|
+
default: () => [],
|
|
289
|
+
},
|
|
268
290
|
});
|
|
269
291
|
|
|
270
|
-
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards"]);
|
|
292
|
+
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "update:availableQrCount"]);
|
|
271
293
|
|
|
272
294
|
const { getAvailableContractorCards, generateQrVms } = useAccessManagement();
|
|
273
295
|
|
|
@@ -309,12 +331,19 @@ function showSnackbar(text: string, color = "error") {
|
|
|
309
331
|
|
|
310
332
|
// ─── Card fetching ────────────────────────────────────────────────
|
|
311
333
|
const cardItems = ref<any[]>([]);
|
|
334
|
+
const filteredCardItems = computed(() =>
|
|
335
|
+
props.excludedCardIds.length
|
|
336
|
+
? cardItems.value.filter((c) => !props.excludedCardIds.includes(c._id))
|
|
337
|
+
: cardItems.value
|
|
338
|
+
);
|
|
312
339
|
const cardsLoading = ref(false);
|
|
340
|
+
const generating = ref(false);
|
|
313
341
|
const availableCount = ref<number | null>(null);
|
|
314
342
|
|
|
315
343
|
async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
316
344
|
if (!props.siteId || !props.unitId) return;
|
|
317
345
|
cardsLoading.value = true;
|
|
346
|
+
if (type === "QRCODE") emit("update:availableQrCount", null);
|
|
318
347
|
try {
|
|
319
348
|
const res = await getAvailableContractorCards({
|
|
320
349
|
type,
|
|
@@ -324,7 +353,9 @@ async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
|
324
353
|
cardItems.value = res?.data?.[0]?.items ?? [];
|
|
325
354
|
availableCount.value = (res?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
326
355
|
|
|
327
|
-
if (type === "QRCODE" && props.settings?.data?._id && availableCount.value !== null && availableCount.value < 50) {
|
|
356
|
+
if (type === "QRCODE" && props.settings?.data?._id && availableCount.value !== null && availableCount.value > 0 && availableCount.value < 50) {
|
|
357
|
+
cardsLoading.value = false;
|
|
358
|
+
generating.value = true;
|
|
328
359
|
await generateQrVms({
|
|
329
360
|
site: props.settings.data.site,
|
|
330
361
|
unitId: props.unitId,
|
|
@@ -338,14 +369,20 @@ async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
|
338
369
|
});
|
|
339
370
|
cardItems.value = refreshed?.data?.[0]?.items ?? [];
|
|
340
371
|
availableCount.value = (refreshed?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
372
|
+
generating.value = false;
|
|
341
373
|
}
|
|
374
|
+
|
|
375
|
+
if (type === "QRCODE") emit("update:availableQrCount", availableCount.value);
|
|
342
376
|
} catch (err: any) {
|
|
343
377
|
const msg = err?.data?.message ?? err?.message ?? String(err);
|
|
344
378
|
showSnackbar(`Entry pass service error: ${msg}`);
|
|
345
379
|
availableCount.value = null;
|
|
346
380
|
cardItems.value = [];
|
|
381
|
+
generating.value = false;
|
|
382
|
+
if (type === "QRCODE") emit("update:availableQrCount", null);
|
|
347
383
|
} finally {
|
|
348
384
|
cardsLoading.value = false;
|
|
385
|
+
generating.value = false;
|
|
349
386
|
}
|
|
350
387
|
}
|
|
351
388
|
|
|
@@ -500,6 +537,7 @@ watch(
|
|
|
500
537
|
fetchCards("QRCODE");
|
|
501
538
|
} else {
|
|
502
539
|
availableCount.value = null;
|
|
540
|
+
emit("update:availableQrCount", null);
|
|
503
541
|
isBarcodeScanningActive.value = false;
|
|
504
542
|
barcodeBuffer.value = "";
|
|
505
543
|
if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
|
|
@@ -17,18 +17,16 @@
|
|
|
17
17
|
class="mb-3"
|
|
18
18
|
/>
|
|
19
19
|
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/>
|
|
31
|
-
</template>
|
|
20
|
+
<div class="field-label">Base URL / IP address <span>*</span></div>
|
|
21
|
+
<v-text-field
|
|
22
|
+
v-model="draft.baseUrl"
|
|
23
|
+
aria-label="Base URL / IP address"
|
|
24
|
+
placeholder="https://your domain URL"
|
|
25
|
+
variant="outlined"
|
|
26
|
+
density="compact"
|
|
27
|
+
hide-details="auto"
|
|
28
|
+
class="mb-3"
|
|
29
|
+
/>
|
|
32
30
|
|
|
33
31
|
<div class="field-label">Device ID <span>*</span></div>
|
|
34
32
|
<v-text-field
|
|
@@ -175,10 +173,6 @@ function submit() {
|
|
|
175
173
|
status: monitorEnabled.value ? "active" : "inactive",
|
|
176
174
|
};
|
|
177
175
|
|
|
178
|
-
if (props.mode === "edit") {
|
|
179
|
-
delete payload.baseUrl;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
176
|
emit("submit", payload);
|
|
183
177
|
}
|
|
184
178
|
</script>
|
|
@@ -77,6 +77,22 @@
|
|
|
77
77
|
</v-autocomplete>
|
|
78
78
|
</v-col>
|
|
79
79
|
|
|
80
|
+
<v-col v-if="props.unitId" cols="12">
|
|
81
|
+
<EntryPassInformation
|
|
82
|
+
v-model="memberPassType"
|
|
83
|
+
v-model:quantity="memberPassQuantity"
|
|
84
|
+
v-model:cards="memberPassCards"
|
|
85
|
+
:settings="props.settings"
|
|
86
|
+
:loading="props.settingsLoading"
|
|
87
|
+
:site-id="props.site"
|
|
88
|
+
:unit-id="props.unitId"
|
|
89
|
+
:visitor-type="props.type"
|
|
90
|
+
:hid-qr-code-enabled="props.hidQrCodeEnabled"
|
|
91
|
+
:hid-qr-printer-configured="props.hidQrPrinterConfigured"
|
|
92
|
+
:excluded-card-ids="props.selectedNfcCards.map((c) => c._id)"
|
|
93
|
+
/>
|
|
94
|
+
</v-col>
|
|
95
|
+
|
|
80
96
|
<v-col cols="12">
|
|
81
97
|
<InputLabel
|
|
82
98
|
class="text-capitalize"
|
|
@@ -164,6 +180,30 @@ const props = defineProps({
|
|
|
164
180
|
type: Boolean,
|
|
165
181
|
default: false,
|
|
166
182
|
},
|
|
183
|
+
settings: {
|
|
184
|
+
type: Object as PropType<Record<string, any> | null>,
|
|
185
|
+
default: null,
|
|
186
|
+
},
|
|
187
|
+
settingsLoading: {
|
|
188
|
+
type: Boolean,
|
|
189
|
+
default: false,
|
|
190
|
+
},
|
|
191
|
+
unitId: {
|
|
192
|
+
type: String as PropType<string | null>,
|
|
193
|
+
default: null,
|
|
194
|
+
},
|
|
195
|
+
hidQrCodeEnabled: {
|
|
196
|
+
type: Boolean,
|
|
197
|
+
default: false,
|
|
198
|
+
},
|
|
199
|
+
hidQrPrinterConfigured: {
|
|
200
|
+
type: Boolean,
|
|
201
|
+
default: false,
|
|
202
|
+
},
|
|
203
|
+
selectedNfcCards: {
|
|
204
|
+
type: Array as PropType<{ _id: string; cardNo: string }[]>,
|
|
205
|
+
default: () => [],
|
|
206
|
+
},
|
|
167
207
|
});
|
|
168
208
|
|
|
169
209
|
const { requiredRule } = useUtils();
|
|
@@ -201,6 +241,9 @@ const dialog = reactive({
|
|
|
201
241
|
});
|
|
202
242
|
|
|
203
243
|
const selectedPass = ref<string>("");
|
|
244
|
+
const memberPassType = ref<string | null>(null);
|
|
245
|
+
const memberPassQuantity = ref<number | null>(1);
|
|
246
|
+
const memberPassCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
204
247
|
|
|
205
248
|
const members = defineModel<TMemberInfo[]>({ required: true, default: [] });
|
|
206
249
|
const committedMembers = ref<TMemberInfo[]>([]);
|
|
@@ -303,12 +346,27 @@ watch(scannedPassValue, (newVal) => {
|
|
|
303
346
|
showMessage("Scanned pass not available", "error");
|
|
304
347
|
});
|
|
305
348
|
|
|
349
|
+
function resetAccessCard() {
|
|
350
|
+
memberPassType.value = null;
|
|
351
|
+
memberPassQuantity.value = 1;
|
|
352
|
+
memberPassCards.value = [];
|
|
353
|
+
}
|
|
354
|
+
|
|
306
355
|
function handleClearForm() {
|
|
307
356
|
formRef.value?.reset();
|
|
357
|
+
resetAccessCard();
|
|
308
358
|
}
|
|
309
359
|
|
|
310
360
|
async function handleAddMember() {
|
|
311
|
-
committedMembers.value.push({
|
|
361
|
+
committedMembers.value.push({
|
|
362
|
+
...memberForm,
|
|
363
|
+
...(memberPassType.value ? {
|
|
364
|
+
passType: memberPassType.value,
|
|
365
|
+
passQuantity: memberPassQuantity.value,
|
|
366
|
+
passCards: memberPassCards.value,
|
|
367
|
+
} : {}),
|
|
368
|
+
} as any);
|
|
369
|
+
resetAccessCard();
|
|
312
370
|
handleClearForm();
|
|
313
371
|
}
|
|
314
372
|
|
|
@@ -204,12 +204,17 @@
|
|
|
204
204
|
v-model:cards="passCards" :settings="entryPassSettings"
|
|
205
205
|
:loading="entryPassSettingsPending || siteDataPending" :site-id="prop.site"
|
|
206
206
|
:unit-id="visitor.unit || null" :visitor-type="prop.type" :hid-qr-code-enabled="hidQrCodePassAllowed"
|
|
207
|
-
:hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
207
|
+
:hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
208
|
+
@update:available-qr-count="(val) => availableQrCount = val" />
|
|
208
209
|
</v-col>
|
|
209
210
|
|
|
210
211
|
<v-col v-show="withStep3 && contractorStep === 3" cols="12">
|
|
211
212
|
<MemberInformation v-model="visitor.members" :type="prop.type" :contractor-type="visitor.contractorType"
|
|
212
|
-
:site="prop.site" :selected-visitor-pass="visitor.visitorPass" :clearable="true"
|
|
213
|
+
:site="prop.site" :selected-visitor-pass="visitor.visitorPass" :clearable="true"
|
|
214
|
+
:settings="entryPassSettings" :settings-loading="entryPassSettingsPending || siteDataPending"
|
|
215
|
+
:unit-id="visitor.unit || null"
|
|
216
|
+
:hid-qr-code-enabled="hidQrCodePassAllowed" :hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
217
|
+
:selected-nfc-cards="passType === 'NFC' ? passCards : []" />
|
|
213
218
|
</v-col>
|
|
214
219
|
|
|
215
220
|
<v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
|
|
@@ -394,6 +399,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
|
|
|
394
399
|
const passType = ref<string | null>(null);
|
|
395
400
|
const passQuantity = ref<number | null>(1);
|
|
396
401
|
const passCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
402
|
+
const availableQrCount = ref<number | null>(null);
|
|
397
403
|
const hidQrPrintPayload = ref<string | null>(null);
|
|
398
404
|
const hidQrPrinted = ref(false);
|
|
399
405
|
|
|
@@ -628,10 +634,10 @@ function selectNricSearchResult(item: Partial<TPeople>) {
|
|
|
628
634
|
if (!visitor.company) {
|
|
629
635
|
visitor.company = companyNames.value?.[0] || visitor.company;
|
|
630
636
|
}
|
|
631
|
-
visitor.block = (item.block as any) ?? (visitor.block as any) ?? "";
|
|
632
|
-
visitor.level = (item.level as any) ?? (visitor.level as any) ?? "";
|
|
633
|
-
visitor.unit = item.unit ?? visitor.unit ?? "";
|
|
634
|
-
visitor.unitName = item.unitName ?? visitor.unitName;
|
|
637
|
+
// visitor.block = (item.block as any) ?? (visitor.block as any) ?? "";
|
|
638
|
+
// visitor.level = (item.level as any) ?? (visitor.level as any) ?? "";
|
|
639
|
+
// visitor.unit = item.unit ?? visitor.unit ?? "";
|
|
640
|
+
// visitor.unitName = item.unitName ?? visitor.unitName;
|
|
635
641
|
|
|
636
642
|
setTimeout(() => {
|
|
637
643
|
currentAutofillSource.value = null;
|
|
@@ -990,6 +996,16 @@ async function handleNextPage() {
|
|
|
990
996
|
return;
|
|
991
997
|
}
|
|
992
998
|
const step = contractorStep.value;
|
|
999
|
+
if (step === 2 && passType.value === "QR") {
|
|
1000
|
+
if (availableQrCount.value === null) {
|
|
1001
|
+
errorMessage.value = "Please wait while QR passes are being generated...";
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (availableQrCount.value === 0) {
|
|
1005
|
+
errorMessage.value = "No QR passes available. Add a non-physical card to this unit in Access Management first.";
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
993
1009
|
if (step === 2 && passType.value === "NFC" && passCards.value.length === 0) {
|
|
994
1010
|
errorMessage.value = "Please select at least one NFC card before proceeding.";
|
|
995
1011
|
return;
|