@7365admin1/layer-common 4.97.1-staging.586 → 4.97.1-staging.588
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/components/AddPassKeyToVisitor.vue +332 -54
- package/components/FileInput.vue +4 -0
- package/components/MemberInformation.vue +580 -281
- package/components/Nfc/NFCPatrolRouteForm.vue +81 -13
- package/components/Nfc/NFCPatrolRouteMain.vue +17 -5
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +1 -0
- package/components/NricScanDialog.vue +512 -0
- package/components/PassInformation.vue +11 -2
- package/components/VisitorForm.vue +1793 -642
- package/components/VisitorFormSelection.vue +51 -18
- package/components/VisitorManagement.vue +4507 -3518
- package/components/VisitorPassKeyQRScanner.vue +1 -1
- package/components/VisitorSocketPopUp.vue +72 -23
- package/composables/useNFCPatrolReportFilters.ts +55 -15
- package/composables/useNFCPatrolRoute.ts +4 -2
- package/composables/usePeople.ts +54 -0
- package/composables/useVisitor.ts +4 -2
- package/composables/useVisitorSocket.ts +19 -0
- package/package.json +2 -1
- package/types/nfc-patrol-report.ts +4 -2
- package/utils/nfc-patrol-report.ts +47 -10
- package/utils/nfc-patrol-schedule.ts +20 -0
- package/utils/nric-card.ts +278 -0
- package/utils/visitor-people.ts +262 -0
|
@@ -1,41 +1,51 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-card width="100%" class="screen-modal passkey" :loading="processing">
|
|
3
3
|
<div class="passkey__head">
|
|
4
|
-
<v-card-title>{{
|
|
5
|
-
<
|
|
6
|
-
variant="icon"
|
|
7
|
-
icon="mdi-close"
|
|
8
|
-
aria-label="Close"
|
|
9
|
-
@click="emit('close')"
|
|
10
|
-
/>
|
|
4
|
+
<v-card-title>{{ dialogTitle }}</v-card-title>
|
|
5
|
+
<v-btn icon="mdi-close" variant="text" aria-label="Close" @click="emit('close')" />
|
|
11
6
|
</div>
|
|
12
7
|
|
|
13
8
|
<div class="screen-modal__body passkey__body">
|
|
14
9
|
<!-- `.filter-field` is the design's input, and it is a descendant
|
|
15
10
|
selector, so both autocompletes get it from this one class. -->
|
|
16
11
|
<v-row no-gutters class="ga-1 filter-field">
|
|
12
|
+
<!-- Whose passes and keys these are, as the return dialog. -->
|
|
17
13
|
<v-col cols="12">
|
|
18
|
-
<
|
|
14
|
+
<div class="passkey__who">
|
|
15
|
+
<div class="passkey__who-line font-weight-bold">
|
|
16
|
+
<v-icon icon="mdi-account-outline" size="18" />
|
|
17
|
+
{{ prop.visitor.name || "N/A" }}
|
|
18
|
+
</div>
|
|
19
|
+
<div class="passkey__who-line">
|
|
20
|
+
<v-icon icon="mdi-storefront-outline" size="18" />
|
|
21
|
+
{{ visitorLocation }}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
19
24
|
</v-col>
|
|
20
25
|
|
|
21
26
|
<!-- Pass section -->
|
|
22
|
-
<v-col cols="12" class="mt-3">
|
|
23
|
-
<
|
|
27
|
+
<v-col v-if="showPass" cols="12" class="mt-3">
|
|
28
|
+
<p class="passkey__label">Pass</p>
|
|
24
29
|
|
|
25
30
|
<!-- Existing pass -->
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
<template v-if="existingPass">
|
|
32
|
+
<p class="passkey__sublabel">Current</p>
|
|
33
|
+
<div class="d-flex flex-wrap ga-2 mb-3">
|
|
34
|
+
<v-chip variant="outlined" prepend-icon="mdi-card-bulleted-outline"
|
|
35
|
+
class="font-weight-bold passkey-chip passkey-chip--pass">
|
|
36
|
+
{{ existingPass.prefixAndName }}
|
|
37
|
+
</v-chip>
|
|
38
|
+
</div>
|
|
39
|
+
<p class="passkey__sublabel">New pass</p>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<!-- New pass selector, with Scan Pass beside it (as iSecure). -->
|
|
43
|
+
<div class="scan-row">
|
|
35
44
|
<v-autocomplete
|
|
36
45
|
v-model="selectedPass"
|
|
46
|
+
class="scan-row__field"
|
|
37
47
|
v-model:search="passInput"
|
|
38
|
-
|
|
48
|
+
|
|
39
49
|
:hide-no-data="false"
|
|
40
50
|
:items="passItems"
|
|
41
51
|
item-title="prefixAndName"
|
|
@@ -61,49 +71,52 @@
|
|
|
61
71
|
</v-list-item>
|
|
62
72
|
</template>
|
|
63
73
|
</v-autocomplete>
|
|
74
|
+
<v-btn variant="flat" class="text-none screen-btn-primary scan-row__btn" prepend-icon="mdi-qrcode"
|
|
75
|
+
data-test="passkey-scan-pass" :disabled="processing" @click="openScanner('pass')">
|
|
76
|
+
Scan Pass
|
|
77
|
+
</v-btn>
|
|
78
|
+
</div>
|
|
79
|
+
<p v-if="passNotice" :class="['passkey__notice', passNotice.error && 'passkey__notice--error']">
|
|
80
|
+
{{ passNotice.text }}
|
|
81
|
+
</p>
|
|
64
82
|
</v-col>
|
|
65
83
|
|
|
66
84
|
<!-- Keys section -->
|
|
67
85
|
<v-col v-if="showKeys" cols="12" class="mt-3">
|
|
68
|
-
<
|
|
86
|
+
<p class="passkey__label">Keys</p>
|
|
69
87
|
|
|
70
|
-
<!-- Existing keys -->
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
class="d-flex flex-wrap ga-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
>
|
|
84
|
-
<v-icon icon="mdi-key" size="13" />
|
|
85
|
-
{{ key.prefixAndName }}
|
|
86
|
-
<button
|
|
87
|
-
class="passkey__remove"
|
|
88
|
-
:aria-label="`Remove ${key.prefixAndName}`"
|
|
89
|
-
@click="removeExistingKey(key)"
|
|
88
|
+
<!-- Existing keys: ✕ takes one off (saved as "Removed"). -->
|
|
89
|
+
<template v-if="existingKeys.some((k) => k.status != 'Removed')">
|
|
90
|
+
<p class="passkey__sublabel">Current</p>
|
|
91
|
+
<div class="d-flex flex-wrap ga-2 mb-3">
|
|
92
|
+
<v-chip
|
|
93
|
+
v-for="key in existingKeys.filter((k) => k.status != 'Removed')"
|
|
94
|
+
:key="key.keyId"
|
|
95
|
+
variant="outlined"
|
|
96
|
+
prepend-icon="mdi-key"
|
|
97
|
+
closable
|
|
98
|
+
:close-label="`Remove ${key.prefixAndName}`"
|
|
99
|
+
class="font-weight-bold passkey-chip passkey-chip--key"
|
|
100
|
+
@click:close="removeExistingKey(key)"
|
|
90
101
|
>
|
|
91
|
-
|
|
92
|
-
</
|
|
93
|
-
</
|
|
94
|
-
<
|
|
95
|
-
</
|
|
96
|
-
|
|
97
|
-
<!-- New keys selector -->
|
|
102
|
+
{{ key.prefixAndName }}
|
|
103
|
+
</v-chip>
|
|
104
|
+
</div>
|
|
105
|
+
<p class="passkey__sublabel">Add more keys</p>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<!-- New keys selector, with Scan Key beside it (as iSecure). -->
|
|
109
|
+
<div class="scan-row">
|
|
98
110
|
<v-autocomplete
|
|
99
111
|
v-model="selectedKeys"
|
|
112
|
+
class="scan-row__field"
|
|
100
113
|
v-model:search="keyInput"
|
|
101
114
|
:hide-no-data="false"
|
|
102
115
|
:items="keyItems"
|
|
103
116
|
item-title="prefixAndName"
|
|
104
117
|
item-value="_id"
|
|
105
118
|
:item-props="keyItemProps"
|
|
106
|
-
|
|
119
|
+
|
|
107
120
|
multiple
|
|
108
121
|
variant="outlined"
|
|
109
122
|
hide-details
|
|
@@ -127,6 +140,14 @@
|
|
|
127
140
|
</v-list-item>
|
|
128
141
|
</template>
|
|
129
142
|
</v-autocomplete>
|
|
143
|
+
<v-btn variant="flat" class="text-none screen-btn-primary scan-row__btn" prepend-icon="mdi-qrcode"
|
|
144
|
+
data-test="passkey-scan-key" :disabled="processing" @click="openScanner('key')">
|
|
145
|
+
Scan Key
|
|
146
|
+
</v-btn>
|
|
147
|
+
</div>
|
|
148
|
+
<p v-if="keyNotice" :class="['passkey__notice', keyNotice.error && 'passkey__notice--error']">
|
|
149
|
+
{{ keyNotice.text }}
|
|
150
|
+
</p>
|
|
130
151
|
</v-col>
|
|
131
152
|
</v-row>
|
|
132
153
|
|
|
@@ -135,10 +156,13 @@
|
|
|
135
156
|
</v-row>
|
|
136
157
|
</div>
|
|
137
158
|
|
|
159
|
+
<VisitorPassKeyQRScanner v-if="scanner.open" :dialog="scanner.open" v-model:scanned-value="scanner.value"
|
|
160
|
+
@close-dialog="scanner.open = false" />
|
|
161
|
+
|
|
138
162
|
<div class="passkey__footer">
|
|
139
163
|
<AppButton variant="ghost" @click="emit('close')">Cancel</AppButton>
|
|
140
164
|
<AppButton :disabled="!canSubmit" @click="handleSubmit">
|
|
141
|
-
{{
|
|
165
|
+
{{ submitLabel }}
|
|
142
166
|
</AppButton>
|
|
143
167
|
</div>
|
|
144
168
|
</v-card>
|
|
@@ -149,6 +173,7 @@ import type { PropType } from "vue";
|
|
|
149
173
|
import usePassKey from "../composables/usePassKey";
|
|
150
174
|
import useVisitor from "../composables/useVisitor";
|
|
151
175
|
import useKeyPresses from "../composables/useKeyPresses";
|
|
176
|
+
import VisitorPassKeyQRScanner from "./VisitorPassKeyQRScanner.vue";
|
|
152
177
|
|
|
153
178
|
const prop = defineProps({
|
|
154
179
|
visitor: {
|
|
@@ -159,8 +184,13 @@ const prop = defineProps({
|
|
|
159
184
|
type: String,
|
|
160
185
|
required: true,
|
|
161
186
|
},
|
|
187
|
+
/**
|
|
188
|
+
* iSecure's pass & key dialogs: "add" is Assign (Assign Pass & Keys for a
|
|
189
|
+
* contractor, Add Pass otherwise), "add-keys" is Add Key(s) (contractor),
|
|
190
|
+
* "edit-pass" is Change Pass, "edit" the combined edit.
|
|
191
|
+
*/
|
|
162
192
|
mode: {
|
|
163
|
-
type: String as PropType<"add" | "edit">,
|
|
193
|
+
type: String as PropType<"add" | "edit" | "add-keys" | "edit-pass">,
|
|
164
194
|
default: "add",
|
|
165
195
|
},
|
|
166
196
|
});
|
|
@@ -172,6 +202,17 @@ const emit = defineEmits<{
|
|
|
172
202
|
|
|
173
203
|
const { getPassKeysByPageSearch } = usePassKey();
|
|
174
204
|
const { updateVisitor } = useVisitor();
|
|
205
|
+
const { currentUser } = useLocalAuth();
|
|
206
|
+
|
|
207
|
+
const { formatLocation } = useSecurityUtils();
|
|
208
|
+
const visitorLocation = computed(
|
|
209
|
+
() =>
|
|
210
|
+
formatLocation({
|
|
211
|
+
block: (prop.visitor as any).block,
|
|
212
|
+
level: (prop.visitor as any).level,
|
|
213
|
+
unit: (prop.visitor as any).unit,
|
|
214
|
+
}) || "N/A"
|
|
215
|
+
);
|
|
175
216
|
|
|
176
217
|
const processing = ref(false);
|
|
177
218
|
const errorMessage = ref("");
|
|
@@ -195,8 +236,40 @@ const existingKeys = ref<
|
|
|
195
236
|
{ keyId: string; prefixAndName: string; status?: string }[]
|
|
196
237
|
>([]);
|
|
197
238
|
|
|
198
|
-
|
|
199
|
-
const
|
|
239
|
+
// Every mode but Assign starts from what the visitor holds now.
|
|
240
|
+
const isEditMode = computed(() => prop.mode !== "add");
|
|
241
|
+
const isContractor = computed(() => prop.visitor.type === "contractor");
|
|
242
|
+
const showPass = computed(() => prop.mode !== "add-keys");
|
|
243
|
+
const showKeys = computed(() => isContractor.value && prop.mode !== "edit-pass");
|
|
244
|
+
|
|
245
|
+
const dialogTitle = computed(() => {
|
|
246
|
+
switch (prop.mode) {
|
|
247
|
+
case "add-keys":
|
|
248
|
+
// Once the visitor holds keys the dialog adds AND removes them.
|
|
249
|
+
return prop.visitor.passKeys?.some((key: any) => key?.status != "Removed")
|
|
250
|
+
? "Add/Remove Key(s)"
|
|
251
|
+
: "Add Key(s)";
|
|
252
|
+
case "edit-pass":
|
|
253
|
+
return "Change Pass";
|
|
254
|
+
case "edit":
|
|
255
|
+
return "Edit Pass & Keys";
|
|
256
|
+
default:
|
|
257
|
+
return isContractor.value ? "Assign Pass & Keys" : "Add Pass";
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const submitLabel = computed(() => {
|
|
262
|
+
switch (prop.mode) {
|
|
263
|
+
case "add-keys":
|
|
264
|
+
return "Add";
|
|
265
|
+
case "edit-pass":
|
|
266
|
+
return "Update";
|
|
267
|
+
case "edit":
|
|
268
|
+
return "Save";
|
|
269
|
+
default:
|
|
270
|
+
return isContractor.value ? "Save" : "Add";
|
|
271
|
+
}
|
|
272
|
+
});
|
|
200
273
|
// The key's press (if the site has any) under each key in the picker. Read
|
|
201
274
|
// only when the key picker is shown.
|
|
202
275
|
const { keyItemProps } = useKeyPresses(() => (showKeys.value ? prop.site : ""));
|
|
@@ -364,8 +437,136 @@ watch(
|
|
|
364
437
|
{ deep: true, immediate: true }
|
|
365
438
|
);
|
|
366
439
|
|
|
440
|
+
/*
|
|
441
|
+
* Scan Pass / Scan Key (iSecure `onPassScanned` / `onScanned`): the QR holds
|
|
442
|
+
* the pass or key name (`prefixAndName`, e.g. VP01). It is looked up on the
|
|
443
|
+
* server across every status, so a pass or key that is not Available is
|
|
444
|
+
* reported with its status instead of "not found".
|
|
445
|
+
*/
|
|
446
|
+
const PASS_TYPE_LABELS: Record<string, string> = {
|
|
447
|
+
"visitor-pass": "Visitor Pass",
|
|
448
|
+
"contractor-pass": "Contractor Pass",
|
|
449
|
+
"agent-pass": "Agent Pass",
|
|
450
|
+
"pass-key": "Key",
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
type TNotice = { text: string; error: boolean } | null;
|
|
454
|
+
const passNotice = ref<TNotice>(null);
|
|
455
|
+
const keyNotice = ref<TNotice>(null);
|
|
456
|
+
const scanner = reactive<{ open: boolean; value: string; target: "pass" | "key" }>({
|
|
457
|
+
open: false,
|
|
458
|
+
value: "",
|
|
459
|
+
target: "pass",
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
function openScanner(target: "pass" | "key") {
|
|
463
|
+
if (target === "pass") passNotice.value = null;
|
|
464
|
+
else keyNotice.value = null;
|
|
465
|
+
scanner.target = target;
|
|
466
|
+
scanner.value = "";
|
|
467
|
+
scanner.open = true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
type TScanned = { _id: string; prefixAndName?: string; status?: string; passType?: string };
|
|
471
|
+
|
|
472
|
+
async function findScanned(code: string): Promise<TScanned[]> {
|
|
473
|
+
const same = (item: TScanned) =>
|
|
474
|
+
(item.prefixAndName ?? "").trim().toLowerCase() === code.toLowerCase();
|
|
475
|
+
try {
|
|
476
|
+
const res: any = await getPassKeysByPageSearch({
|
|
477
|
+
search: code,
|
|
478
|
+
page: 1,
|
|
479
|
+
limit: 50,
|
|
480
|
+
sites: [prop.site],
|
|
481
|
+
});
|
|
482
|
+
const items: TScanned[] = res?.items ?? res?.data?.items ?? [];
|
|
483
|
+
return items.filter(same);
|
|
484
|
+
} catch {
|
|
485
|
+
// Offline / failed: the Available lists already loaded.
|
|
486
|
+
return [
|
|
487
|
+
...((passesData.value as any)?.items ?? []),
|
|
488
|
+
...((keysData.value as any)?.items ?? []),
|
|
489
|
+
].filter(same);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function onPassScanned(code: string) {
|
|
494
|
+
const fail = (text: string) => (passNotice.value = { text, error: true });
|
|
495
|
+
const found = await findScanned(code);
|
|
496
|
+
if (!found.length) return fail(`Pass or Key ${code} not found.`);
|
|
497
|
+
const item = found[0];
|
|
498
|
+
const isKey = item.passType === "pass-key";
|
|
499
|
+
const label = PASS_TYPE_LABELS[item.passType ?? ""] ?? (isKey ? "Key" : "Pass");
|
|
500
|
+
const name = item.prefixAndName ?? code;
|
|
501
|
+
if (found.length > 1) return fail(`${label} ${name} found duplicates.`);
|
|
502
|
+
if (isKey) return fail(`${label} ${name} is a key, not a pass.`);
|
|
503
|
+
if (item.status && item.status !== "Available") return fail(`${label} ${name} is ${item.status}.`);
|
|
504
|
+
if (existingPassId.value === item._id)
|
|
505
|
+
return fail(`${label} ${name} is already selected to this visitor.`);
|
|
506
|
+
if (!passTypesComputed.value.includes(item.passType ?? "")) {
|
|
507
|
+
const allowed = passTypesComputed.value
|
|
508
|
+
.map((t) => PASS_TYPE_LABELS[t] ?? t)
|
|
509
|
+
.join(" / ");
|
|
510
|
+
return fail(`${label} ${name} cannot be used for this visitor type (${allowed}).`);
|
|
511
|
+
}
|
|
512
|
+
// Shown in the picker even if the loaded list did not have it yet.
|
|
513
|
+
if (!passItems.value.some((p) => p._id === item._id)) {
|
|
514
|
+
passItems.value = [...passItems.value, item as TPassKey];
|
|
515
|
+
}
|
|
516
|
+
selectedPass.value = item._id;
|
|
517
|
+
passNotice.value = { text: `${label} ${name} selected.`, error: false };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function onKeyScanned(code: string) {
|
|
521
|
+
const fail = (text: string) => (keyNotice.value = { text, error: true });
|
|
522
|
+
const found = await findScanned(code);
|
|
523
|
+
if (!found.length) return fail(`Pass or Key ${code} not found.`);
|
|
524
|
+
const item = found[0];
|
|
525
|
+
const isKey = item.passType === "pass-key";
|
|
526
|
+
const label = PASS_TYPE_LABELS[item.passType ?? ""] ?? (isKey ? "Key" : "Pass");
|
|
527
|
+
const name = item.prefixAndName ?? code;
|
|
528
|
+
if (found.length > 1) return fail(`${label} ${name} found duplicates.`);
|
|
529
|
+
if (!isKey) return fail(`You are scanning a key, but ${label} ${name} is a pass, not a key.`);
|
|
530
|
+
// A current key marked for removal: scanning it just undoes that.
|
|
531
|
+
if (existingKeys.value.some((k) => k.keyId === item._id && k.status === "Removed")) {
|
|
532
|
+
selectedKeys.value = [...selectedKeys.value, item._id];
|
|
533
|
+
selectKeys();
|
|
534
|
+
keyNotice.value = { text: `${label} ${name} restored.`, error: false };
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (
|
|
538
|
+
selectedKeys.value.includes(item._id) ||
|
|
539
|
+
existingKeys.value.some((k) => k.keyId === item._id && k.status !== "Removed")
|
|
540
|
+
)
|
|
541
|
+
return fail(`${label} ${name} is already selected to this visitor.`);
|
|
542
|
+
if (item.status && item.status !== "Available") return fail(`${label} ${name} is ${item.status}.`);
|
|
543
|
+
if (!keyItems.value.some((k) => k._id === item._id)) {
|
|
544
|
+
keyItems.value = [...keyItems.value, item as TPassKey];
|
|
545
|
+
}
|
|
546
|
+
selectedKeys.value = [...selectedKeys.value, item._id];
|
|
547
|
+
keyNotice.value = { text: `${label} ${name} selected.`, error: false };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
watch(
|
|
551
|
+
() => scanner.value,
|
|
552
|
+
(value) => {
|
|
553
|
+
const code = String(value ?? "").trim();
|
|
554
|
+
if (!code) return;
|
|
555
|
+
scanner.value = "";
|
|
556
|
+
if (scanner.target === "pass") void onPassScanned(code);
|
|
557
|
+
else void onKeyScanned(code);
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Picking by hand clears the last scan message.
|
|
562
|
+
watch(selectedPass, () => {
|
|
563
|
+
if (passNotice.value?.error) passNotice.value = null;
|
|
564
|
+
});
|
|
565
|
+
|
|
367
566
|
const canSubmit = computed(() => {
|
|
368
567
|
const hasRemovedKeys = existingKeys.value.some((k) => k.status === "Removed");
|
|
568
|
+
if (prop.mode === "edit-pass") return !!selectedPass.value;
|
|
569
|
+
if (prop.mode === "add-keys") return selectedKeys.value.length > 0 || hasRemovedKeys;
|
|
369
570
|
return (
|
|
370
571
|
!!selectedPass.value || selectedKeys.value.length > 0 || hasRemovedKeys
|
|
371
572
|
);
|
|
@@ -394,7 +595,8 @@ async function handleSubmit() {
|
|
|
394
595
|
add: true,
|
|
395
596
|
});
|
|
396
597
|
}
|
|
397
|
-
|
|
598
|
+
// Add Key(s) changes the keys only (iSecure `addKeysPayload`).
|
|
599
|
+
if (showPass.value) payload.visitorPass = finalPasses;
|
|
398
600
|
|
|
399
601
|
// Build final keys list: kept existing + newly selected
|
|
400
602
|
if (showKeys.value) {
|
|
@@ -413,6 +615,7 @@ async function handleSubmit() {
|
|
|
413
615
|
payload.passKeys = finalKeys;
|
|
414
616
|
}
|
|
415
617
|
|
|
618
|
+
payload.updatedBy = currentUser.value?._id;
|
|
416
619
|
await updateVisitor(prop.visitor._id, payload);
|
|
417
620
|
emit("done");
|
|
418
621
|
emit("close");
|
|
@@ -468,6 +671,81 @@ async function handleSubmit() {
|
|
|
468
671
|
opacity: 1;
|
|
469
672
|
}
|
|
470
673
|
|
|
674
|
+
/* A Scan button beside its input, the same height, as on the visitor form. */
|
|
675
|
+
.scan-row {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: flex-start;
|
|
678
|
+
gap: 8px;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.scan-row__field {
|
|
682
|
+
flex: 1 1 auto;
|
|
683
|
+
min-width: 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.scan-row__btn {
|
|
687
|
+
flex: 0 0 auto;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.passkey__notice {
|
|
691
|
+
margin: 6px 0 0;
|
|
692
|
+
font-size: var(--fs-cell);
|
|
693
|
+
font-weight: 600;
|
|
694
|
+
color: rgb(var(--v-theme-success));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.passkey__notice--error {
|
|
698
|
+
color: var(--err);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.passkey__who {
|
|
702
|
+
display: flex;
|
|
703
|
+
flex-direction: column;
|
|
704
|
+
gap: 4px;
|
|
705
|
+
padding: 10px 12px;
|
|
706
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
707
|
+
border-radius: 8px;
|
|
708
|
+
font-size: 0.9375rem;
|
|
709
|
+
color: rgb(var(--v-theme-on-surface));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.passkey__who-line {
|
|
713
|
+
display: flex;
|
|
714
|
+
align-items: center;
|
|
715
|
+
gap: 8px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.passkey__label {
|
|
719
|
+
margin: 6px 0 6px;
|
|
720
|
+
font-size: 1rem;
|
|
721
|
+
font-weight: 600;
|
|
722
|
+
color: rgb(var(--v-theme-on-surface));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.passkey__sublabel {
|
|
726
|
+
margin: 0 0 6px;
|
|
727
|
+
font-size: var(--fs-cell);
|
|
728
|
+
color: var(--text2);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* The same pass / key chips as the visitor list and the return dialog. */
|
|
732
|
+
.passkey-chip.v-chip {
|
|
733
|
+
--v-chip-height: 32px;
|
|
734
|
+
font-size: 0.875rem;
|
|
735
|
+
padding: 0 12px;
|
|
736
|
+
background: rgb(var(--v-theme-surface));
|
|
737
|
+
color: rgb(var(--v-theme-on-surface));
|
|
738
|
+
border: 1.5px solid rgba(var(--v-theme-on-surface), 0.87);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.passkey-chip--pass :deep(.v-chip__prepend .v-icon) {
|
|
742
|
+
color: #1565c0;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.passkey-chip--key :deep(.v-chip__prepend .v-icon) {
|
|
746
|
+
color: #e65100;
|
|
747
|
+
}
|
|
748
|
+
|
|
471
749
|
.passkey__footer {
|
|
472
750
|
display: flex;
|
|
473
751
|
justify-content: flex-end;
|
package/components/FileInput.vue
CHANGED
|
@@ -288,6 +288,10 @@ const removeFile = (index) => {
|
|
|
288
288
|
onUnmounted(() => {
|
|
289
289
|
stopCamera();
|
|
290
290
|
});
|
|
291
|
+
|
|
292
|
+
// Lets a screen open the camera from its own button (the visitor form's
|
|
293
|
+
// "Camera") while the picker itself stays hidden.
|
|
294
|
+
defineExpose({ openCameraDialog });
|
|
291
295
|
</script>
|
|
292
296
|
|
|
293
297
|
<style scoped>
|