@7365admin1/layer-common 4.0.0 → 4.0.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.
- package/CHANGELOG.md +124 -0
- package/components/CameraForm.vue +2 -5
- package/components/CameraMain.vue +3 -6
- package/components/ClientDetailForm.vue +65 -5
- package/components/ClientMain.vue +201 -59
- package/components/HidAccessLogDashboard.vue +114 -34
- package/components/HidQrCodeConfiguration.vue +1 -1
- package/components/HidReaderManagement.vue +34 -11
- package/components/HidReaderUserRoster.vue +376 -0
- package/components/HidUserEnrollment.vue +7 -1
- package/components/HidUserMapping.vue +583 -0
- package/components/MemberMain.vue +12 -2
- package/components/RolePermissionMain.vue +2 -1
- package/composables/useConsoleTier.ts +73 -0
- package/composables/useHidAmico.ts +30 -6
- package/composables/useHidNavigation.ts +8 -1
- package/composables/useMember.ts +0 -5
- package/composables/usePromoCode.ts +57 -0
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-user-mapping/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +1 -1
- package/utils/client-subscription.test.ts +95 -0
- package/utils/console-tier.test.ts +87 -0
- package/utils/console-tier.ts +67 -0
- package/utils/data.test.ts +84 -0
- package/utils/data.ts +92 -0
- package/utils/promo-code-form.test.ts +437 -0
- package/utils/promo-code-form.ts +246 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="hid-reader-user-roster">
|
|
3
|
+
<TableMain
|
|
4
|
+
title="HID Reader Users"
|
|
5
|
+
:headers="headers"
|
|
6
|
+
:items="users"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
:items-per-page="-1"
|
|
9
|
+
:page="page"
|
|
10
|
+
:pages="pages"
|
|
11
|
+
:page-range="pageRange"
|
|
12
|
+
item-value="_rowKey"
|
|
13
|
+
show-header
|
|
14
|
+
@refresh="loadUsers"
|
|
15
|
+
@update:page="goToPage"
|
|
16
|
+
>
|
|
17
|
+
<template #actions>
|
|
18
|
+
<AppButton icon="mdi-account-switch-outline" @click="openMapping">
|
|
19
|
+
HID User Mapping
|
|
20
|
+
</AppButton>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #extension>
|
|
24
|
+
<div class="app-filter-row hid-reader-user-roster__filters">
|
|
25
|
+
<AppSelect
|
|
26
|
+
v-if="readers.length"
|
|
27
|
+
v-model="selectedReaderId"
|
|
28
|
+
class="reader-select"
|
|
29
|
+
:items="readerOptions"
|
|
30
|
+
@update:model-value="reloadFromFirstPage"
|
|
31
|
+
/>
|
|
32
|
+
<AppField
|
|
33
|
+
v-model="search"
|
|
34
|
+
search
|
|
35
|
+
placeholder="Search Amico user, UID or registration"
|
|
36
|
+
@keyup.enter="reloadFromFirstPage"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<template #[`item.name`]="{ item }">
|
|
42
|
+
<span class="app-cell--strong">{{ getRawName(item) }}</span>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<template #[`item.hidUserId`]="{ item }">
|
|
46
|
+
<span class="app-cell--num">{{ formatUid(item.hidUserId) }}</span>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template #[`item.registration`]="{ item }">
|
|
50
|
+
<span class="app-cell--num">{{ item.registration || "N/A" }}</span>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<template #[`item.facialData`]="{ item }">
|
|
54
|
+
<v-avatar
|
|
55
|
+
v-if="getUserImageSrc(item)"
|
|
56
|
+
size="34"
|
|
57
|
+
rounded="lg"
|
|
58
|
+
class="hid-face-avatar"
|
|
59
|
+
>
|
|
60
|
+
<v-img :src="getUserImageSrc(item)" cover />
|
|
61
|
+
</v-avatar>
|
|
62
|
+
<span v-else :class="hasFacialData(item) ? 'facial-data--available' : 'app-cell--muted'">
|
|
63
|
+
<v-icon
|
|
64
|
+
:icon="hasFacialData(item) ? 'mdi-face-recognition' : 'mdi-face-man-shimmer-outline'"
|
|
65
|
+
size="19"
|
|
66
|
+
/>
|
|
67
|
+
{{ hasFacialData(item) ? "Enrolled" : "Not enrolled" }}
|
|
68
|
+
</span>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<template #[`item.isAdministrator`]="{ item }">
|
|
72
|
+
<v-icon
|
|
73
|
+
:icon="item.isAdministrator ? 'mdi-check-circle' : 'mdi-close-circle'"
|
|
74
|
+
:class="item.isAdministrator ? 'admin-rule--enabled' : 'admin-rule--disabled'"
|
|
75
|
+
:aria-label="item.isAdministrator ? 'Administrator' : 'Not administrator'"
|
|
76
|
+
size="22"
|
|
77
|
+
/>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<template #[`item.mappingStatus`]="{ item }">
|
|
81
|
+
<StatusChip :status="isMapped(item) ? 'Mapped' : 'Unmapped'" />
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<template #no-data>
|
|
85
|
+
<div class="table-card__empty">
|
|
86
|
+
<v-icon icon="mdi-account-off-outline" size="32" />
|
|
87
|
+
<span>No users found on this HID reader.</span>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
</TableMain>
|
|
91
|
+
|
|
92
|
+
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3500">
|
|
93
|
+
{{ snackbar.message }}
|
|
94
|
+
</v-snackbar>
|
|
95
|
+
</section>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
const props = defineProps({
|
|
100
|
+
site: {
|
|
101
|
+
type: String,
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
type HidMetadata = Record<string, unknown> & { name?: string };
|
|
107
|
+
type HidReader = Record<string, unknown> & {
|
|
108
|
+
_id: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
deviceId?: string;
|
|
111
|
+
portalName?: string;
|
|
112
|
+
};
|
|
113
|
+
type HidReaderUser = Record<string, unknown> & {
|
|
114
|
+
_rowKey?: string;
|
|
115
|
+
hidUserId?: string;
|
|
116
|
+
registration?: string;
|
|
117
|
+
name?: string;
|
|
118
|
+
metadata?: HidMetadata;
|
|
119
|
+
person?: string;
|
|
120
|
+
member?: string;
|
|
121
|
+
serviceProvider?: string;
|
|
122
|
+
visitor?: string;
|
|
123
|
+
isAdministrator?: boolean;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const { getReaders, getReaderUsers, getUserImage } = useHidAmico();
|
|
127
|
+
const route = useRoute();
|
|
128
|
+
const router = useRouter();
|
|
129
|
+
const readers = ref<HidReader[]>([]);
|
|
130
|
+
const users = ref<HidReaderUser[]>([]);
|
|
131
|
+
const userImages = ref<Record<string, string>>({});
|
|
132
|
+
const selectedReaderId = ref("");
|
|
133
|
+
const search = ref("");
|
|
134
|
+
const page = ref(1);
|
|
135
|
+
const pages = ref(1);
|
|
136
|
+
const total = ref(0);
|
|
137
|
+
const serverPageRange = ref("");
|
|
138
|
+
const loading = ref(false);
|
|
139
|
+
const snackbar = reactive({ show: false, color: "success", message: "" });
|
|
140
|
+
|
|
141
|
+
const headers = [
|
|
142
|
+
{ title: "Amico Name", value: "name", sortable: false },
|
|
143
|
+
{ title: "UID", value: "hidUserId", sortable: false },
|
|
144
|
+
{ title: "Registration No.", value: "registration", sortable: false },
|
|
145
|
+
{ title: "Facial Data", value: "facialData", sortable: false },
|
|
146
|
+
{ title: "Admin", value: "isAdministrator", sortable: false },
|
|
147
|
+
{ title: "iService Mapping", value: "mappingStatus", sortable: false },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const readerOptions = computed(() => readers.value.map((reader) => ({
|
|
151
|
+
title: `${reader.name || reader.deviceId || reader._id} - ${reader.portalName || "Portal not configured"}`,
|
|
152
|
+
value: reader._id,
|
|
153
|
+
})));
|
|
154
|
+
|
|
155
|
+
const pageRange = computed(() => {
|
|
156
|
+
if (serverPageRange.value) return serverPageRange.value;
|
|
157
|
+
if (!total.value) return "0-0 of 0";
|
|
158
|
+
const start = (page.value - 1) * 10 + 1;
|
|
159
|
+
return `${start}-${Math.min(page.value * 10, total.value)} of ${total.value}`;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
watch(() => props.site, init, { immediate: true });
|
|
163
|
+
|
|
164
|
+
async function init() {
|
|
165
|
+
await loadReaders();
|
|
166
|
+
await loadUsers();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function loadReaders() {
|
|
170
|
+
if (!props.site) return;
|
|
171
|
+
try {
|
|
172
|
+
const response = toRecord(await getReaders({ site: props.site, page: 1, limit: 100 }));
|
|
173
|
+
const data = toRecord(response.data);
|
|
174
|
+
const items = response.items ?? data.items ?? data.readers ?? [];
|
|
175
|
+
readers.value = (Array.isArray(items) ? items : [])
|
|
176
|
+
.map(toReader)
|
|
177
|
+
.filter((reader): reader is HidReader => reader !== null);
|
|
178
|
+
if (!selectedReaderId.value && readers.value.length) selectedReaderId.value = readers.value[0]._id;
|
|
179
|
+
} catch (error: unknown) {
|
|
180
|
+
readers.value = [];
|
|
181
|
+
showToast(getErrorMessage(error), "error");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function loadUsers() {
|
|
186
|
+
if (!selectedReaderId.value) {
|
|
187
|
+
users.value = [];
|
|
188
|
+
total.value = 0;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
loading.value = true;
|
|
193
|
+
try {
|
|
194
|
+
const response = toRecord(await getReaderUsers(selectedReaderId.value, {
|
|
195
|
+
page: page.value,
|
|
196
|
+
limit: 10,
|
|
197
|
+
search: search.value.trim(),
|
|
198
|
+
}));
|
|
199
|
+
const data = toRecord(response.data);
|
|
200
|
+
const items = response.items ?? data.items ?? [];
|
|
201
|
+
users.value = (Array.isArray(items) ? items : []).map(toReaderUser);
|
|
202
|
+
await loadUserImages(users.value);
|
|
203
|
+
total.value = toNumber(response.total ?? data.total, users.value.length);
|
|
204
|
+
pages.value = Math.max(1, toNumber(response.pages ?? data.pages, 1));
|
|
205
|
+
serverPageRange.value = toText(response.pageRange ?? data.pageRange);
|
|
206
|
+
} catch (error: unknown) {
|
|
207
|
+
users.value = [];
|
|
208
|
+
total.value = 0;
|
|
209
|
+
pages.value = 1;
|
|
210
|
+
serverPageRange.value = "";
|
|
211
|
+
showToast(getErrorMessage(error), "error");
|
|
212
|
+
} finally {
|
|
213
|
+
loading.value = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function goToPage(nextPage: number) {
|
|
218
|
+
page.value = nextPage;
|
|
219
|
+
loadUsers();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function reloadFromFirstPage() {
|
|
223
|
+
page.value = 1;
|
|
224
|
+
userImages.value = {};
|
|
225
|
+
loadUsers();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function openMapping() {
|
|
229
|
+
router.push({
|
|
230
|
+
name: "org-site-access-mgmt-hid-user-mapping",
|
|
231
|
+
params: { org: route.params.org, site: props.site },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isMapped(user: HidReaderUser) {
|
|
236
|
+
return Boolean(user.person || user.member || user.serviceProvider || user.visitor);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasFacialData(user: HidReaderUser) {
|
|
240
|
+
return user.metadata?.facialEnrolled === true || Boolean(toText(user.metadata?.facialData));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function loadUserImages(items: HidReaderUser[]) {
|
|
244
|
+
if (!selectedReaderId.value) return;
|
|
245
|
+
|
|
246
|
+
await Promise.all(items.map(async (user) => {
|
|
247
|
+
const hidUserId = toHidNumericId(user.hidUserId);
|
|
248
|
+
const key = String(hidUserId || "");
|
|
249
|
+
if (!hidUserId || key in userImages.value) return;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const response = toRecord(await getUserImage(selectedReaderId.value, hidUserId));
|
|
253
|
+
const image = toRecord(response.data ?? response);
|
|
254
|
+
const base64 = toText(image.base64);
|
|
255
|
+
userImages.value[key] = base64
|
|
256
|
+
? `data:${toText(image.contentType) || "image/jpeg"};base64,${base64}`
|
|
257
|
+
: "";
|
|
258
|
+
} catch {
|
|
259
|
+
userImages.value[key] = "";
|
|
260
|
+
}
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getUserImageSrc(user: HidReaderUser) {
|
|
265
|
+
const hidUserId = toHidNumericId(user.hidUserId);
|
|
266
|
+
return toText(user.metadata?.photo) || (hidUserId ? userImages.value[String(hidUserId)] : "") || "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getRawName(user: HidReaderUser) {
|
|
270
|
+
return toText(user.metadata?.name) || toText(user.name) || "Unknown";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatUid(value: unknown) {
|
|
274
|
+
const text = toText(value);
|
|
275
|
+
if (!text) return "N/A";
|
|
276
|
+
const numeric = Number(text);
|
|
277
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? `UID${String(numeric).padStart(6, "0")}` : text;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function toHidNumericId(value: unknown) {
|
|
281
|
+
const numeric = Number(toText(value));
|
|
282
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function showToast(message: string, color = "success") {
|
|
286
|
+
snackbar.message = message;
|
|
287
|
+
snackbar.color = color;
|
|
288
|
+
snackbar.show = true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getErrorMessage(error: unknown) {
|
|
292
|
+
const record = toRecord(error);
|
|
293
|
+
const data = toRecord(record.data);
|
|
294
|
+
return toText(data.message) || toText(record.message) || "Unable to load HID reader users.";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function toReader(value: unknown): HidReader | null {
|
|
298
|
+
const reader = toRecord(value);
|
|
299
|
+
const id = toText(reader._id);
|
|
300
|
+
return id ? { ...reader, _id: id, name: toText(reader.name) || undefined } : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function toReaderUser(value: unknown): HidReaderUser {
|
|
304
|
+
const user = toRecord(value);
|
|
305
|
+
return {
|
|
306
|
+
...user,
|
|
307
|
+
_rowKey: toText(user._rowKey) || toText(user.id) || toText(user.hidUserId),
|
|
308
|
+
hidUserId: toText(user.hidUserId) || toText(user.id) || undefined,
|
|
309
|
+
registration: toText(user.registration) || undefined,
|
|
310
|
+
name: toText(user.name) || undefined,
|
|
311
|
+
metadata: toMetadata(user.metadata),
|
|
312
|
+
person: toText(user.person) || undefined,
|
|
313
|
+
member: toText(user.member) || undefined,
|
|
314
|
+
serviceProvider: toText(user.serviceProvider) || undefined,
|
|
315
|
+
visitor: toText(user.visitor) || undefined,
|
|
316
|
+
isAdministrator: user.isAdministrator === true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toMetadata(value: unknown): HidMetadata {
|
|
321
|
+
return toRecord(value) as HidMetadata;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
326
|
+
? value as Record<string, unknown>
|
|
327
|
+
: {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toText(value: unknown) {
|
|
331
|
+
if (typeof value === "string") return value.trim();
|
|
332
|
+
if (typeof value === "number") return String(value);
|
|
333
|
+
return "";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function toNumber(value: unknown, fallback: number) {
|
|
337
|
+
const number = Number(value);
|
|
338
|
+
return Number.isFinite(number) ? number : fallback;
|
|
339
|
+
}
|
|
340
|
+
</script>
|
|
341
|
+
|
|
342
|
+
<style scoped>
|
|
343
|
+
.hid-reader-user-roster {
|
|
344
|
+
padding: 24px 0;
|
|
345
|
+
min-width: 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.hid-reader-user-roster__filters {
|
|
349
|
+
margin: 0;
|
|
350
|
+
padding: 10px 14px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.hid-reader-user-roster__filters :deep(.app-field) {
|
|
354
|
+
max-width: 320px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.admin-rule--enabled {
|
|
358
|
+
color: var(--success, #16855f);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.admin-rule--disabled {
|
|
362
|
+
color: var(--err, #d94a4a);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.hid-face-avatar {
|
|
366
|
+
border: 1px solid var(--border-color, #e0e3e8);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.facial-data--available {
|
|
370
|
+
align-items: center;
|
|
371
|
+
color: var(--success, #16855f);
|
|
372
|
+
display: inline-flex;
|
|
373
|
+
gap: 6px;
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
}
|
|
376
|
+
</style>
|
|
@@ -936,7 +936,7 @@ async function loadSubjectCandidates() {
|
|
|
936
936
|
page: 1,
|
|
937
937
|
limit: 500,
|
|
938
938
|
});
|
|
939
|
-
permissionCandidates.value = response.items
|
|
939
|
+
permissionCandidates.value = response.items ?? response.data?.items ?? [];
|
|
940
940
|
} finally {
|
|
941
941
|
loadingSubjects.value = false;
|
|
942
942
|
}
|
|
@@ -1892,6 +1892,12 @@ function toRecord(value: unknown): Record<string, unknown> {
|
|
|
1892
1892
|
: {};
|
|
1893
1893
|
}
|
|
1894
1894
|
|
|
1895
|
+
function toText(value: unknown, fallback = "") {
|
|
1896
|
+
if (typeof value === "string") return value.trim() || fallback;
|
|
1897
|
+
if (typeof value === "number") return String(value);
|
|
1898
|
+
return fallback;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1895
1901
|
function toHidReader(value: unknown): HidReader | null {
|
|
1896
1902
|
const reader = toRecord(value);
|
|
1897
1903
|
const id = toText(reader._id);
|