@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
|
|
51
51
|
<v-card flat border class="table-card">
|
|
52
52
|
<div class="table-refresh">
|
|
53
|
-
<v-btn
|
|
53
|
+
<v-btn
|
|
54
|
+
icon="mdi-refresh"
|
|
55
|
+
variant="text"
|
|
56
|
+
density="comfortable"
|
|
57
|
+
:loading="loading"
|
|
58
|
+
@click="loadUsers"
|
|
59
|
+
/>
|
|
54
60
|
<div class="page-count">{{ pageRange }}</div>
|
|
55
61
|
</div>
|
|
56
62
|
|
|
@@ -66,10 +72,18 @@
|
|
|
66
72
|
</tr>
|
|
67
73
|
</thead>
|
|
68
74
|
<tbody>
|
|
69
|
-
<tr
|
|
75
|
+
<tr
|
|
76
|
+
v-for="user in users"
|
|
77
|
+
:key="user._rowKey || user._id || user.hidUserId"
|
|
78
|
+
>
|
|
70
79
|
<td>{{ getName(user) }}</td>
|
|
71
80
|
<td>
|
|
72
|
-
<v-avatar
|
|
81
|
+
<v-avatar
|
|
82
|
+
v-if="getUserImageSrc(user)"
|
|
83
|
+
size="34"
|
|
84
|
+
rounded="lg"
|
|
85
|
+
class="hid-face-avatar"
|
|
86
|
+
>
|
|
73
87
|
<v-img :src="getUserImageSrc(user)" cover />
|
|
74
88
|
</v-avatar>
|
|
75
89
|
<span v-else>{{ getFacialData(user) }}</span>
|
|
@@ -88,13 +102,22 @@
|
|
|
88
102
|
<td class="text-right">
|
|
89
103
|
<v-menu>
|
|
90
104
|
<template #activator="{ props: menuProps }">
|
|
91
|
-
<v-btn
|
|
105
|
+
<v-btn
|
|
106
|
+
v-bind="menuProps"
|
|
107
|
+
icon="mdi-dots-vertical"
|
|
108
|
+
variant="text"
|
|
109
|
+
density="comfortable"
|
|
110
|
+
/>
|
|
92
111
|
</template>
|
|
93
112
|
|
|
94
113
|
<v-list density="compact" min-width="150">
|
|
95
114
|
<v-list-item title="View" @click="openView(user)" />
|
|
96
115
|
<v-list-item title="Edit" @click="openEdit(user)" />
|
|
97
|
-
<v-list-item
|
|
116
|
+
<v-list-item
|
|
117
|
+
title="Delete"
|
|
118
|
+
class="text-error"
|
|
119
|
+
@click="openDelete(user)"
|
|
120
|
+
/>
|
|
98
121
|
</v-list>
|
|
99
122
|
</v-menu>
|
|
100
123
|
</td>
|
|
@@ -109,8 +132,26 @@
|
|
|
109
132
|
|
|
110
133
|
<div class="table-footer">
|
|
111
134
|
<span>{{ pageRange }}</span>
|
|
112
|
-
<v-btn
|
|
113
|
-
|
|
135
|
+
<v-btn
|
|
136
|
+
icon="mdi-chevron-left"
|
|
137
|
+
variant="text"
|
|
138
|
+
density="comfortable"
|
|
139
|
+
:disabled="page <= 1"
|
|
140
|
+
@click="
|
|
141
|
+
page--;
|
|
142
|
+
loadUsers();
|
|
143
|
+
"
|
|
144
|
+
/>
|
|
145
|
+
<v-btn
|
|
146
|
+
icon="mdi-chevron-right"
|
|
147
|
+
variant="text"
|
|
148
|
+
density="comfortable"
|
|
149
|
+
:disabled="page >= pages"
|
|
150
|
+
@click="
|
|
151
|
+
page++;
|
|
152
|
+
loadUsers();
|
|
153
|
+
"
|
|
154
|
+
/>
|
|
114
155
|
</div>
|
|
115
156
|
</v-card>
|
|
116
157
|
|
|
@@ -125,7 +166,12 @@
|
|
|
125
166
|
<v-menu>
|
|
126
167
|
<template #activator="{ props: menuProps }">
|
|
127
168
|
<button v-bind="menuProps" class="photo-button" type="button">
|
|
128
|
-
<v-img
|
|
169
|
+
<v-img
|
|
170
|
+
v-if="form.photoPreview"
|
|
171
|
+
:src="form.photoPreview"
|
|
172
|
+
cover
|
|
173
|
+
class="photo-preview"
|
|
174
|
+
/>
|
|
129
175
|
<span v-if="form.photoPreview" class="photo-edit-icon">
|
|
130
176
|
<v-icon icon="mdi-pencil" size="16" />
|
|
131
177
|
</span>
|
|
@@ -137,10 +183,25 @@
|
|
|
137
183
|
</template>
|
|
138
184
|
<v-list density="compact" min-width="190">
|
|
139
185
|
<v-list-item title="Browse" @click="triggerPhotoInput" />
|
|
140
|
-
<v-list-item
|
|
186
|
+
<v-list-item
|
|
187
|
+
title="Take Photo using Camera"
|
|
188
|
+
@click="triggerCameraInput"
|
|
189
|
+
/>
|
|
190
|
+
<v-list-item
|
|
191
|
+
v-if="form.photoPreview"
|
|
192
|
+
title="Remove Photo"
|
|
193
|
+
class="text-error"
|
|
194
|
+
@click="removePhoto"
|
|
195
|
+
/>
|
|
141
196
|
</v-list>
|
|
142
197
|
</v-menu>
|
|
143
|
-
<input
|
|
198
|
+
<input
|
|
199
|
+
ref="photoInput"
|
|
200
|
+
type="file"
|
|
201
|
+
accept="image/*"
|
|
202
|
+
class="hidden-input"
|
|
203
|
+
@change="onPhotoChange"
|
|
204
|
+
/>
|
|
144
205
|
</div>
|
|
145
206
|
|
|
146
207
|
<div class="field-label">Name <span>*</span></div>
|
|
@@ -177,57 +238,83 @@
|
|
|
177
238
|
class="mb-3"
|
|
178
239
|
/>
|
|
179
240
|
|
|
180
|
-
<div class="field-label">
|
|
241
|
+
<div class="field-label">Access PIN (optional)</div>
|
|
181
242
|
<v-text-field
|
|
182
|
-
v-model="form.
|
|
183
|
-
aria-label="
|
|
184
|
-
placeholder="Enter
|
|
243
|
+
v-model="form.accessPin"
|
|
244
|
+
aria-label="Access PIN"
|
|
245
|
+
placeholder="Enter numeric access PIN"
|
|
185
246
|
density="compact"
|
|
186
247
|
variant="outlined"
|
|
187
|
-
|
|
248
|
+
inputmode="numeric"
|
|
249
|
+
autocomplete="new-password"
|
|
250
|
+
:type="showPin ? 'text' : 'password'"
|
|
251
|
+
:append-inner-icon="showPin ? 'mdi-eye-off' : 'mdi-eye'"
|
|
252
|
+
:hint="selectedUser && pinEnrolled ? 'Leave blank to keep the current PIN.' : 'PIN works as an alternative to face or QR.'"
|
|
253
|
+
persistent-hint
|
|
254
|
+
@click:append-inner="showPin = !showPin"
|
|
188
255
|
class="mb-3"
|
|
189
256
|
/>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
aria-label="Panic Password"
|
|
195
|
-
placeholder="Enter password"
|
|
257
|
+
<v-checkbox
|
|
258
|
+
v-if="selectedUser && pinEnrolled"
|
|
259
|
+
v-model="removePinRequested"
|
|
260
|
+
label="Remove current access PIN"
|
|
196
261
|
density="compact"
|
|
197
|
-
|
|
198
|
-
hide-details="auto"
|
|
199
|
-
:type="showPassword ? 'text' : 'password'"
|
|
200
|
-
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
|
201
|
-
@click:append-inner="showPassword = !showPassword"
|
|
262
|
+
hide-details
|
|
202
263
|
/>
|
|
203
264
|
</v-card-text>
|
|
204
265
|
|
|
205
266
|
<v-card-actions class="pa-0">
|
|
206
|
-
<v-btn
|
|
207
|
-
|
|
267
|
+
<v-btn
|
|
268
|
+
class="text-none action-cancel"
|
|
269
|
+
variant="flat"
|
|
270
|
+
height="44"
|
|
271
|
+
@click="formDialog = false"
|
|
272
|
+
>Cancel</v-btn
|
|
273
|
+
>
|
|
274
|
+
<v-btn
|
|
275
|
+
class="text-none action-submit"
|
|
276
|
+
variant="flat"
|
|
277
|
+
height="44"
|
|
278
|
+
:loading="saving"
|
|
279
|
+
@click="saveUser"
|
|
280
|
+
>
|
|
208
281
|
Save
|
|
209
282
|
</v-btn>
|
|
210
283
|
</v-card-actions>
|
|
211
284
|
</v-card>
|
|
212
285
|
</v-dialog>
|
|
213
286
|
|
|
287
|
+
<HidCameraCaptureDialog v-model="cameraDialog" @capture="onCameraCapture" />
|
|
288
|
+
|
|
214
289
|
<v-dialog v-model="viewDialog" max-width="430">
|
|
215
290
|
<v-card rounded="lg">
|
|
216
291
|
<v-card-title class="small-title">User Information</v-card-title>
|
|
217
292
|
<v-card-text>
|
|
218
293
|
<div class="detail-grid">
|
|
219
294
|
<span>Name</span><strong>{{ getName(selectedUser) }}</strong>
|
|
220
|
-
<span>User ID</span
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<span>
|
|
225
|
-
|
|
295
|
+
<span>User ID</span
|
|
296
|
+
><strong>{{
|
|
297
|
+
selectedUser ? formatHidUid(selectedUser.hidUserId) : "N/A"
|
|
298
|
+
}}</strong>
|
|
299
|
+
<span>Registration No.</span
|
|
300
|
+
><strong>{{ selectedUser?.registration || "N/A" }}</strong>
|
|
301
|
+
<span>Last access time</span
|
|
302
|
+
><strong>{{
|
|
303
|
+
formatDate(selectedUser?.metadata?.lastAccessAt)
|
|
304
|
+
}}</strong>
|
|
305
|
+
<span>Status</span
|
|
306
|
+
><strong>{{
|
|
307
|
+
selectedUser ? getMappingStatus(selectedUser) : "N/A"
|
|
308
|
+
}}</strong>
|
|
309
|
+
<span>Access PIN</span
|
|
310
|
+
><strong>{{ selectedUser?.metadata?.pinEnrolled ? "Configured" : "N/A" }}</strong>
|
|
226
311
|
</div>
|
|
227
312
|
</v-card-text>
|
|
228
313
|
<v-card-actions>
|
|
229
314
|
<v-spacer />
|
|
230
|
-
<v-btn class="text-none" variant="text" @click="viewDialog = false"
|
|
315
|
+
<v-btn class="text-none" variant="text" @click="viewDialog = false"
|
|
316
|
+
>Close</v-btn
|
|
317
|
+
>
|
|
231
318
|
</v-card-actions>
|
|
232
319
|
</v-card>
|
|
233
320
|
</v-dialog>
|
|
@@ -238,12 +325,26 @@
|
|
|
238
325
|
<v-card-text class="text-body-2">
|
|
239
326
|
Are you sure you want to permanently delete this HID user?
|
|
240
327
|
<div class="text-caption text-medium-emphasis mt-2">
|
|
241
|
-
This action will remove the user from the HID reader and mark the
|
|
328
|
+
This action will remove the user from the HID reader and mark the
|
|
329
|
+
identity as deleted.
|
|
242
330
|
</div>
|
|
243
331
|
</v-card-text>
|
|
244
332
|
<v-card-actions class="pa-0">
|
|
245
|
-
<v-btn
|
|
246
|
-
|
|
333
|
+
<v-btn
|
|
334
|
+
class="text-none action-cancel"
|
|
335
|
+
variant="flat"
|
|
336
|
+
height="44"
|
|
337
|
+
@click="deleteDialog = false"
|
|
338
|
+
>Cancel</v-btn
|
|
339
|
+
>
|
|
340
|
+
<v-btn
|
|
341
|
+
class="text-none action-submit"
|
|
342
|
+
variant="flat"
|
|
343
|
+
height="44"
|
|
344
|
+
:loading="deleting"
|
|
345
|
+
@click="deleteUser"
|
|
346
|
+
>Delete</v-btn
|
|
347
|
+
>
|
|
247
348
|
</v-card-actions>
|
|
248
349
|
</v-card>
|
|
249
350
|
</v-dialog>
|
|
@@ -270,6 +371,11 @@ const {
|
|
|
270
371
|
getReaders,
|
|
271
372
|
getIdentities,
|
|
272
373
|
getUserImage,
|
|
374
|
+
setUserImage,
|
|
375
|
+
deleteUserImage,
|
|
376
|
+
getUserPinStatus,
|
|
377
|
+
setUserPin,
|
|
378
|
+
deleteUserPin,
|
|
273
379
|
runObjectOperation,
|
|
274
380
|
createIdentity,
|
|
275
381
|
updateIdentity,
|
|
@@ -294,8 +400,13 @@ const formDialog = ref(false);
|
|
|
294
400
|
const viewDialog = ref(false);
|
|
295
401
|
const deleteDialog = ref(false);
|
|
296
402
|
const selectedUser = ref<Record<string, any> | null>(null);
|
|
297
|
-
const
|
|
403
|
+
const showPin = ref(false);
|
|
404
|
+
const pinEnrolled = ref(false);
|
|
298
405
|
const photoInput = ref<HTMLInputElement | null>(null);
|
|
406
|
+
const cameraDialog = ref(false);
|
|
407
|
+
const selectedPhotoFile = ref<File | null>(null);
|
|
408
|
+
const removePhotoRequested = ref(false);
|
|
409
|
+
const removePinRequested = ref(false);
|
|
299
410
|
const snackbar = reactive({
|
|
300
411
|
show: false,
|
|
301
412
|
color: "success",
|
|
@@ -310,8 +421,7 @@ const form = reactive({
|
|
|
310
421
|
name: "",
|
|
311
422
|
hidUserId: "",
|
|
312
423
|
registration: "",
|
|
313
|
-
|
|
314
|
-
pinPassword: "",
|
|
424
|
+
accessPin: "",
|
|
315
425
|
photoPreview: "",
|
|
316
426
|
});
|
|
317
427
|
|
|
@@ -321,7 +431,7 @@ const readerOptions = computed(() =>
|
|
|
321
431
|
readers.value.map((reader) => ({
|
|
322
432
|
title: reader.name || reader.deviceId || reader._id,
|
|
323
433
|
value: reader._id,
|
|
324
|
-
}))
|
|
434
|
+
}))
|
|
325
435
|
);
|
|
326
436
|
|
|
327
437
|
const pageRange = computed(() => {
|
|
@@ -335,7 +445,7 @@ const pageRange = computed(() => {
|
|
|
335
445
|
watch(
|
|
336
446
|
() => props.site,
|
|
337
447
|
() => init(),
|
|
338
|
-
{ immediate: true }
|
|
448
|
+
{ immediate: true }
|
|
339
449
|
);
|
|
340
450
|
|
|
341
451
|
async function init() {
|
|
@@ -346,7 +456,8 @@ async function init() {
|
|
|
346
456
|
async function loadReaders() {
|
|
347
457
|
if (!props.site) return;
|
|
348
458
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
349
|
-
readers.value =
|
|
459
|
+
readers.value =
|
|
460
|
+
response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
350
461
|
if (!selectedReaderId.value && readers.value.length) {
|
|
351
462
|
selectedReaderId.value = readers.value[0]._id;
|
|
352
463
|
}
|
|
@@ -375,12 +486,19 @@ async function loadUsers() {
|
|
|
375
486
|
limit: 500,
|
|
376
487
|
offset: 0,
|
|
377
488
|
}),
|
|
378
|
-
isAdministratorIdentity()
|
|
489
|
+
isAdministratorIdentity()
|
|
490
|
+
? loadAdministratorRoles(selectedReaderId.value)
|
|
491
|
+
: Promise.resolve(null),
|
|
379
492
|
]);
|
|
380
493
|
|
|
381
|
-
const identities =
|
|
494
|
+
const identities =
|
|
495
|
+
identityResponse?.items ??
|
|
496
|
+
identityResponse?.data?.items ??
|
|
497
|
+
identityResponse?.data?.identities ??
|
|
498
|
+
[];
|
|
382
499
|
const adminRoleIds = getAdministratorRoleUserIds(roleResponse);
|
|
383
500
|
const readerUsers = normalizeHidUsers(readerResponse).filter((user) => {
|
|
501
|
+
if (Number(user.userTypeId) === 1) return false;
|
|
384
502
|
if (!isAdministratorIdentity()) return true;
|
|
385
503
|
const hidUserId = toHidNumericId(user.hidUserId);
|
|
386
504
|
return Boolean(hidUserId && adminRoleIds.has(hidUserId));
|
|
@@ -409,21 +527,23 @@ async function loadUsers() {
|
|
|
409
527
|
async function loadUserImages(items: Record<string, any>[]) {
|
|
410
528
|
if (!selectedReaderId.value) return;
|
|
411
529
|
|
|
412
|
-
await Promise.all(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
530
|
+
await Promise.all(
|
|
531
|
+
items.map(async (user) => {
|
|
532
|
+
const hidUserId = toHidNumericId(user.hidUserId);
|
|
533
|
+
const key = String(hidUserId || "");
|
|
534
|
+
if (!hidUserId || userImages.value[key] || user?.metadata?.photo) return;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const response = await getUserImage(selectedReaderId.value, hidUserId);
|
|
538
|
+
const image = response?.data || response;
|
|
539
|
+
userImages.value[key] = image?.base64
|
|
540
|
+
? `data:${image.contentType || "image/jpeg"};base64,${image.base64}`
|
|
541
|
+
: "";
|
|
542
|
+
} catch {
|
|
543
|
+
userImages.value[key] = "";
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
);
|
|
427
547
|
}
|
|
428
548
|
|
|
429
549
|
function resetForm() {
|
|
@@ -434,9 +554,13 @@ function resetForm() {
|
|
|
434
554
|
form.name = "";
|
|
435
555
|
form.hidUserId = "";
|
|
436
556
|
form.registration = "";
|
|
437
|
-
form.
|
|
438
|
-
|
|
557
|
+
form.accessPin = "";
|
|
558
|
+
pinEnrolled.value = false;
|
|
559
|
+
showPin.value = false;
|
|
560
|
+
removePinRequested.value = false;
|
|
439
561
|
form.photoPreview = "";
|
|
562
|
+
selectedPhotoFile.value = null;
|
|
563
|
+
removePhotoRequested.value = false;
|
|
440
564
|
}
|
|
441
565
|
|
|
442
566
|
async function openEnroll() {
|
|
@@ -460,11 +584,24 @@ async function openEdit(user: Record<string, any>) {
|
|
|
460
584
|
form.name = getName(user);
|
|
461
585
|
form.hidUserId = formatHidUid(user.hidUserId);
|
|
462
586
|
form.registration = user.registration ?? "";
|
|
463
|
-
form.
|
|
464
|
-
|
|
587
|
+
form.accessPin = "";
|
|
588
|
+
pinEnrolled.value = Boolean(user.metadata?.pinEnrolled);
|
|
589
|
+
removePinRequested.value = false;
|
|
465
590
|
form.photoPreview = getUserImageSrc(user);
|
|
591
|
+
selectedPhotoFile.value = null;
|
|
592
|
+
removePhotoRequested.value = false;
|
|
466
593
|
formDialog.value = true;
|
|
467
594
|
|
|
595
|
+
try {
|
|
596
|
+
const status = await getUserPinStatus(
|
|
597
|
+
form.reader,
|
|
598
|
+
toHidNumericId(user.hidUserId),
|
|
599
|
+
);
|
|
600
|
+
pinEnrolled.value = Boolean(status?.data?.pinEnrolled);
|
|
601
|
+
} catch {
|
|
602
|
+
// Preserve the last known status if the reader is temporarily offline.
|
|
603
|
+
}
|
|
604
|
+
|
|
468
605
|
if (!form.photoPreview) {
|
|
469
606
|
await loadUserImages([user]);
|
|
470
607
|
if (selectedUser.value?._rowKey === user._rowKey) {
|
|
@@ -487,9 +624,53 @@ function triggerPhotoInput() {
|
|
|
487
624
|
photoInput.value?.click();
|
|
488
625
|
}
|
|
489
626
|
|
|
627
|
+
function triggerCameraInput() {
|
|
628
|
+
cameraDialog.value = true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function onCameraCapture(dataUrl: string) {
|
|
632
|
+
const [header, encodedData] = dataUrl.split(",", 2);
|
|
633
|
+
if (!header || !encodedData) {
|
|
634
|
+
showToast("Unable to capture the facial image.", "error");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const mimeType = header.match(/^data:([^;]+);base64$/)?.[1] || "image/jpeg";
|
|
639
|
+
const decodedData = atob(encodedData);
|
|
640
|
+
const bytes = new Uint8Array(decodedData.length);
|
|
641
|
+
for (let index = 0; index < decodedData.length; index += 1) {
|
|
642
|
+
bytes[index] = decodedData.charCodeAt(index);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
selectedPhotoFile.value = new File([bytes], `hid-facial-${Date.now()}.jpg`, {
|
|
646
|
+
type: mimeType,
|
|
647
|
+
});
|
|
648
|
+
form.photoPreview = dataUrl;
|
|
649
|
+
removePhotoRequested.value = false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function removePhoto() {
|
|
653
|
+
form.photoPreview = "";
|
|
654
|
+
selectedPhotoFile.value = null;
|
|
655
|
+
removePhotoRequested.value = true;
|
|
656
|
+
}
|
|
657
|
+
|
|
490
658
|
function onPhotoChange(event: Event) {
|
|
491
|
-
const
|
|
659
|
+
const input = event.target as HTMLInputElement;
|
|
660
|
+
const file = input.files?.[0];
|
|
661
|
+
input.value = "";
|
|
492
662
|
if (!file) return;
|
|
663
|
+
if (!["image/jpeg", "image/png"].includes(file.type)) {
|
|
664
|
+
showToast("Facial enrollment only accepts JPG or PNG images.", "error");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (file.size > 2 * 1024 * 1024) {
|
|
668
|
+
showToast("Facial enrollment images must be smaller than 2 MB.", "error");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
selectedPhotoFile.value = file;
|
|
673
|
+
removePhotoRequested.value = false;
|
|
493
674
|
const reader = new FileReader();
|
|
494
675
|
reader.onload = () => {
|
|
495
676
|
form.photoPreview = String(reader.result || "");
|
|
@@ -498,11 +679,16 @@ function onPhotoChange(event: Event) {
|
|
|
498
679
|
}
|
|
499
680
|
|
|
500
681
|
async function saveUser() {
|
|
501
|
-
const readerId =
|
|
682
|
+
const readerId =
|
|
683
|
+
selectedReaderId.value || form.reader || readers.value[0]?._id;
|
|
502
684
|
if (!readerId || !form.name || !form.hidUserId || !form.registration) {
|
|
503
685
|
showToast("Please complete the required HID user fields.", "error");
|
|
504
686
|
return;
|
|
505
687
|
}
|
|
688
|
+
if (form.accessPin && !/^\d{1,32}$/.test(form.accessPin)) {
|
|
689
|
+
showToast("Access PIN must contain numbers only.", "error");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
506
692
|
|
|
507
693
|
saving.value = true;
|
|
508
694
|
try {
|
|
@@ -519,8 +705,13 @@ async function saveUser() {
|
|
|
519
705
|
const payload = {
|
|
520
706
|
hidUserId: String(hidUser.id),
|
|
521
707
|
registration: form.registration,
|
|
522
|
-
|
|
523
|
-
|
|
708
|
+
type: props.identityType as
|
|
709
|
+
| "resident"
|
|
710
|
+
| "staff"
|
|
711
|
+
| "contractor"
|
|
712
|
+
| "visitor"
|
|
713
|
+
| "admin"
|
|
714
|
+
| "unknown",
|
|
524
715
|
status: "active" as const,
|
|
525
716
|
metadata: {
|
|
526
717
|
name: form.name,
|
|
@@ -530,13 +721,18 @@ async function saveUser() {
|
|
|
530
721
|
unitLabel: buildUnitLabel(),
|
|
531
722
|
facialData: form.photoPreview ? form.hidUserId : "",
|
|
532
723
|
photo: form.photoPreview,
|
|
533
|
-
|
|
724
|
+
imageTimestamp: selectedUser.value?.metadata?.imageTimestamp || "",
|
|
725
|
+
facialScores: selectedUser.value?.metadata?.facialScores || {},
|
|
726
|
+
pinEnrolled: pinEnrolled.value,
|
|
534
727
|
},
|
|
535
728
|
};
|
|
536
729
|
|
|
537
730
|
if (selectedUser.value) {
|
|
538
731
|
await updateHidUserOnReader(readerId, hidUser);
|
|
539
732
|
await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
|
|
733
|
+
const facialResult = await syncFacialImage(readerId, hidUser.id);
|
|
734
|
+
applyFacialResult(payload.metadata, facialResult);
|
|
735
|
+
payload.metadata.pinEnrolled = await syncAccessPin(readerId, hidUser.id);
|
|
540
736
|
if (selectedUser.value._id) {
|
|
541
737
|
await updateIdentity(selectedUser.value._id, payload);
|
|
542
738
|
} else {
|
|
@@ -545,13 +741,20 @@ async function saveUser() {
|
|
|
545
741
|
} else {
|
|
546
742
|
await createHidUserOnReader(readerId, hidUser);
|
|
547
743
|
await syncHidUserRole(readerId, hidUser.id, isAdministratorIdentity());
|
|
744
|
+
const facialResult = await syncFacialImage(readerId, hidUser.id);
|
|
745
|
+
applyFacialResult(payload.metadata, facialResult);
|
|
746
|
+
payload.metadata.pinEnrolled = await syncAccessPin(readerId, hidUser.id);
|
|
548
747
|
await saveIdentityOnReader(readerId, payload);
|
|
549
748
|
selectedReaderId.value = readerId;
|
|
550
749
|
}
|
|
551
750
|
|
|
552
751
|
formDialog.value = false;
|
|
553
752
|
await loadUsers();
|
|
554
|
-
showToast(
|
|
753
|
+
showToast(
|
|
754
|
+
selectedUser.value?._id
|
|
755
|
+
? "HID user updated on reader."
|
|
756
|
+
: "HID user enrolled on reader."
|
|
757
|
+
);
|
|
555
758
|
} catch (error: any) {
|
|
556
759
|
console.error("Unable to save HID user:", error);
|
|
557
760
|
showToast(getHidErrorMessage(error), "error");
|
|
@@ -560,12 +763,86 @@ async function saveUser() {
|
|
|
560
763
|
}
|
|
561
764
|
}
|
|
562
765
|
|
|
766
|
+
async function syncAccessPin(readerId: string, hidUserId: number) {
|
|
767
|
+
if (removePinRequested.value) {
|
|
768
|
+
await deleteUserPin(readerId, hidUserId);
|
|
769
|
+
pinEnrolled.value = false;
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (form.accessPin) {
|
|
773
|
+
await setUserPin(readerId, hidUserId, form.accessPin);
|
|
774
|
+
form.accessPin = "";
|
|
775
|
+
pinEnrolled.value = true;
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
return pinEnrolled.value;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
type FacialSyncResult = {
|
|
782
|
+
action: "none" | "enrolled" | "removed";
|
|
783
|
+
timestamp?: number;
|
|
784
|
+
scores?: Record<string, unknown>;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
async function syncFacialImage(
|
|
788
|
+
readerId: string,
|
|
789
|
+
hidUserId: number
|
|
790
|
+
): Promise<FacialSyncResult> {
|
|
791
|
+
if (selectedPhotoFile.value) {
|
|
792
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
793
|
+
const response = await setUserImage(
|
|
794
|
+
readerId,
|
|
795
|
+
hidUserId,
|
|
796
|
+
selectedPhotoFile.value,
|
|
797
|
+
{
|
|
798
|
+
timestamp,
|
|
799
|
+
match: true,
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
const result = response?.data;
|
|
803
|
+
userImages.value[String(hidUserId)] = form.photoPreview;
|
|
804
|
+
return { action: "enrolled", timestamp, scores: result?.scores };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (removePhotoRequested.value) {
|
|
808
|
+
await deleteUserImage(readerId, hidUserId);
|
|
809
|
+
delete userImages.value[String(hidUserId)];
|
|
810
|
+
return { action: "removed" };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return { action: "none" };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function applyFacialResult(
|
|
817
|
+
metadata: Record<string, unknown>,
|
|
818
|
+
result: FacialSyncResult
|
|
819
|
+
) {
|
|
820
|
+
if (result.action === "enrolled") {
|
|
821
|
+
metadata.facialData = form.hidUserId;
|
|
822
|
+
metadata.imageTimestamp = String(result.timestamp || "");
|
|
823
|
+
metadata.facialScores = result.scores || {};
|
|
824
|
+
} else if (result.action === "removed") {
|
|
825
|
+
metadata.facialData = "";
|
|
826
|
+
metadata.photo = "";
|
|
827
|
+
metadata.imageTimestamp = "";
|
|
828
|
+
metadata.facialScores = {};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
563
832
|
async function deleteUser() {
|
|
564
833
|
if (!selectedUser.value) return;
|
|
565
834
|
|
|
566
835
|
deleting.value = true;
|
|
567
836
|
try {
|
|
568
|
-
await syncHidUserRole(
|
|
837
|
+
await syncHidUserRole(
|
|
838
|
+
selectedReaderId.value,
|
|
839
|
+
toHidNumericId(selectedUser.value.hidUserId),
|
|
840
|
+
false
|
|
841
|
+
);
|
|
842
|
+
await deleteUserPin(
|
|
843
|
+
selectedReaderId.value,
|
|
844
|
+
toHidNumericId(selectedUser.value.hidUserId),
|
|
845
|
+
);
|
|
569
846
|
await deleteHidUserFromReader(selectedReaderId.value, selectedUser.value);
|
|
570
847
|
if (selectedUser.value._id) {
|
|
571
848
|
await deleteIdentity(selectedUser.value._id);
|
|
@@ -581,8 +858,14 @@ async function deleteUser() {
|
|
|
581
858
|
}
|
|
582
859
|
}
|
|
583
860
|
|
|
584
|
-
async function saveIdentityOnReader(
|
|
585
|
-
|
|
861
|
+
async function saveIdentityOnReader(
|
|
862
|
+
readerId: string,
|
|
863
|
+
payload: Record<string, any>
|
|
864
|
+
) {
|
|
865
|
+
const existingIdentity = await findExistingIdentityOnReader(
|
|
866
|
+
readerId,
|
|
867
|
+
payload
|
|
868
|
+
);
|
|
586
869
|
if (existingIdentity?._id) {
|
|
587
870
|
await updateIdentity(existingIdentity._id, payload);
|
|
588
871
|
return;
|
|
@@ -594,31 +877,47 @@ async function saveIdentityOnReader(readerId: string, payload: Record<string, an
|
|
|
594
877
|
const message = getHidErrorMessage(error);
|
|
595
878
|
if (!message.toLowerCase().includes("already exists")) throw error;
|
|
596
879
|
|
|
597
|
-
const duplicatedIdentity = await findExistingIdentityOnReader(
|
|
880
|
+
const duplicatedIdentity = await findExistingIdentityOnReader(
|
|
881
|
+
readerId,
|
|
882
|
+
payload
|
|
883
|
+
);
|
|
598
884
|
if (!duplicatedIdentity?._id) throw error;
|
|
599
885
|
await updateIdentity(duplicatedIdentity._id, payload);
|
|
600
886
|
}
|
|
601
887
|
}
|
|
602
888
|
|
|
603
|
-
async function findExistingIdentityOnReader(
|
|
889
|
+
async function findExistingIdentityOnReader(
|
|
890
|
+
readerId: string,
|
|
891
|
+
payload: Record<string, any>
|
|
892
|
+
) {
|
|
604
893
|
const response = await getIdentities(readerId, {
|
|
605
894
|
page: 1,
|
|
606
895
|
limit: 500,
|
|
607
896
|
search: payload.hidUserId || payload.registration || payload.cardNo || "",
|
|
608
897
|
});
|
|
609
|
-
const identities =
|
|
898
|
+
const identities =
|
|
899
|
+
response?.items ??
|
|
900
|
+
response?.data?.items ??
|
|
901
|
+
response?.data?.identities ??
|
|
902
|
+
[];
|
|
610
903
|
const hidUserId = String(payload.hidUserId || "");
|
|
611
904
|
const registration = String(payload.registration || "");
|
|
612
905
|
const cardNo = String(payload.cardNo || "");
|
|
613
906
|
|
|
614
|
-
return identities.find(
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
907
|
+
return identities.find(
|
|
908
|
+
(identity: Record<string, any>) =>
|
|
909
|
+
String(identity.hidUserId || "") === hidUserId ||
|
|
910
|
+
Boolean(
|
|
911
|
+
registration && String(identity.registration || "") === registration
|
|
912
|
+
) ||
|
|
913
|
+
Boolean(cardNo && String(identity.cardNo || "") === cardNo)
|
|
618
914
|
);
|
|
619
915
|
}
|
|
620
916
|
|
|
621
|
-
async function createHidUserOnReader(
|
|
917
|
+
async function createHidUserOnReader(
|
|
918
|
+
readerId: string,
|
|
919
|
+
user: Record<string, any>
|
|
920
|
+
) {
|
|
622
921
|
const existing = await getHidUserFromReader(readerId, user.id);
|
|
623
922
|
if (existing) {
|
|
624
923
|
await updateHidUserOnReader(readerId, user);
|
|
@@ -648,7 +947,10 @@ async function getHidUserFromReader(readerId: string, id: unknown) {
|
|
|
648
947
|
return normalizeHidUsers(response)[0];
|
|
649
948
|
}
|
|
650
949
|
|
|
651
|
-
async function updateHidUserOnReader(
|
|
950
|
+
async function updateHidUserOnReader(
|
|
951
|
+
readerId: string,
|
|
952
|
+
user: Record<string, any>
|
|
953
|
+
) {
|
|
652
954
|
const { id, ...values } = user;
|
|
653
955
|
await runObjectOperation(readerId, {
|
|
654
956
|
operation: "modify",
|
|
@@ -662,7 +964,10 @@ async function updateHidUserOnReader(readerId: string, user: Record<string, any>
|
|
|
662
964
|
});
|
|
663
965
|
}
|
|
664
966
|
|
|
665
|
-
async function deleteHidUserFromReader(
|
|
967
|
+
async function deleteHidUserFromReader(
|
|
968
|
+
readerId: string,
|
|
969
|
+
user: Record<string, any>
|
|
970
|
+
) {
|
|
666
971
|
const id = toHidNumericId(user.hidUserId);
|
|
667
972
|
if (!readerId || !id) return;
|
|
668
973
|
|
|
@@ -677,7 +982,11 @@ async function deleteHidUserFromReader(readerId: string, user: Record<string, an
|
|
|
677
982
|
});
|
|
678
983
|
}
|
|
679
984
|
|
|
680
|
-
async function syncHidUserRole(
|
|
985
|
+
async function syncHidUserRole(
|
|
986
|
+
readerId: string,
|
|
987
|
+
hidUserId: unknown,
|
|
988
|
+
isAdministrator: boolean
|
|
989
|
+
) {
|
|
681
990
|
const userId = toHidNumericId(hidUserId);
|
|
682
991
|
if (!readerId || !userId) return;
|
|
683
992
|
|
|
@@ -771,14 +1080,17 @@ function getAdministratorRoleUserIds(response: Record<string, any> | null) {
|
|
|
771
1080
|
return new Set(
|
|
772
1081
|
(Array.isArray(roles) ? roles : [])
|
|
773
1082
|
.map((role) => toHidNumericId(role.user_id))
|
|
774
|
-
.filter((userId): userId is number => Boolean(userId))
|
|
1083
|
+
.filter((userId): userId is number => Boolean(userId))
|
|
775
1084
|
);
|
|
776
1085
|
}
|
|
777
1086
|
|
|
778
1087
|
function toHidUserObject(identity: Record<string, any>) {
|
|
779
1088
|
return {
|
|
780
1089
|
id: toHidNumericId(identity.hidUserId),
|
|
781
|
-
name:
|
|
1090
|
+
name:
|
|
1091
|
+
identity.metadata?.name ||
|
|
1092
|
+
identity.name ||
|
|
1093
|
+
`User ${identity.hidUserId || ""}`.trim(),
|
|
782
1094
|
registration: identity.registration || "",
|
|
783
1095
|
};
|
|
784
1096
|
}
|
|
@@ -807,23 +1119,40 @@ function getName(user?: Record<string, any> | null) {
|
|
|
807
1119
|
}
|
|
808
1120
|
|
|
809
1121
|
function getUnit(user?: Record<string, any> | null) {
|
|
810
|
-
return
|
|
1122
|
+
return (
|
|
1123
|
+
user?.metadata?.unitLabel ||
|
|
1124
|
+
[user?.metadata?.block, user?.metadata?.level, user?.metadata?.unit]
|
|
1125
|
+
.filter(Boolean)
|
|
1126
|
+
.join("/") ||
|
|
1127
|
+
"N/A"
|
|
1128
|
+
);
|
|
811
1129
|
}
|
|
812
1130
|
|
|
813
1131
|
function getFacialData(user: Record<string, any>) {
|
|
814
|
-
return
|
|
1132
|
+
return (
|
|
1133
|
+
user?.metadata?.facialData ||
|
|
1134
|
+
user?.metadata?.imageTimestamp ||
|
|
1135
|
+
(user?.metadata?.photo ? user.hidUserId : "N/A")
|
|
1136
|
+
);
|
|
815
1137
|
}
|
|
816
1138
|
|
|
817
1139
|
function getUserImageSrc(user: Record<string, any>) {
|
|
818
1140
|
const hidUserId = toHidNumericId(user?.hidUserId);
|
|
819
|
-
return
|
|
1141
|
+
return (
|
|
1142
|
+
user?.metadata?.photo ||
|
|
1143
|
+
(hidUserId ? userImages.value[String(hidUserId)] : "") ||
|
|
1144
|
+
""
|
|
1145
|
+
);
|
|
820
1146
|
}
|
|
821
1147
|
|
|
822
1148
|
async function getNextUid() {
|
|
823
1149
|
const readerIds = await getExistingHidReaderIds();
|
|
824
1150
|
const maxNumber = users.value.reduce((max, user) => {
|
|
825
1151
|
const match = String(user.hidUserId ?? "").match(/^UID(\d+)$/i);
|
|
826
|
-
return Math.max(
|
|
1152
|
+
return Math.max(
|
|
1153
|
+
max,
|
|
1154
|
+
match ? Number(match[1]) : toHidNumericId(user.hidUserId) || 0
|
|
1155
|
+
);
|
|
827
1156
|
}, 0);
|
|
828
1157
|
const nextNumber = Math.max(maxNumber, ...readerIds, 0) + 1;
|
|
829
1158
|
return `UID${String(nextNumber).padStart(6, "0")}`;
|
|
@@ -837,10 +1166,15 @@ async function getNextRegistration() {
|
|
|
837
1166
|
const numericRegistrations = registrations
|
|
838
1167
|
.map((registration) => {
|
|
839
1168
|
const match = registration.match(/^(\d+)$/);
|
|
840
|
-
return match
|
|
1169
|
+
return match
|
|
1170
|
+
? { value: Number(match[1]), width: match[1].length }
|
|
1171
|
+
: undefined;
|
|
841
1172
|
})
|
|
842
1173
|
.filter((item): item is { value: number; width: number } => Boolean(item));
|
|
843
|
-
const maxRegistration = numericRegistrations.reduce(
|
|
1174
|
+
const maxRegistration = numericRegistrations.reduce(
|
|
1175
|
+
(max, item) => Math.max(max, item.value),
|
|
1176
|
+
0
|
|
1177
|
+
);
|
|
844
1178
|
const width = Math.max(6, ...numericRegistrations.map((item) => item.width));
|
|
845
1179
|
|
|
846
1180
|
return String(maxRegistration + 1).padStart(width, "0");
|
|
@@ -882,11 +1216,14 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
882
1216
|
hidUserId: String(user.id || ""),
|
|
883
1217
|
rawHidUserId: user.id,
|
|
884
1218
|
registration: user.registration,
|
|
1219
|
+
userTypeId: user.user_type_id,
|
|
885
1220
|
cardNo: user.card_value || user.cardNo || "",
|
|
886
1221
|
name: user.name,
|
|
887
1222
|
metadata: {
|
|
888
1223
|
name: user.name,
|
|
889
|
-
imageTimestamp: user.image_timestamp
|
|
1224
|
+
imageTimestamp: user.image_timestamp
|
|
1225
|
+
? String(user.image_timestamp)
|
|
1226
|
+
: "",
|
|
890
1227
|
lastAccessAt: fromUnixSeconds(user.last_access),
|
|
891
1228
|
hidReaderSource: true,
|
|
892
1229
|
},
|
|
@@ -894,7 +1231,10 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
894
1231
|
: [];
|
|
895
1232
|
}
|
|
896
1233
|
|
|
897
|
-
function mergeReaderUsersWithIdentities(
|
|
1234
|
+
function mergeReaderUsersWithIdentities(
|
|
1235
|
+
readerUsers: Record<string, any>[],
|
|
1236
|
+
identities: Record<string, any>[]
|
|
1237
|
+
) {
|
|
898
1238
|
const identityById = new Map<string, Record<string, any>>();
|
|
899
1239
|
identities.forEach((identity) => {
|
|
900
1240
|
const id = toHidNumericId(identity.hidUserId);
|
|
@@ -902,7 +1242,9 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
902
1242
|
});
|
|
903
1243
|
|
|
904
1244
|
const merged = readerUsers.map((readerUser) => {
|
|
905
|
-
const identity = identityById.get(
|
|
1245
|
+
const identity = identityById.get(
|
|
1246
|
+
String(toHidNumericId(readerUser.hidUserId))
|
|
1247
|
+
);
|
|
906
1248
|
if (!identity) return readerUser;
|
|
907
1249
|
|
|
908
1250
|
return {
|
|
@@ -917,8 +1259,11 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
917
1259
|
metadata: {
|
|
918
1260
|
...readerUser.metadata,
|
|
919
1261
|
...identity.metadata,
|
|
920
|
-
imageTimestamp:
|
|
921
|
-
|
|
1262
|
+
imageTimestamp:
|
|
1263
|
+
identity.metadata?.imageTimestamp ||
|
|
1264
|
+
readerUser.metadata?.imageTimestamp,
|
|
1265
|
+
lastAccessAt:
|
|
1266
|
+
identity.metadata?.lastAccessAt || readerUser.metadata?.lastAccessAt,
|
|
922
1267
|
},
|
|
923
1268
|
};
|
|
924
1269
|
});
|
|
@@ -929,13 +1274,16 @@ function mergeReaderUsersWithIdentities(readerUsers: Record<string, any>[], iden
|
|
|
929
1274
|
function filterUsers(items: Record<string, any>[]) {
|
|
930
1275
|
const searchText = search.value.trim().toLowerCase();
|
|
931
1276
|
return items.filter((item) => {
|
|
932
|
-
const matchesSearch =
|
|
933
|
-
|
|
934
|
-
item.hidUserId,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1277
|
+
const matchesSearch =
|
|
1278
|
+
!searchText ||
|
|
1279
|
+
[getName(item), item.hidUserId, item.registration, item.cardNo].some(
|
|
1280
|
+
(value) =>
|
|
1281
|
+
String(value || "")
|
|
1282
|
+
.toLowerCase()
|
|
1283
|
+
.includes(searchText)
|
|
1284
|
+
);
|
|
1285
|
+
const matchesStatus =
|
|
1286
|
+
status.value === "Status" || getMappingStatus(item) === status.value;
|
|
939
1287
|
return matchesSearch && matchesStatus;
|
|
940
1288
|
});
|
|
941
1289
|
}
|
|
@@ -995,7 +1343,7 @@ function getHidErrorMessage(error: any) {
|
|
|
995
1343
|
data?.statusMessage ||
|
|
996
1344
|
error?.statusMessage ||
|
|
997
1345
|
error?.message ||
|
|
998
|
-
"Unable to save HID user."
|
|
1346
|
+
"Unable to save HID user."
|
|
999
1347
|
);
|
|
1000
1348
|
}
|
|
1001
1349
|
</script>
|