@7365admin1/layer-common 4.90.2-staging.491 → 4.90.3
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 +16 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InventoryItemDetail.vue +1 -2
- package/components/InventoryLinesDialog.vue +13 -91
- package/components/InventoryPhotoInput.vue +6 -44
- package/components/InventoryReportsTab.vue +3 -23
- package/components/InventoryRequestsTab.vue +4 -27
- package/components/InventoryStockTab.vue +0 -3
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useInventory.ts +1 -4
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/inventory.d.ts +0 -27
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/InventoryPoDialog.vue +0 -111
- package/components/VehicleQrStickerDialog.vue +0 -249
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/inventory-receiving.ts +0 -90
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- package/utils/vehicle-qr-sticker.ts +0 -184
|
@@ -87,8 +87,6 @@
|
|
|
87
87
|
|
|
88
88
|
<v-list density="compact" min-width="180">
|
|
89
89
|
<v-list-item title="View" @click="openView(item)" />
|
|
90
|
-
<v-list-item title="Access Permissions" @click="openPermissions(item)" />
|
|
91
|
-
<v-list-item title="Identification Methods" @click="openIdentification(item)" />
|
|
92
90
|
<v-list-item title="Sync HID Reader" @click="openSyncConfirm(item)" />
|
|
93
91
|
<v-list-item title="Test Connection" @click="openTestConfirm(item)" />
|
|
94
92
|
<v-list-item title="Edit" @click="openEdit(item)" />
|
|
@@ -146,125 +144,6 @@
|
|
|
146
144
|
</v-card>
|
|
147
145
|
</v-dialog>
|
|
148
146
|
|
|
149
|
-
<!--
|
|
150
|
-
IDENTIFICATION METHODS - what this reader accepts at the door.
|
|
151
|
-
|
|
152
|
-
Per reader, not per site: the device's own `identifier` module is the
|
|
153
|
-
authority, and a site-wide copy could only ever describe one of them.
|
|
154
|
-
|
|
155
|
-
There is deliberately no Bluetooth control. The HID API has no Bluetooth
|
|
156
|
-
identification field; the old site-level toggle of that name referred to
|
|
157
|
-
the phone-as-a-card feature in the mobile app, which is a different thing.
|
|
158
|
-
-->
|
|
159
|
-
<v-dialog v-model="identificationDialog" max-width="430" persistent>
|
|
160
|
-
<v-card rounded="lg">
|
|
161
|
-
<v-card-title class="screen-card-title">Identification Methods</v-card-title>
|
|
162
|
-
|
|
163
|
-
<v-card-text v-if="identificationStage === 'edit'">
|
|
164
|
-
<p class="identification-lead">
|
|
165
|
-
Credentials {{ selectedReader?.name || "this HID Reader" }} will accept at the door.
|
|
166
|
-
</p>
|
|
167
|
-
|
|
168
|
-
<div class="identification-grid">
|
|
169
|
-
<div
|
|
170
|
-
v-for="method in identificationMethodRows"
|
|
171
|
-
:key="method.key"
|
|
172
|
-
class="identification-row"
|
|
173
|
-
>
|
|
174
|
-
<div class="identification-copy">
|
|
175
|
-
<span>{{ method.label }}</span>
|
|
176
|
-
<small v-if="!method.supported">Not supported by this reader</small>
|
|
177
|
-
</div>
|
|
178
|
-
<v-switch
|
|
179
|
-
v-model="identificationForm[method.key]"
|
|
180
|
-
class="app-switch"
|
|
181
|
-
hide-details
|
|
182
|
-
density="compact"
|
|
183
|
-
:disabled="!method.supported || identificationSaving"
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
<!--
|
|
189
|
-
One reader setting with two modes, not two switches:
|
|
190
|
-
`pin_identification_enabled` is 1 for PIN and 0 for ID-and-password.
|
|
191
|
-
A reader without the PIN capability can be given neither.
|
|
192
|
-
-->
|
|
193
|
-
<div class="identification-credential">
|
|
194
|
-
<div class="identification-copy">
|
|
195
|
-
<span>Credential entry</span>
|
|
196
|
-
<small v-if="!credentialModeSupported">Not supported by this reader</small>
|
|
197
|
-
</div>
|
|
198
|
-
<v-radio-group
|
|
199
|
-
v-model="identificationForm.credentialMode"
|
|
200
|
-
hide-details
|
|
201
|
-
density="compact"
|
|
202
|
-
:disabled="!credentialModeSupported || identificationSaving"
|
|
203
|
-
>
|
|
204
|
-
<v-radio label="PIN" value="pin" />
|
|
205
|
-
<v-radio label="ID and password" value="idPassword" />
|
|
206
|
-
</v-radio-group>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<p class="identification-applied">
|
|
210
|
-
Confirmed on the reader:
|
|
211
|
-
<strong>{{ formatDate(selectedReader?.identificationMethodsAppliedAt) }}</strong>
|
|
212
|
-
</p>
|
|
213
|
-
|
|
214
|
-
<!--
|
|
215
|
-
Its own action, not a save. A reader's settings can change without
|
|
216
|
-
us — a firmware update, its own touchscreen, a restore, a reset —
|
|
217
|
-
and saving only writes what is on screen. This reads the device
|
|
218
|
-
first and reports what had moved.
|
|
219
|
-
-->
|
|
220
|
-
<div class="dialog-actions-inline">
|
|
221
|
-
<AppButton
|
|
222
|
-
variant="ghost"
|
|
223
|
-
icon="mdi-sync"
|
|
224
|
-
:disabled="identificationReconciling || identificationSaving || !selectedReader?.identificationMethods"
|
|
225
|
-
@click="reapplyIdentificationMethods"
|
|
226
|
-
>
|
|
227
|
-
{{ identificationReconciling ? "Checking reader..." : "Re-apply settings" }}
|
|
228
|
-
</AppButton>
|
|
229
|
-
</div>
|
|
230
|
-
</v-card-text>
|
|
231
|
-
|
|
232
|
-
<v-card-text v-else class="reader-action-content">
|
|
233
|
-
<v-icon icon="mdi-shield-key-outline" class="dialog-mark" size="48" />
|
|
234
|
-
<h3>Change what opens this door?</h3>
|
|
235
|
-
<p>
|
|
236
|
-
You are about to change the credentials {{ selectedReader?.name || "this HID Reader" }}
|
|
237
|
-
accepts.
|
|
238
|
-
</p>
|
|
239
|
-
<p v-if="identificationBeingDisabled.length" class="identification-warning">
|
|
240
|
-
Turning off: <strong>{{ identificationBeingDisabled.join(", ") }}</strong>.
|
|
241
|
-
Anyone relying on that credential will stop getting in at this reader.
|
|
242
|
-
</p>
|
|
243
|
-
</v-card-text>
|
|
244
|
-
|
|
245
|
-
<v-card-actions>
|
|
246
|
-
<v-spacer />
|
|
247
|
-
<AppButton
|
|
248
|
-
variant="ghost"
|
|
249
|
-
:disabled="identificationSaving || identificationReconciling"
|
|
250
|
-
@click="identificationStage === 'confirm' ? (identificationStage = 'edit') : (identificationDialog = false)"
|
|
251
|
-
>
|
|
252
|
-
{{ identificationStage === "confirm" ? "Back" : "Cancel" }}
|
|
253
|
-
</AppButton>
|
|
254
|
-
<AppButton
|
|
255
|
-
v-if="identificationStage === 'edit'"
|
|
256
|
-
:disabled="!identificationChanged || identificationReconciling"
|
|
257
|
-
@click="identificationStage = 'confirm'"
|
|
258
|
-
>
|
|
259
|
-
Save
|
|
260
|
-
</AppButton>
|
|
261
|
-
<AppButton v-else :disabled="identificationSaving" @click="saveIdentificationMethods">
|
|
262
|
-
{{ identificationSaving ? "Applying..." : "Apply to reader" }}
|
|
263
|
-
</AppButton>
|
|
264
|
-
</v-card-actions>
|
|
265
|
-
</v-card>
|
|
266
|
-
</v-dialog>
|
|
267
|
-
|
|
268
147
|
<v-dialog v-model="confirmDialog" max-width="390" persistent>
|
|
269
148
|
<v-card rounded="lg" class="reader-action-dialog">
|
|
270
149
|
<v-card-title class="screen-card-title">{{ confirmTitle }}</v-card-title>
|
|
@@ -362,18 +241,6 @@
|
|
|
362
241
|
</v-card-actions>
|
|
363
242
|
</v-card>
|
|
364
243
|
</v-dialog>
|
|
365
|
-
|
|
366
|
-
<!-- Who may actually pass this reader. Enrolment makes a person
|
|
367
|
-
RECOGNISABLE; this is what puts them in the group the access rule is
|
|
368
|
-
attached to, and without it a recognised person is refused at the
|
|
369
|
-
door. Reloads the list on save so the reader's own counts stay true. -->
|
|
370
|
-
<HidAccessPermissions
|
|
371
|
-
v-model="permissionsDialog"
|
|
372
|
-
:site="props.site"
|
|
373
|
-
:org="props.org"
|
|
374
|
-
:reader="selectedReader"
|
|
375
|
-
@saved="loadReaders"
|
|
376
|
-
/>
|
|
377
244
|
</section>
|
|
378
245
|
</template>
|
|
379
246
|
|
|
@@ -417,21 +284,6 @@ type HidRecord = {
|
|
|
417
284
|
cardReadingMode?: "unconfigured" | "pacs" | "csn" | "csn_for_mifare";
|
|
418
285
|
csnByteOrder?: "msb" | "lsb";
|
|
419
286
|
pacsFormat?: "raw" | "wiegand";
|
|
420
|
-
capabilities?: {
|
|
421
|
-
card?: boolean;
|
|
422
|
-
facial?: boolean;
|
|
423
|
-
qr?: boolean;
|
|
424
|
-
pin?: boolean;
|
|
425
|
-
intercom?: boolean;
|
|
426
|
-
};
|
|
427
|
-
identificationMethods?: {
|
|
428
|
-
facial?: boolean;
|
|
429
|
-
card?: boolean;
|
|
430
|
-
qrCode?: boolean;
|
|
431
|
-
credentialMode?: "pin" | "idPassword";
|
|
432
|
-
};
|
|
433
|
-
/** When the READER confirmed the methods, not when they were saved here. */
|
|
434
|
-
identificationMethodsAppliedAt?: string;
|
|
435
287
|
};
|
|
436
288
|
|
|
437
289
|
type HidReaderFormPayload = {
|
|
@@ -470,13 +322,6 @@ const props = defineProps({
|
|
|
470
322
|
type: String,
|
|
471
323
|
required: true,
|
|
472
324
|
},
|
|
473
|
-
// The permission endpoints are org-scoped (`assertOrganizationSiteAccess`),
|
|
474
|
-
// and this component previously had no reason to know the org. Passed from
|
|
475
|
-
// the page, as `HidQrCodeConfiguration` already does.
|
|
476
|
-
org: {
|
|
477
|
-
type: String,
|
|
478
|
-
required: true,
|
|
479
|
-
},
|
|
480
325
|
});
|
|
481
326
|
|
|
482
327
|
const {
|
|
@@ -488,8 +333,6 @@ const {
|
|
|
488
333
|
configureReaderIntegration,
|
|
489
334
|
setReaderMonitor,
|
|
490
335
|
setReaderOperatingMode,
|
|
491
|
-
setReaderIdentificationMethods,
|
|
492
|
-
reconcileReaderIdentificationMethods,
|
|
493
336
|
getReaderUsers,
|
|
494
337
|
} = useHidAmico();
|
|
495
338
|
|
|
@@ -504,20 +347,6 @@ const viewDialog = ref(false);
|
|
|
504
347
|
const confirmDialog = ref(false);
|
|
505
348
|
const resultDialog = ref(false);
|
|
506
349
|
const selectedReader = ref<HidRecord | null>(null);
|
|
507
|
-
const permissionsDialog = ref(false);
|
|
508
|
-
const identificationDialog = ref(false);
|
|
509
|
-
/* Two stages in one dialog, like the sync flow: the controls, then the
|
|
510
|
-
confirmation. Switching a credential off decides what stops opening a door,
|
|
511
|
-
so it is never one click away. */
|
|
512
|
-
const identificationStage = ref<"edit" | "confirm">("edit");
|
|
513
|
-
const identificationSaving = ref(false);
|
|
514
|
-
const identificationReconciling = ref(false);
|
|
515
|
-
const identificationForm = ref<{
|
|
516
|
-
facial: boolean;
|
|
517
|
-
card: boolean;
|
|
518
|
-
qrCode: boolean;
|
|
519
|
-
credentialMode: "pin" | "idPassword";
|
|
520
|
-
}>({ facial: false, card: false, qrCode: false, credentialMode: "idPassword" });
|
|
521
350
|
const pendingAction = ref<"test" | "sync" | "delete" | null>(null);
|
|
522
351
|
const actionStage = ref<"confirm" | "review" | "loading">("confirm");
|
|
523
352
|
const resultSuccess = ref(true);
|
|
@@ -655,182 +484,6 @@ function openView(reader: HidRecord) {
|
|
|
655
484
|
viewDialog.value = true;
|
|
656
485
|
}
|
|
657
486
|
|
|
658
|
-
function openPermissions(reader: HidRecord) {
|
|
659
|
-
selectedReader.value = reader;
|
|
660
|
-
permissionsDialog.value = true;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/*
|
|
664
|
-
* The reader record is the source of truth, including for what is already on.
|
|
665
|
-
* A missing `identificationMethods` means nothing has been applied through this
|
|
666
|
-
* screen yet; the switches start off rather than guessing at the device.
|
|
667
|
-
*/
|
|
668
|
-
function openIdentification(reader: HidRecord) {
|
|
669
|
-
selectedReader.value = reader;
|
|
670
|
-
const current = reader.identificationMethods || {};
|
|
671
|
-
identificationForm.value = {
|
|
672
|
-
facial: current.facial === true,
|
|
673
|
-
card: current.card === true,
|
|
674
|
-
qrCode: current.qrCode === true,
|
|
675
|
-
credentialMode: current.credentialMode === "pin" ? "pin" : "idPassword",
|
|
676
|
-
};
|
|
677
|
-
identificationStage.value = "edit";
|
|
678
|
-
identificationDialog.value = true;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/*
|
|
682
|
-
* A reader saved before capabilities were recorded has none. Those are treated
|
|
683
|
-
* as capable rather than having every control disabled on an older record —
|
|
684
|
-
* the backend gate makes the same allowance, and it is the one that decides.
|
|
685
|
-
*/
|
|
686
|
-
function readerSupports(key: "facial" | "card" | "qr" | "pin") {
|
|
687
|
-
const capabilities = selectedReader.value?.capabilities;
|
|
688
|
-
if (!capabilities) return true;
|
|
689
|
-
return capabilities[key] === true;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const identificationMethodRows = computed(() => [
|
|
693
|
-
{ key: "facial" as const, label: "Facial", supported: readerSupports("facial") },
|
|
694
|
-
{ key: "card" as const, label: "Card", supported: readerSupports("card") },
|
|
695
|
-
{ key: "qrCode" as const, label: "QR code", supported: readerSupports("qr") },
|
|
696
|
-
]);
|
|
697
|
-
|
|
698
|
-
/* Both modes ride the reader's single PIN switch, so the pair stands or falls
|
|
699
|
-
together on that one capability. */
|
|
700
|
-
const credentialModeSupported = computed(() => readerSupports("pin"));
|
|
701
|
-
|
|
702
|
-
const identificationChanged = computed(() => {
|
|
703
|
-
const current = selectedReader.value?.identificationMethods || {};
|
|
704
|
-
const form = identificationForm.value;
|
|
705
|
-
return (
|
|
706
|
-
form.facial !== (current.facial === true)
|
|
707
|
-
|| form.card !== (current.card === true)
|
|
708
|
-
|| form.qrCode !== (current.qrCode === true)
|
|
709
|
-
|| form.credentialMode !== (current.credentialMode === "pin" ? "pin" : "idPassword")
|
|
710
|
-
);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
/* Named in the confirmation, because switching a credential OFF is the change
|
|
714
|
-
that locks somebody out. */
|
|
715
|
-
const identificationBeingDisabled = computed(() => {
|
|
716
|
-
const current = selectedReader.value?.identificationMethods || {};
|
|
717
|
-
const form = identificationForm.value;
|
|
718
|
-
const disabled: string[] = [];
|
|
719
|
-
if (current.facial === true && !form.facial) disabled.push("Facial");
|
|
720
|
-
if (current.card === true && !form.card) disabled.push("Card");
|
|
721
|
-
if (current.qrCode === true && !form.qrCode) disabled.push("QR code");
|
|
722
|
-
if (current.credentialMode === "pin" && form.credentialMode !== "pin") disabled.push("PIN");
|
|
723
|
-
return disabled;
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
/*
|
|
727
|
-
* Reload after BOTH outcomes.
|
|
728
|
-
*
|
|
729
|
-
* The confirmed time and the stored methods must come from the server, which
|
|
730
|
-
* only writes them once the reader has echoed the values back. Assuming them
|
|
731
|
-
* here would let the screen claim a door accepts something it refused. On
|
|
732
|
-
* failure the reload matters just as much: the API names the credential the
|
|
733
|
-
* reader would not take, and the controls must go back to what the device
|
|
734
|
-
* actually holds rather than keeping the rejected choice on screen.
|
|
735
|
-
*/
|
|
736
|
-
/*
|
|
737
|
-
* What the device is actually holding, put back to what we hold for it.
|
|
738
|
-
*
|
|
739
|
-
* Worth a button of its own because drift has causes nobody here controls — a
|
|
740
|
-
* firmware update, somebody using the reader's own touchscreen, a restore, a
|
|
741
|
-
* reset. Saving the dialog only writes what is on screen; this reads the
|
|
742
|
-
* device first and reports what had moved.
|
|
743
|
-
*/
|
|
744
|
-
async function reapplyIdentificationMethods() {
|
|
745
|
-
const readerId = String(selectedReader.value?._id || "");
|
|
746
|
-
if (!readerId) return;
|
|
747
|
-
|
|
748
|
-
const readerName = String(selectedReader.value?.name || "HID Reader");
|
|
749
|
-
identificationReconciling.value = true;
|
|
750
|
-
try {
|
|
751
|
-
const response = await reconcileReaderIdentificationMethods(readerId);
|
|
752
|
-
const result = (response?.data || response || {}) as HidRecord;
|
|
753
|
-
identificationDialog.value = false;
|
|
754
|
-
await loadReaders();
|
|
755
|
-
selectedReader.value = readers.value.find((reader) => String(reader._id) === readerId) || null;
|
|
756
|
-
|
|
757
|
-
const status = String(result.status || "");
|
|
758
|
-
const drifted = Array.isArray(result.drifted) ? result.drifted.map(String) : [];
|
|
759
|
-
if (status === "drift_corrected") {
|
|
760
|
-
showResult(
|
|
761
|
-
true,
|
|
762
|
-
"Settings Re-applied",
|
|
763
|
-
`${readerName} had drifted and has been put back. Corrected: ${drifted.map(identificationFieldLabel).join(", ")}.`,
|
|
764
|
-
{ readerName },
|
|
765
|
-
);
|
|
766
|
-
} else if (status === "in_sync") {
|
|
767
|
-
showResult(true, "Settings Already Correct", `${readerName} matches what iService365 holds for it.`, { readerName });
|
|
768
|
-
} else if (status === "skipped" && result.reason === "duplicate_reader") {
|
|
769
|
-
// Not a failure to retry: the device is registered twice, and until a
|
|
770
|
-
// person removes one record both would fight over its configuration.
|
|
771
|
-
showResult(
|
|
772
|
-
false,
|
|
773
|
-
"Duplicate HID Reader",
|
|
774
|
-
`${readerName} shares a device with another registered reader. Remove or deactivate one of them before re-applying settings.`,
|
|
775
|
-
{ readerName },
|
|
776
|
-
);
|
|
777
|
-
} else if (status === "skipped") {
|
|
778
|
-
showResult(
|
|
779
|
-
false,
|
|
780
|
-
"Nothing to Re-apply",
|
|
781
|
-
`No identification methods have been set for ${readerName} yet.`,
|
|
782
|
-
{ readerName },
|
|
783
|
-
);
|
|
784
|
-
} else {
|
|
785
|
-
showResult(false, "Could Not Re-apply Settings", String(result.message || "The HID reader could not be reached."), { readerName });
|
|
786
|
-
}
|
|
787
|
-
} catch (error: unknown) {
|
|
788
|
-
console.error("Unable to reconcile HID identification methods:", error);
|
|
789
|
-
identificationDialog.value = false;
|
|
790
|
-
await loadReaders();
|
|
791
|
-
showResult(false, "Could Not Re-apply Settings", getFriendlyHidError(error), { readerName });
|
|
792
|
-
} finally {
|
|
793
|
-
identificationReconciling.value = false;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/** The reader's own field names, as the person in front of the door knows them. */
|
|
798
|
-
function identificationFieldLabel(field: string) {
|
|
799
|
-
const labels: Record<string, string> = {
|
|
800
|
-
face_identification_enabled: "Facial",
|
|
801
|
-
card_identification_enabled: "Card",
|
|
802
|
-
qrcode_identification_enabled: "QR code",
|
|
803
|
-
pin_identification_enabled: "Credential entry",
|
|
804
|
-
multi_factor_authentication: "Multi-factor",
|
|
805
|
-
};
|
|
806
|
-
return labels[field] || field;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
async function saveIdentificationMethods() {
|
|
810
|
-
const readerId = String(selectedReader.value?._id || "");
|
|
811
|
-
if (!readerId) return;
|
|
812
|
-
|
|
813
|
-
const readerName = String(selectedReader.value?.name || "HID Reader");
|
|
814
|
-
identificationSaving.value = true;
|
|
815
|
-
try {
|
|
816
|
-
await setReaderIdentificationMethods(readerId, { ...identificationForm.value });
|
|
817
|
-
identificationDialog.value = false;
|
|
818
|
-
await loadReaders();
|
|
819
|
-
selectedReader.value = readers.value.find((reader) => String(reader._id) === readerId) || null;
|
|
820
|
-
showResult(true, "Identification Methods Updated", `${readerName} now accepts the credentials you selected.`, {
|
|
821
|
-
readerName,
|
|
822
|
-
});
|
|
823
|
-
} catch (error: unknown) {
|
|
824
|
-
console.error("Unable to set HID identification methods:", error);
|
|
825
|
-
identificationDialog.value = false;
|
|
826
|
-
await loadReaders();
|
|
827
|
-
showResult(false, "Identification Methods Not Applied", getFriendlyHidError(error), { readerName });
|
|
828
|
-
} finally {
|
|
829
|
-
identificationSaving.value = false;
|
|
830
|
-
identificationStage.value = "edit";
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
487
|
function openTestConfirm(reader?: HidRecord | null) {
|
|
835
488
|
if (reader) selectedReader.value = reader;
|
|
836
489
|
pendingAction.value = "test";
|
|
@@ -1266,57 +919,6 @@ function formatCallback(reader?: HidRecord | null) {
|
|
|
1266
919
|
margin-top: 20px;
|
|
1267
920
|
}
|
|
1268
921
|
|
|
1269
|
-
.identification-lead {
|
|
1270
|
-
margin: 0 0 14px;
|
|
1271
|
-
color: var(--muted);
|
|
1272
|
-
font-size: 13px;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
.identification-grid {
|
|
1276
|
-
display: grid;
|
|
1277
|
-
gap: 4px;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
.identification-row,
|
|
1281
|
-
.identification-credential {
|
|
1282
|
-
display: flex;
|
|
1283
|
-
align-items: center;
|
|
1284
|
-
justify-content: space-between;
|
|
1285
|
-
gap: 16px;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
.identification-credential {
|
|
1289
|
-
align-items: flex-start;
|
|
1290
|
-
margin-top: 12px;
|
|
1291
|
-
padding-top: 12px;
|
|
1292
|
-
border-top: 1px solid var(--border);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
.identification-copy {
|
|
1296
|
-
display: grid;
|
|
1297
|
-
gap: 2px;
|
|
1298
|
-
font-size: 13px;
|
|
1299
|
-
color: var(--text);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
/* The reason a control is dead, next to the control rather than in a tooltip
|
|
1303
|
-
nobody opens. */
|
|
1304
|
-
.identification-copy small {
|
|
1305
|
-
color: var(--muted);
|
|
1306
|
-
font-size: 11px;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
.identification-applied {
|
|
1310
|
-
margin: 16px 0 0;
|
|
1311
|
-
color: var(--muted);
|
|
1312
|
-
font-size: 12px;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
.identification-warning {
|
|
1316
|
-
color: var(--text);
|
|
1317
|
-
font-size: 13px;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
922
|
.reader-action-dialog {
|
|
1321
923
|
overflow: hidden;
|
|
1322
924
|
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
v-model="search"
|
|
28
28
|
search
|
|
29
29
|
placeholder="Search Amico user, UID or registration"
|
|
30
|
-
@keyup.enter="
|
|
30
|
+
@keyup.enter="reloadFromFirstPage"
|
|
31
31
|
/>
|
|
32
32
|
<AppSelect
|
|
33
33
|
v-model="userType"
|
|
@@ -208,7 +208,6 @@ const {
|
|
|
208
208
|
} = useHidAmico();
|
|
209
209
|
const readers = ref<HidReader[]>([]);
|
|
210
210
|
const users = ref<HidReaderUser[]>([]);
|
|
211
|
-
const { selectHidReader, resolveForSite } = useHidReaderSelection();
|
|
212
211
|
const selectedReaderId = ref("");
|
|
213
212
|
const search = ref("");
|
|
214
213
|
const userType = ref<"all" | "user" | "visitor">("all");
|
|
@@ -271,11 +270,7 @@ async function loadReaders() {
|
|
|
271
270
|
readers.value = (Array.isArray(items) ? items : [])
|
|
272
271
|
.map(toReader)
|
|
273
272
|
.filter((reader): reader is HidReader => reader !== null);
|
|
274
|
-
|
|
275
|
-
resolver refuses a remembered reader that is not this site's, so a site
|
|
276
|
-
switch or a deleted reader falls back to the first rather than querying
|
|
277
|
-
an id the site does not own. */
|
|
278
|
-
selectedReaderId.value = resolveForSite(props.site, readers.value);
|
|
273
|
+
if (!selectedReaderId.value && readers.value.length) selectedReaderId.value = readers.value[0]._id;
|
|
279
274
|
} catch (error: unknown) {
|
|
280
275
|
readers.value = [];
|
|
281
276
|
showToast(getErrorMessage(error), "error");
|
|
@@ -289,8 +284,6 @@ async function loadUsers() {
|
|
|
289
284
|
return;
|
|
290
285
|
}
|
|
291
286
|
|
|
292
|
-
const sequence = loadUsersSequence.value + 1;
|
|
293
|
-
loadUsersSequence.value = sequence;
|
|
294
287
|
loading.value = true;
|
|
295
288
|
try {
|
|
296
289
|
let clientFiltersUserType = false;
|
|
@@ -326,7 +319,6 @@ async function loadUsers() {
|
|
|
326
319
|
const pageItems = clientFiltersUserType
|
|
327
320
|
? matchingItems.slice(pageStart, pageStart + 10)
|
|
328
321
|
: matchingItems;
|
|
329
|
-
if (sequence !== loadUsersSequence.value) return;
|
|
330
322
|
users.value = pageItems.map(toReaderUser);
|
|
331
323
|
total.value = clientFiltersUserType
|
|
332
324
|
? matchingItems.length
|
|
@@ -338,15 +330,13 @@ async function loadUsers() {
|
|
|
338
330
|
? ""
|
|
339
331
|
: toText(response.pageRange ?? data.pageRange);
|
|
340
332
|
} catch (error: unknown) {
|
|
341
|
-
if (sequence !== loadUsersSequence.value) return;
|
|
342
333
|
users.value = [];
|
|
343
334
|
total.value = 0;
|
|
344
335
|
pages.value = 1;
|
|
345
336
|
serverPageRange.value = "";
|
|
346
337
|
showToast(getErrorMessage(error), "error");
|
|
347
338
|
} finally {
|
|
348
|
-
|
|
349
|
-
if (sequence === loadUsersSequence.value) loading.value = false;
|
|
339
|
+
loading.value = false;
|
|
350
340
|
}
|
|
351
341
|
}
|
|
352
342
|
|
|
@@ -360,31 +350,6 @@ function reloadFromFirstPage() {
|
|
|
360
350
|
loadUsers();
|
|
361
351
|
}
|
|
362
352
|
|
|
363
|
-
/**
|
|
364
|
-
* Typing is the search. It used to reload on `@keyup.enter` and on nothing
|
|
365
|
-
* else, so unless you guessed Enter was required the screen sent no request at
|
|
366
|
-
* all. Debounced, because each reload is a login/load/logout round trip to a
|
|
367
|
-
* reader over a VPN; Enter still works and skips the wait.
|
|
368
|
-
*/
|
|
369
|
-
const SEARCH_DEBOUNCE_MS = 400;
|
|
370
|
-
let searchDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
371
|
-
|
|
372
|
-
function onSearchInput() {
|
|
373
|
-
clearTimeout(searchDebounce);
|
|
374
|
-
searchDebounce = setTimeout(reloadFromFirstPage, SEARCH_DEBOUNCE_MS);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function onSearchSubmit() {
|
|
378
|
-
clearTimeout(searchDebounce);
|
|
379
|
-
reloadFromFirstPage();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
watch(search, onSearchInput);
|
|
383
|
-
onBeforeUnmount(() => clearTimeout(searchDebounce));
|
|
384
|
-
|
|
385
|
-
/** Discards a load whose answer arrives after a newer one started. */
|
|
386
|
-
const loadUsersSequence = ref(0);
|
|
387
|
-
|
|
388
353
|
function hasFacialData(user: HidReaderUser) {
|
|
389
354
|
return user.metadata?.facialEnrolled === true || Boolean(toText(user.metadata?.facialData));
|
|
390
355
|
}
|
|
@@ -546,12 +511,6 @@ function toNumber(value: unknown, fallback: number) {
|
|
|
546
511
|
const number = Number(value);
|
|
547
512
|
return Number.isFinite(number) ? number : fallback;
|
|
548
513
|
}
|
|
549
|
-
|
|
550
|
-
/* Publish every change, including the seed above: a screen that had to fall
|
|
551
|
-
back to the first reader should leave the others agreeing with it. */
|
|
552
|
-
watch(selectedReaderId, (value) => {
|
|
553
|
-
if (value) selectHidReader(props.site, value);
|
|
554
|
-
});
|
|
555
514
|
</script>
|
|
556
515
|
|
|
557
516
|
<style scoped>
|