@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
@@ -1,424 +0,0 @@
1
- <template>
2
- <v-dialog v-model="open" max-width="760" scrollable>
3
- <v-card class="hid-access-permissions">
4
- <v-card-title class="hid-access-permissions__title">
5
- <div>
6
- <div class="text-h6">Access Permissions</div>
7
- <div class="text-caption text-medium-emphasis">{{ readerLabel }}</div>
8
- </div>
9
- </v-card-title>
10
-
11
- <v-divider />
12
-
13
- <!-- The one precondition the server enforces before it will talk about
14
- permissions at all. Saying it here beats letting the first request
15
- fail with the same sentence in a snackbar. -->
16
- <v-alert
17
- v-if="!readerLocation"
18
- type="warning"
19
- variant="tonal"
20
- density="compact"
21
- class="ma-4"
22
- >
23
- Set a location on this HID Face Reader before managing permissions.
24
- </v-alert>
25
-
26
- <template v-else>
27
- <div class="hid-access-permissions__intro">
28
- <p class="text-body-2 text-medium-emphasis mb-0">
29
- Residents enrolled on this reader. Enrolling already grants access — switch
30
- somebody off to close the door to them without removing their face, card
31
- or PIN.
32
- </p>
33
- </div>
34
-
35
- <v-card-text class="hid-access-permissions__body">
36
- <TableMain
37
- :headers="headers"
38
- :items="rows"
39
- :loading="loading"
40
- :items-per-page="-1"
41
- :page="page"
42
- :pages="pages"
43
- :page-range="pageRange"
44
- item-value="rowKey"
45
- @refresh="loadEnrolled"
46
- @update:page="goToPage"
47
- >
48
- <template #extension>
49
- <div class="app-filter-row">
50
- <AppField
51
- v-model="search"
52
- search
53
- placeholder="Search name, unit or registration"
54
- @keyup.enter="searchFromFirstPage"
55
- />
56
- </div>
57
- </template>
58
-
59
- <template #[`item.name`]="{ item }">
60
- <div class="hid-access-permissions__person">
61
- <div>{{ item.name || "—" }}</div>
62
- <div class="text-caption text-medium-emphasis">{{ item.subtitle || "—" }}</div>
63
- </div>
64
- </template>
65
-
66
- <template #[`item.selected`]="{ item }">
67
- <div v-if="!item.assignable" class="hid-access-permissions__toggle">
68
- <span class="text-caption text-medium-emphasis">{{ item.reason }}</span>
69
- </div>
70
- <div v-else class="hid-access-permissions__toggle">
71
- <v-switch
72
- :model-value="item.selected"
73
- :disabled="saving"
74
- color="primary"
75
- density="compact"
76
- hide-details
77
- :aria-label="`Door access for ${item.name}`"
78
- @update:model-value="(value) => toggle(item, value === true)"
79
- />
80
- <span
81
- class="text-caption"
82
- :class="item.selected ? 'text-success' : 'text-medium-emphasis'"
83
- >
84
- {{ item.selected ? "Has access" : "No access" }}
85
- </span>
86
- </div>
87
- </template>
88
- </TableMain>
89
- </v-card-text>
90
- </template>
91
-
92
- <v-divider />
93
-
94
- <v-card-actions class="hid-access-permissions__actions">
95
- <span v-if="readerLocation" class="text-caption text-medium-emphasis">
96
- {{ assignedTotal }} with access on this reader
97
- </span>
98
- <v-spacer />
99
- <AppButton variant="ghost" :disabled="saving" @click="open = false">Cancel</AppButton>
100
- <AppButton
101
- icon="mdi-content-save-outline"
102
- :loading="saving"
103
- :disabled="!readerLocation || !dirty || saving"
104
- @click="save"
105
- >
106
- Save Permissions
107
- </AppButton>
108
- </v-card-actions>
109
- </v-card>
110
-
111
- <v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3500">
112
- {{ snackbar.message }}
113
- </v-snackbar>
114
- </v-dialog>
115
- </template>
116
-
117
- <script setup lang="ts">
118
- import {
119
- applySelection,
120
- hasChanges,
121
- readCandidatePage,
122
- seedAssignments,
123
- setAssignment,
124
- toAssignmentPayload,
125
- type HidAssignmentState,
126
- } from "../utils/hid-permission-assignments";
127
-
128
- /**
129
- * WHO MAY PASS THIS READER — a review and revoke screen, not a picker.
130
- *
131
- * Enrolling somebody now grants them access: `createIdentity` writes the
132
- * assignment, and the reconcile puts them in the reader's group in the same
133
- * pass. A property manager therefore has ONE thing to do, and this screen
134
- * exists for the other direction — taking access away again.
135
- *
136
- * So it lists the people ENROLLED on this reader rather than everybody at the
137
- * site. Switching somebody off removes their assignment, and the reconcile then
138
- * deletes their `user_groups` row; their identity, face, card and PIN are left
139
- * alone, so they stay recognisable at the door and are simply refused.
140
- *
141
- * The editing model lives in `utils/hid-permission-assignments.ts`, because the
142
- * save is whole-reader while the list is one page at a time — and getting that
143
- * wrong silently revokes everybody who is not on screen. See the tests there.
144
- */
145
- const props = defineProps<{
146
- modelValue: boolean;
147
- site: string;
148
- org: string;
149
- reader: Record<string, unknown> | null;
150
- }>();
151
-
152
- const emit = defineEmits<{
153
- (event: "update:modelValue", value: boolean): void;
154
- (event: "saved"): void;
155
- }>();
156
-
157
- const { getSitePermissions, getIdentities, updateSitePermissions } = useHidAmico();
158
-
159
- /* Name over unit in one cell rather than a "Unit" column — see `subtitleOf`. */
160
- const headers = [
161
- { title: "Resident", value: "name", sortable: false },
162
- { title: "Door access", value: "selected", sortable: false, width: 190 },
163
- ];
164
-
165
- const search = ref("");
166
- const page = ref(1);
167
- const pages = ref(1);
168
- const pageRange = ref("");
169
- const loading = ref(false);
170
- const saving = ref(false);
171
- const enrolled = ref<THidPermissionCandidate[]>([]);
172
- const snackbar = reactive({ show: false, color: "success", message: "" });
173
-
174
- /** What the server holds, and the working copy. The pair drives `dirty`. */
175
- const savedState = ref<HidAssignmentState>(new Map());
176
- const draft = ref<HidAssignmentState>(new Map());
177
-
178
- const open = computed({
179
- get: () => props.modelValue,
180
- set: (value: boolean) => emit("update:modelValue", value),
181
- });
182
-
183
- const readerId = computed(() => String(props.reader?._id ?? ""));
184
- const readerLocation = computed(() => String(props.reader?.location ?? "").trim());
185
- const readerLabel = computed(() => {
186
- const name = String(props.reader?.name ?? "").trim() || "HID Face Reader";
187
- return readerLocation.value ? `${name} · ${readerLocation.value}` : name;
188
- });
189
-
190
- const assignedTotal = computed(() => draft.value.size);
191
- const dirty = computed(() => hasChanges(savedState.value, draft.value));
192
- /* The draft is the truth once the dialog is open, so `selected` is recomputed
193
- on every render rather than trusted from the row. */
194
- const rows = computed(() => applySelection(enrolled.value, draft.value));
195
-
196
- watch(
197
- () => [props.modelValue, readerId.value] as const,
198
- ([isOpen]) => {
199
- if (!isOpen || !readerId.value) return;
200
- search.value = "";
201
- page.value = 1;
202
- void loadAll();
203
- },
204
- { immediate: true },
205
- );
206
-
207
- async function loadAll() {
208
- if (!readerLocation.value) return;
209
- await loadAssignments();
210
- await loadEnrolled();
211
- }
212
-
213
- /**
214
- * `getSitePermissions` returns the reader's assignments across ALL categories.
215
- * That is what makes a whole-reader payload possible from a paged view, so it
216
- * is loaded once per opening rather than once per page.
217
- */
218
- async function loadAssignments() {
219
- try {
220
- const response = await getSitePermissions(props.site, props.org, readerId.value);
221
- const assignments = response?.data?.assignments ?? [];
222
- savedState.value = seedAssignments(assignments);
223
- draft.value = seedAssignments(assignments);
224
- } catch (error) {
225
- showToast(getErrorMessage(error, "Unable to load this reader's permissions."), "error");
226
- }
227
- }
228
-
229
- function toRecord(value: unknown): Record<string, unknown> {
230
- return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
231
- }
232
-
233
- /**
234
- * The subject an identity names, in the order `createIdentity` validates them.
235
- *
236
- * A visitor returns null and is left out of the list: visitor access comes from
237
- * `user_access_rules`, not from a group, so there is nothing here to switch
238
- * off. An identity linked to nobody — a device user the reader holds that was
239
- * never tied to a person — is dropped for the same reason.
240
- */
241
- function subjectOf(identity: Record<string, unknown>) {
242
- if (identity.person) {
243
- return { subjectId: String(identity.person), category: "resident" as const };
244
- }
245
- if (identity.member) {
246
- return { subjectId: String(identity.member), category: "property_management" as const };
247
- }
248
- if (identity.serviceProvider) {
249
- return { subjectId: String(identity.serviceProvider), category: "service_provider" as const };
250
- }
251
- return null;
252
- }
253
-
254
- /**
255
- * Why a row cannot be switched, in words the reader of the screen can act on.
256
- *
257
- * The row is still DRAWN. Dropping one silently is what made the pager say
258
- * "1-9 of 9" over seven rows: the count comes from the server and the rows came
259
- * from a filtered list, so people vanished with nothing to say they had.
260
- *
261
- * What lands here is a resident identity holding only an account link - no
262
- * person, member or service-provider record - which leaves no subject to
263
- * assign. Visitors never reach it: they are excluded by the query above,
264
- * because their access comes from `user_access_rules` rather than a group and
265
- * there would be nothing here to switch.
266
- */
267
- function unassignableReason() {
268
- return "Not linked to a person";
269
- }
270
-
271
- /**
272
- * The unit, when the enrolment captured one.
273
- *
274
- * `unitLabel` and NOT `block`/`level`/`unit`: those three hold ObjectIds, and
275
- * the enrolment form resolves them through the occupancy tree before storing
276
- * the readable label beside them. `buildUnitLabel` carries the warning this
277
- * ignored once already — joining the raw values stamps
278
- * "665f.../665f.../665f..." into the column.
279
- *
280
- * Falls back to the registration number, which is what the rows created by an
281
- * assignment rather than an enrolment carry instead of a unit.
282
- */
283
- function subtitleOf(identity: Record<string, unknown>) {
284
- const metadata = toRecord(identity.metadata);
285
- return String(metadata.unitLabel ?? "").trim()
286
- || String(identity.registration ?? "").trim();
287
- }
288
-
289
- async function loadEnrolled() {
290
- if (!readerId.value || !readerLocation.value) return;
291
- loading.value = true;
292
- try {
293
- const response = await getIdentities(readerId.value, {
294
- page: page.value,
295
- limit: 10,
296
- search: search.value.trim(),
297
- // Residents only, and filtered by the SERVER rather than here. The pager's
298
- // count comes back from the same query, so narrowing the list on this side
299
- // would print a total that never matched the rows under it.
300
- type: "resident",
301
- });
302
- const result = readCandidatePage(response);
303
- /*
304
- * EVERY row the server returned, including the ones this screen cannot act
305
- * on. A visitor's access comes from `user_access_rules` rather than a group,
306
- * and an identity linked to nobody has no subject to assign - but both are
307
- * still enrolled on this reader, and the count in the pager counts them. They
308
- * are drawn with the switch disabled and a reason, instead of disappearing.
309
- */
310
- enrolled.value = result.items.map((row) => {
311
- const identity = toRecord(row);
312
- const subject = subjectOf(identity);
313
- const metadata = toRecord(identity.metadata);
314
- return {
315
- subjectId: subject?.subjectId ?? "",
316
- category: subject?.category ?? "resident",
317
- // The row's own key. `subjectId` is empty for the rows below, and two
318
- // empty keys would collapse into one row in the table.
319
- rowKey: String(identity._id ?? identity.hidUserId ?? Math.random()),
320
- assignable: Boolean(subject),
321
- reason: subject ? "" : unassignableReason(),
322
- name: String(metadata.name ?? "").trim()
323
- || String(identity.registration ?? "").trim()
324
- || "—",
325
- subtitle: subtitleOf(identity),
326
- intercom: false,
327
- selected: false,
328
- } as THidPermissionCandidate;
329
- });
330
- // `pages` only. The page number stays whatever the pager set — this
331
- // endpoint's `paginate` never echoes it back, so reading one here pinned
332
- // the screen to page one on every fetch.
333
- pages.value = result.pages;
334
- pageRange.value = result.pageRange;
335
- } catch (error) {
336
- enrolled.value = [];
337
- pages.value = 1;
338
- pageRange.value = "";
339
- showToast(getErrorMessage(error, "Unable to load the people enrolled on this reader."), "error");
340
- } finally {
341
- loading.value = false;
342
- }
343
- }
344
-
345
- function goToPage(value: number) {
346
- page.value = Number(value) || 1;
347
- void loadEnrolled();
348
- }
349
-
350
- function searchFromFirstPage() {
351
- page.value = 1;
352
- void loadEnrolled();
353
- }
354
-
355
- function toggle(item: THidPermissionCandidate, selected: boolean) {
356
- if (!String(item.subjectId ?? "")) return;
357
- draft.value = setAssignment(
358
- draft.value,
359
- savedState.value,
360
- String(item.subjectId ?? ""),
361
- item.category,
362
- selected,
363
- );
364
- }
365
-
366
- async function save() {
367
- if (!dirty.value || saving.value) return;
368
- saving.value = true;
369
- try {
370
- await updateSitePermissions(
371
- props.site,
372
- props.org,
373
- readerId.value,
374
- toAssignmentPayload(draft.value),
375
- );
376
- // The server reconciles the device inside this call: anybody switched off
377
- // loses their `user_groups` row before it returns, so a success here means
378
- // the door has already been updated.
379
- savedState.value = new Map(draft.value);
380
- showToast("HID permissions updated.");
381
- emit("saved");
382
- open.value = false;
383
- } catch (error) {
384
- showToast(getErrorMessage(error, "Unable to update HID permissions."), "error");
385
- } finally {
386
- saving.value = false;
387
- }
388
- }
389
-
390
- function showToast(message: string, color = "success") {
391
- snackbar.message = message;
392
- snackbar.color = color;
393
- snackbar.show = true;
394
- }
395
-
396
- function getErrorMessage(error: unknown, fallback: string) {
397
- const record = toRecord(error);
398
- const data = toRecord(record.data);
399
- return String(data.message ?? "") || String(record.message ?? "") || fallback;
400
- }
401
- </script>
402
-
403
- <style scoped lang="scss">
404
- .hid-access-permissions {
405
- &__title { padding-block: 16px; }
406
-
407
- &__intro { padding: 12px 16px 0; }
408
-
409
- &__body {
410
- padding-top: 8px;
411
- min-height: 320px;
412
- }
413
-
414
- &__actions { padding: 12px 16px; }
415
-
416
- &__person > div:first-child { font-weight: 500; }
417
-
418
- &__toggle {
419
- display: flex;
420
- align-items: center;
421
- gap: 8px;
422
- }
423
- }
424
- </style>