@7365admin1/layer-common 4.90.2-staging.491 → 4.90.2

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +0 -57
  3. package/assets/css/screens.css +4 -8
  4. package/components/AccessCardDetailsDialog.vue +6 -59
  5. package/components/AccessCardPreviewDialog.vue +53 -371
  6. package/components/AppSelect.vue +4 -143
  7. package/components/BuildingUnitFormEdit.vue +0 -6
  8. package/components/Dialog/UpdateMoreAction.vue +1 -11
  9. package/components/DocumentForm.vue +112 -407
  10. package/components/DocumentManagement.vue +12 -213
  11. package/components/Facility/BookingSetup.vue +0 -63
  12. package/components/HidAccessLogDashboard.vue +17 -74
  13. package/components/HidIntercomManagement.vue +14 -183
  14. package/components/HidQrCodeConfiguration.vue +195 -17
  15. package/components/HidReaderManagement.vue +0 -398
  16. package/components/HidReaderUserRoster.vue +3 -44
  17. package/components/HidUserEnrollment.vue +292 -2255
  18. package/components/InventoryItemDetail.vue +1 -2
  19. package/components/InventoryLinesDialog.vue +13 -91
  20. package/components/InventoryPhotoInput.vue +6 -44
  21. package/components/InventoryReportsTab.vue +3 -23
  22. package/components/InventoryRequestsTab.vue +4 -27
  23. package/components/InventoryStockTab.vue +0 -3
  24. package/components/InvitationClientForm.vue +1 -19
  25. package/components/InvitationForm.vue +4 -31
  26. package/components/InvitationMain.vue +22 -124
  27. package/components/MemberInformation.vue +4 -4
  28. package/components/PassInformation.vue +2 -2
  29. package/components/SiteSettings.vue +0 -48
  30. package/components/TableMain.vue +7 -22
  31. package/components/VehicleManagement.vue +5 -80
  32. package/components/VisitorForm.vue +19 -65
  33. package/components/VisitorManagement.vue +5 -267
  34. package/composables/useAccessManagement.ts +1 -17
  35. package/composables/useDocument.ts +0 -7
  36. package/composables/useFacility.ts +0 -11
  37. package/composables/useHidAmico.ts +0 -165
  38. package/composables/useHidNavigation.ts +1 -25
  39. package/composables/useInventory.ts +1 -4
  40. package/composables/useMember.ts +0 -12
  41. package/composables/useNFCPatrolReportFilters.ts +3 -21
  42. package/composables/useNFCPatrolRoute.ts +1 -8
  43. package/composables/usePeople.ts +0 -19
  44. package/composables/useServiceProvider.ts +2 -45
  45. package/composables/useSettingsPermission.ts +0 -14
  46. package/composables/useUser.ts +2 -6
  47. package/composables/useVehicle.ts +1 -40
  48. package/composables/useVerification.ts +0 -3
  49. package/middleware/01.auth.ts +7 -3
  50. package/package.json +1 -1
  51. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  52. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  53. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  54. package/types/document.d.ts +0 -14
  55. package/types/facility.d.ts +0 -4
  56. package/types/inventory.d.ts +0 -27
  57. package/types/member.d.ts +0 -4
  58. package/types/people.d.ts +1 -28
  59. package/types/service-provider.d.ts +0 -5
  60. package/types/site.d.ts +1 -7
  61. package/components/BuildingUnitManagement.vue +0 -298
  62. package/components/HidAccessPermissions.vue +0 -424
  63. package/components/HidProfileQrCode.vue +0 -332
  64. package/components/InventoryPoDialog.vue +0 -111
  65. package/components/VehicleQrStickerDialog.vue +0 -249
  66. package/composables/useHidReaderSelection.ts +0 -50
  67. package/composables/useSiteCategory.ts +0 -45
  68. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  69. package/utils/hid-enrolment-subject.ts +0 -344
  70. package/utils/hid-permission-assignments.ts +0 -198
  71. package/utils/hid-reader-selection.ts +0 -52
  72. package/utils/inventory-receiving.ts +0 -90
  73. package/utils/invite-group.ts +0 -61
  74. package/utils/invite-sites.ts +0 -25
  75. package/utils/occupancy-role.ts +0 -109
  76. package/utils/service-type.ts +0 -30
  77. 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="onSearchSubmit"
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
- /* Shared with every other HID screen — see `useHidReaderSelection`. The
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
- // Only the newest load owns the spinner.
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>