@7365admin1/layer-common 4.90.2-staging.491 → 4.90.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InventoryItemDetail.vue +1 -2
- package/components/InventoryLinesDialog.vue +13 -91
- package/components/InventoryPhotoInput.vue +6 -44
- package/components/InventoryReportsTab.vue +3 -23
- package/components/InventoryRequestsTab.vue +4 -27
- package/components/InventoryStockTab.vue +0 -3
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useInventory.ts +1 -4
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/inventory.d.ts +0 -27
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/InventoryPoDialog.vue +0 -111
- package/components/VehicleQrStickerDialog.vue +0 -249
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/inventory-receiving.ts +0 -90
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- package/utils/vehicle-qr-sticker.ts +0 -184
|
@@ -18,108 +18,50 @@
|
|
|
18
18
|
|
|
19
19
|
<v-card-text class="pa-4">
|
|
20
20
|
<div class="text-caption text-fontgray mb-4">
|
|
21
|
-
|
|
22
|
-
not greyed out with the "Card:" label around it. `text-high-emphasis`
|
|
23
|
-
rather than a literal black: every app has a light/dark toggle
|
|
24
|
-
(plugins/vuetify.ts), and #000 here would be black text on a dark
|
|
25
|
-
panel. It resolves to black in the light theme. -->
|
|
26
|
-
Card:
|
|
27
|
-
<strong class="text-body-2 font-weight-bold text-high-emphasis">{{
|
|
28
|
-
selectedCardInUnit?.cardNo
|
|
29
|
-
}}</strong>
|
|
21
|
+
Card: <strong class="text-body-2">{{ selectedCardInUnit?.cardNo }}</strong>
|
|
30
22
|
</div>
|
|
31
23
|
|
|
32
|
-
<!--
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
dialog, and this stays legible at phone width.
|
|
38
|
-
-->
|
|
39
|
-
<InputLabel title="Assign to" required class="mb-1" />
|
|
40
|
-
<v-select
|
|
41
|
-
v-model="assignCategory"
|
|
42
|
-
:items="assignCategories"
|
|
24
|
+
<!-- Person Type Filter -->
|
|
25
|
+
<!-- <div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">Person Type</div>
|
|
26
|
+
<v-btn-toggle
|
|
27
|
+
v-model="assignPersonType"
|
|
28
|
+
color="primary"
|
|
43
29
|
density="compact"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
hide-details
|
|
30
|
+
variant="outlined"
|
|
31
|
+
rounded="pill"
|
|
47
32
|
class="mb-4"
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
>
|
|
34
|
+
<v-btn value="" size="small" class="text-none px-3">All</v-btn>
|
|
35
|
+
<v-btn value="owner" size="small" class="text-none px-3">Unit Owner</v-btn>
|
|
36
|
+
<v-btn value="resident" size="small" class="text-none px-3">Resident</v-btn>
|
|
37
|
+
<v-btn value="tenant" size="small" class="text-none px-3">Tenant</v-btn>
|
|
38
|
+
</v-btn-toggle> -->
|
|
50
39
|
|
|
51
40
|
<!-- Person Selector -->
|
|
52
41
|
<InputLabel title="Person" required class="mb-1" />
|
|
53
|
-
|
|
54
|
-
An autocomplete rather than a select: a unit's Management and provider
|
|
55
|
-
lists are short, but a site's can run to dozens, and scrolling for a
|
|
56
|
-
name is slow. `custom-filter` widens the match beyond the title,
|
|
57
|
-
because the thing an operator remembers is often the email or the role
|
|
58
|
-
- and a member's name IS frequently their email address.
|
|
59
|
-
-->
|
|
60
|
-
<v-autocomplete
|
|
42
|
+
<v-select
|
|
61
43
|
v-model="assignPerson"
|
|
62
|
-
|
|
63
|
-
:items="assignPeopleItems"
|
|
64
|
-
:custom-filter="filterPerson"
|
|
44
|
+
:items="filteredPeopleItems"
|
|
65
45
|
density="compact"
|
|
66
46
|
item-title="name"
|
|
67
|
-
item-value="
|
|
47
|
+
item-value="_id"
|
|
68
48
|
hide-details="auto"
|
|
69
|
-
placeholder="
|
|
49
|
+
placeholder="Select person..."
|
|
70
50
|
persistent-placeholder
|
|
71
51
|
:loading="peopleLoading"
|
|
72
|
-
:disabled="peopleLoading"
|
|
73
52
|
return-object
|
|
74
|
-
|
|
75
|
-
:no-data-text="assignNoDataText"
|
|
53
|
+
no-data-text="No residents or tenants found for this unit"
|
|
76
54
|
>
|
|
77
|
-
<!--
|
|
78
|
-
Three lines: who they are, their email, and what they are here -
|
|
79
|
-
the member's role, or the provider's line of business. `title` is
|
|
80
|
-
bound rather than left to `itemProps` so the subtitle slot can add
|
|
81
|
-
the third line under it.
|
|
82
|
-
-->
|
|
83
55
|
<template #item="{ props: itemProps, item }">
|
|
84
|
-
<v-list-item v-bind="itemProps" :
|
|
85
|
-
<template v-if="item.raw.subtitle || item.raw.meta" #subtitle>
|
|
86
|
-
<div v-if="item.raw.subtitle" class="text-truncate">
|
|
87
|
-
{{ item.raw.subtitle }}
|
|
88
|
-
</div>
|
|
89
|
-
<div v-if="item.raw.meta" class="text-caption text-medium-emphasis">
|
|
90
|
-
{{ item.raw.meta }}
|
|
91
|
-
</div>
|
|
92
|
-
</template>
|
|
93
|
-
</v-list-item>
|
|
56
|
+
<v-list-item v-bind="itemProps" :subtitle="personTypeLabel(item.raw)" />
|
|
94
57
|
</template>
|
|
95
|
-
<!--
|
|
96
|
-
The same two lines once a person is chosen. A member's name is
|
|
97
|
-
often their email, so a selection showing the name alone would drop
|
|
98
|
-
the one thing that tells two of them apart - their role.
|
|
99
|
-
-->
|
|
100
58
|
<template #selection="{ item }">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class="text-caption text-medium-emphasis text-truncate"
|
|
106
|
-
>
|
|
107
|
-
{{ [item.raw.subtitle, item.raw.meta].filter(Boolean).join(" · ") }}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
59
|
+
<span>{{ item.raw.name }}</span>
|
|
60
|
+
<v-chip size="x-small" class="ml-2" variant="tonal">
|
|
61
|
+
{{ personTypeLabel(item.raw) }}
|
|
62
|
+
</v-chip>
|
|
110
63
|
</template>
|
|
111
|
-
</v-
|
|
112
|
-
|
|
113
|
-
<!--
|
|
114
|
-
A card is assigned by writing the person's USER id onto it, so a row
|
|
115
|
-
the list cannot give one for must not be sent - the server stores
|
|
116
|
-
whatever id it is handed without checking, which would silently bind
|
|
117
|
-
the card to nobody. Data-driven rather than per-group, so this
|
|
118
|
-
disappears by itself once the endpoint carries the id.
|
|
119
|
-
-->
|
|
120
|
-
<p v-if="assignPerson && !assignPerson.assigneeId" class="text-warning text-caption mt-2">
|
|
121
|
-
{{ assignUnassignableText }}
|
|
122
|
-
</p>
|
|
64
|
+
</v-select>
|
|
123
65
|
<p v-if="assignError" class="text-error text-caption mt-2">{{ assignError }}</p>
|
|
124
66
|
</v-card-text>
|
|
125
67
|
|
|
@@ -134,7 +76,7 @@
|
|
|
134
76
|
<v-btn
|
|
135
77
|
variant="flat"
|
|
136
78
|
class="text-none screen-btn-primary"
|
|
137
|
-
:disabled="!assignPerson
|
|
79
|
+
:disabled="!assignPerson || assignLoading"
|
|
138
80
|
:loading="assignLoading"
|
|
139
81
|
@click="confirmAssign"
|
|
140
82
|
>
|
|
@@ -462,104 +404,12 @@ const emit = defineEmits<{
|
|
|
462
404
|
|
|
463
405
|
const { getResidents, assignUser } = useAccessManagement();
|
|
464
406
|
const { getEncryptedAcmUrl } = useSiteEntryPassSettings();
|
|
465
|
-
const { getAll: getMembers } = useMember();
|
|
466
|
-
const { getSiteMembers } = useServiceProvider();
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* The member type of whichever app this layer is running in
|
|
470
|
-
* ("property_management_agency", "security_agency", ...), which `/api/members`
|
|
471
|
-
* requires and rejects empty.
|
|
472
|
-
*
|
|
473
|
-
* Read HERE, during setup, not inside the loader that uses it:
|
|
474
|
-
* `useRuntimeConfig` needs the Nuxt instance, and that is only guaranteed
|
|
475
|
-
* synchronously in setup. Called from an async function resumed after an
|
|
476
|
-
* await — which is exactly what the loader is — it throws "nuxt instance
|
|
477
|
-
* unavailable", the loader's catch turns that into an empty list, and the
|
|
478
|
-
* Management group silently shows nothing with no request ever sent.
|
|
479
|
-
*/
|
|
480
|
-
const APP = String(useRuntimeConfig().public.APP ?? "");
|
|
481
407
|
|
|
482
408
|
const historyDialog = ref(false);
|
|
483
409
|
const showAssignForm = ref(false);
|
|
410
|
+
const assignPersonType = ref<string>("");
|
|
484
411
|
const assignPerson = ref<Record<string, any> | null>(null);
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* WHO A CARD CAN BE GIVEN TO.
|
|
488
|
-
*
|
|
489
|
-
* The dialog only ever listed the unit's residents and tenants. Management
|
|
490
|
-
* (the site's own members) and Service Providers hold cards on a site too, so
|
|
491
|
-
* the operator picks the group first and then the person.
|
|
492
|
-
*
|
|
493
|
-
* `value` is only ever read here; nothing is sent to the server, which takes a
|
|
494
|
-
* flat list of user ids and does not know about these groups.
|
|
495
|
-
*/
|
|
496
|
-
type AssignCategory = "residents" | "management" | "providers";
|
|
497
|
-
|
|
498
|
-
const assignCategories: {
|
|
499
|
-
value: AssignCategory;
|
|
500
|
-
label: string;
|
|
501
|
-
/**
|
|
502
|
-
* What the server stores as the card's `holderType` (core
|
|
503
|
-
* `EAccessCardHolderTypes`). Sent with the assignment, because `assignees`
|
|
504
|
-
* alone is ambiguous: it is a `users` id for a resident and a membership id
|
|
505
|
-
* for a provider, and only the group says which to resolve against. These
|
|
506
|
-
* strings are a server contract - the labels above are free to change, these
|
|
507
|
-
* are not.
|
|
508
|
-
*/
|
|
509
|
-
holderType: "Visitor/Resident" | "Management" | "Service Provider";
|
|
510
|
-
}[] = [
|
|
511
|
-
{ value: "residents", label: "Resident / Tenant", holderType: "Visitor/Resident" },
|
|
512
|
-
{ value: "management", label: "Management", holderType: "Management" },
|
|
513
|
-
{ value: "providers", label: "Service Provider", holderType: "Service Provider" },
|
|
514
|
-
];
|
|
515
|
-
|
|
516
|
-
const assignCategory = ref<AssignCategory>("residents");
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* One row shape for all three groups, so the picker does not care which it is
|
|
520
|
-
* showing.
|
|
521
|
-
*
|
|
522
|
-
* `assigneeId` is the person's USER id - the id `assignUser` sends and the
|
|
523
|
-
* server writes onto the card. Residents already come from the users
|
|
524
|
-
* collection, and a member row carries its `user`. A row that cannot supply
|
|
525
|
-
* one keeps `assigneeId: ""`, which is what disables Assign for it rather than
|
|
526
|
-
* sending an id that binds the card to nobody.
|
|
527
|
-
*/
|
|
528
|
-
type AssignRow = {
|
|
529
|
-
assigneeId: string;
|
|
530
|
-
name: string;
|
|
531
|
-
/** Second line - the email, or what kind of resident this is. */
|
|
532
|
-
subtitle: string;
|
|
533
|
-
/** Third line, under the subtitle - the member's role or the provider's service. */
|
|
534
|
-
meta?: string;
|
|
535
|
-
raw: Record<string, any>;
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const assignPeopleItems = ref<AssignRow[]>([]);
|
|
539
|
-
|
|
540
|
-
/** What is typed in the person picker. Cleared whenever the group changes. */
|
|
541
|
-
const personSearch = ref("");
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Match a person on everything the row shows, not just the name.
|
|
545
|
-
*
|
|
546
|
-
* Vuetify filters on the title alone by default, which is too narrow here: a
|
|
547
|
-
* Management member's name is often their email address, and two provider
|
|
548
|
-
* people from one company are told apart only by their role. Both of those sit
|
|
549
|
-
* in the second line, so a name-only match would hide the row an operator is
|
|
550
|
-
* actually searching for.
|
|
551
|
-
*
|
|
552
|
-
* The first argument is the title Vuetify matched against; it is ignored in
|
|
553
|
-
* favour of the row itself, which carries all three fields.
|
|
554
|
-
*/
|
|
555
|
-
function filterPerson(_title: string, query: string, item: any): boolean {
|
|
556
|
-
const q = String(query ?? "").trim().toLowerCase();
|
|
557
|
-
if (!q) return true;
|
|
558
|
-
const row = item?.raw ?? {};
|
|
559
|
-
return [row.name, row.subtitle, row.meta].some((field) =>
|
|
560
|
-
String(field ?? "").toLowerCase().includes(q),
|
|
561
|
-
);
|
|
562
|
-
}
|
|
412
|
+
const peopleItems = ref<Record<string, any>[]>([]);
|
|
563
413
|
const peopleLoading = ref(false);
|
|
564
414
|
const assignLoading = ref(false);
|
|
565
415
|
const assignError = ref("");
|
|
@@ -602,6 +452,12 @@ const isSelectedCardDeletable = computed(() => {
|
|
|
602
452
|
].some((c) => c._id === id);
|
|
603
453
|
});
|
|
604
454
|
|
|
455
|
+
const filteredPeopleItems = computed(() => {
|
|
456
|
+
if (!assignPersonType.value) return peopleItems.value;
|
|
457
|
+
if (assignPersonType.value === "owner") return peopleItems.value.filter((p) => p.isOwner);
|
|
458
|
+
return peopleItems.value.filter((p) => p.type === assignPersonType.value && !p.isOwner);
|
|
459
|
+
});
|
|
460
|
+
|
|
605
461
|
const personTypeLabel = (person: Record<string, any>) => {
|
|
606
462
|
if (person.isOwner) return "Unit Owner";
|
|
607
463
|
if (person.type === "resident") return "Resident";
|
|
@@ -609,188 +465,34 @@ const personTypeLabel = (person: Record<string, any>) => {
|
|
|
609
465
|
return person.type ?? "";
|
|
610
466
|
};
|
|
611
467
|
|
|
612
|
-
const assignNoDataText = computed(() => {
|
|
613
|
-
if (peopleLoading.value) return "Loading...";
|
|
614
|
-
if (assignCategory.value === "management") return "No management members assigned to this unit";
|
|
615
|
-
if (assignCategory.value === "providers") return "No service provider members assigned to this unit";
|
|
616
|
-
return "No residents or tenants found for this unit";
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
const assignUnassignableText = computed(
|
|
620
|
-
() => "This person has no account on record, so a card cannot be assigned to them.",
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
/** Residents and tenants of THIS UNIT - the list the dialog has always shown. */
|
|
624
|
-
async function loadResidentRows(): Promise<AssignRow[]> {
|
|
625
|
-
if (!props.unit?._id || !props.orgId || !props.siteId) return [];
|
|
626
|
-
const res = await getResidents({
|
|
627
|
-
orgId: props.orgId,
|
|
628
|
-
siteId: props.siteId,
|
|
629
|
-
unitId: props.unit._id,
|
|
630
|
-
});
|
|
631
|
-
const rows = res?.data ?? [];
|
|
632
|
-
return rows.map((person: Record<string, any>) => ({
|
|
633
|
-
// Already the users collection's own `_id` - see core's residents repo.
|
|
634
|
-
assigneeId: String(person._id ?? ""),
|
|
635
|
-
name: person.name ?? "",
|
|
636
|
-
subtitle: person.email ?? "",
|
|
637
|
-
meta: personTypeLabel(person),
|
|
638
|
-
raw: person,
|
|
639
|
-
}));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Management - the site's own members ATTACHED TO THIS UNIT.
|
|
644
|
-
*
|
|
645
|
-
* `unit` comes from core #2515 (Unit Management tab), which gave members a
|
|
646
|
-
* `unit`/`unitName` and taught `GET /api/members` to filter on it. Narrowing
|
|
647
|
-
* to the unit matches what the card is: it belongs to a unit, so the people
|
|
648
|
-
* offered are the ones that unit has.
|
|
649
|
-
*
|
|
650
|
-
* `type` is the member type of whichever app this layer is running in
|
|
651
|
-
* (`property_management_agency`, `security_agency`, ...) - the endpoint
|
|
652
|
-
* requires it and rejects an empty string, and `limit` is capped at 50 there.
|
|
653
|
-
*/
|
|
654
|
-
async function loadManagementRows(): Promise<AssignRow[]> {
|
|
655
|
-
if (!props.orgId || !props.siteId || !props.unit?._id) return [];
|
|
656
|
-
const res: any = await getMembers({
|
|
657
|
-
org: props.orgId,
|
|
658
|
-
siteId: props.siteId,
|
|
659
|
-
unit: props.unit._id,
|
|
660
|
-
type: APP,
|
|
661
|
-
status: "active",
|
|
662
|
-
page: 1,
|
|
663
|
-
limit: 50,
|
|
664
|
-
});
|
|
665
|
-
const rows: Record<string, any>[] = res?.items ?? res?.data ?? [];
|
|
666
|
-
return rows.map((member) => ({
|
|
667
|
-
// `user` is the account; `_id` is the membership record and is NOT it.
|
|
668
|
-
assigneeId: String(member.user ?? ""),
|
|
669
|
-
name: member.name ?? member.email ?? "",
|
|
670
|
-
// One line under the name: "Access Mgmt Only · towefa4573@aghism.com".
|
|
671
|
-
// The role first, because that is what distinguishes two members of the
|
|
672
|
-
// same unit from each other. Either half is dropped if the row lacks it,
|
|
673
|
-
// so the separator never dangles.
|
|
674
|
-
subtitle: [member.roleName, member.email].filter(Boolean).join(" · "),
|
|
675
|
-
raw: member,
|
|
676
|
-
}));
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Service Providers - the provider PEOPLE attached to this unit.
|
|
681
|
-
*
|
|
682
|
-
* `GET /api/service-providers/site-members?siteId&unit=<id>`. A card is held by
|
|
683
|
-
* a person, so this lists provider staff rather than the companies
|
|
684
|
-
* `GET /api/service-providers` returns, and the `unit` filter keeps it to the
|
|
685
|
-
* people that unit actually has - the same scoping Management uses.
|
|
686
|
-
*
|
|
687
|
-
* The response also carries a `companies` summary; only `items` is people.
|
|
688
|
-
*/
|
|
689
|
-
async function loadProviderRows(): Promise<AssignRow[]> {
|
|
690
|
-
if (!props.siteId || !props.unit?._id) return [];
|
|
691
|
-
const res = await getSiteMembers(props.siteId, props.unit._id);
|
|
692
|
-
const rows = res?.items ?? [];
|
|
693
|
-
return rows.map((member: Record<string, any>) => ({
|
|
694
|
-
/*
|
|
695
|
-
* `user` when the endpoint returns it, otherwise the MEMBERSHIP `_id`.
|
|
696
|
-
*
|
|
697
|
-
* `site-members` does not carry `user` today (the repository projects it,
|
|
698
|
-
* the service layer drops it), and the product decision is to assign on
|
|
699
|
-
* `_id` rather than wait. Worth knowing what that means: the card's
|
|
700
|
-
* `userId` then holds a `service-provider-members` id, so the card list's
|
|
701
|
-
* `userId -> users` lookup finds nothing and the holder column reads blank
|
|
702
|
-
* for these cards. The door access itself is unaffected - the ACM command
|
|
703
|
-
* is built from the CARD's id, not the assignee's.
|
|
704
|
-
*
|
|
705
|
-
* `user` is preferred so that the day it starts arriving, these cards
|
|
706
|
-
* begin resolving to real people with no change here.
|
|
707
|
-
*/
|
|
708
|
-
assigneeId: String(member.user ?? member._id ?? ""),
|
|
709
|
-
name: member.name || member.email || "",
|
|
710
|
-
subtitle: member.email ?? "",
|
|
711
|
-
/*
|
|
712
|
-
* The service the company provides, plus this person's role - two people
|
|
713
|
-
* from the same company differ only by role ("VMS Access" vs "Security
|
|
714
|
-
* Access"). `typeLabel` is computed by the endpoint; the local converter is
|
|
715
|
-
* the fallback for a row that predates it.
|
|
716
|
-
*/
|
|
717
|
-
meta:
|
|
718
|
-
[member.typeLabel || serviceTypeLabel(member.type), member.role]
|
|
719
|
-
.filter(Boolean)
|
|
720
|
-
.join(" · "),
|
|
721
|
-
raw: member,
|
|
722
|
-
}));
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/** Load the chosen group, dropping a response that a later switch superseded. */
|
|
726
|
-
let assignLoadSeq = 0;
|
|
727
|
-
async function loadAssignPeople() {
|
|
728
|
-
const seq = ++assignLoadSeq;
|
|
729
|
-
const category = assignCategory.value;
|
|
730
|
-
peopleLoading.value = true;
|
|
731
|
-
assignPeopleItems.value = [];
|
|
732
|
-
try {
|
|
733
|
-
const rows =
|
|
734
|
-
category === "management"
|
|
735
|
-
? await loadManagementRows()
|
|
736
|
-
: category === "providers"
|
|
737
|
-
? await loadProviderRows()
|
|
738
|
-
: await loadResidentRows();
|
|
739
|
-
if (seq !== assignLoadSeq) return;
|
|
740
|
-
assignPeopleItems.value = rows;
|
|
741
|
-
} catch (e: any) {
|
|
742
|
-
if (seq !== assignLoadSeq) return;
|
|
743
|
-
assignPeopleItems.value = [];
|
|
744
|
-
/*
|
|
745
|
-
* Say that the list failed rather than showing an empty one.
|
|
746
|
-
*
|
|
747
|
-
* This swallowed everything, so a group whose request 400'd, or never got
|
|
748
|
-
* sent at all, looked exactly like a group with nobody in it - there was
|
|
749
|
-
* nothing on screen to tell the two apart, and no way to tell from the UI
|
|
750
|
-
* that anything had gone wrong.
|
|
751
|
-
*/
|
|
752
|
-
assignError.value =
|
|
753
|
-
e?.data?.message ?? e?.message ?? "Could not load the list. Please try again.";
|
|
754
|
-
} finally {
|
|
755
|
-
if (seq === assignLoadSeq) peopleLoading.value = false;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function onAssignCategoryChange() {
|
|
760
|
-
assignPerson.value = null;
|
|
761
|
-
// A term typed for the previous group means nothing in the next one, and
|
|
762
|
-
// leaving it would show an empty list over a list that has people in it.
|
|
763
|
-
personSearch.value = "";
|
|
764
|
-
assignError.value = "";
|
|
765
|
-
loadAssignPeople();
|
|
766
|
-
}
|
|
767
|
-
|
|
768
468
|
async function openAssignForm() {
|
|
769
469
|
showAssignForm.value = true;
|
|
770
470
|
assignPerson.value = null;
|
|
771
|
-
|
|
772
|
-
assignCategory.value = "residents";
|
|
471
|
+
assignPersonType.value = "";
|
|
773
472
|
assignError.value = "";
|
|
774
|
-
|
|
473
|
+
peopleItems.value = [];
|
|
775
474
|
encryptedAcmUrl.value = "";
|
|
776
475
|
|
|
777
476
|
if (!props.unit?._id || !props.orgId || !props.siteId) return;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
477
|
+
peopleLoading.value = true;
|
|
478
|
+
try {
|
|
479
|
+
const [residentsRes, acmUrl] = await Promise.all([
|
|
480
|
+
getResidents({ orgId: props.orgId, siteId: props.siteId, unitId: props.unit._id }),
|
|
481
|
+
getEncryptedAcmUrl(props.siteId),
|
|
482
|
+
]);
|
|
483
|
+
peopleItems.value = residentsRes?.data ?? [];
|
|
484
|
+
encryptedAcmUrl.value = acmUrl ?? "";
|
|
485
|
+
} catch {
|
|
486
|
+
peopleItems.value = [];
|
|
487
|
+
} finally {
|
|
488
|
+
peopleLoading.value = false;
|
|
489
|
+
}
|
|
786
490
|
}
|
|
787
491
|
|
|
788
492
|
function cancelAssign() {
|
|
789
493
|
showAssignForm.value = false;
|
|
790
494
|
assignPerson.value = null;
|
|
791
|
-
|
|
792
|
-
assignCategory.value = "residents";
|
|
793
|
-
assignPeopleItems.value = [];
|
|
495
|
+
assignPersonType.value = "";
|
|
794
496
|
assignError.value = "";
|
|
795
497
|
}
|
|
796
498
|
|
|
@@ -805,28 +507,20 @@ const selectedCardType = computed(() => {
|
|
|
805
507
|
});
|
|
806
508
|
|
|
807
509
|
async function confirmAssign() {
|
|
808
|
-
|
|
809
|
-
// lists and only this field is the user account the card is written to. The
|
|
810
|
-
// Assign button is already disabled without one; this is the guard behind it.
|
|
811
|
-
const assigneeId = assignPerson.value?.assigneeId;
|
|
812
|
-
if (!assigneeId || !props.unit?._id) return;
|
|
510
|
+
if (!assignPerson.value || !props.unit?._id) return;
|
|
813
511
|
assignLoading.value = true;
|
|
814
512
|
assignError.value = "";
|
|
815
513
|
try {
|
|
816
514
|
await assignUser({
|
|
817
|
-
assignees: [
|
|
515
|
+
assignees: [assignPerson.value._id],
|
|
818
516
|
unit: props.unit._id,
|
|
819
517
|
type: selectedCardType.value,
|
|
820
518
|
acm_url: encryptedAcmUrl.value,
|
|
821
519
|
id: props.selectedCardInUnit?._id,
|
|
822
|
-
holderType: assignCategories.find((c) => c.value === assignCategory.value)
|
|
823
|
-
?.holderType,
|
|
824
520
|
});
|
|
825
521
|
emit("assign-to-person", {
|
|
826
522
|
card: props.selectedCardInUnit,
|
|
827
|
-
|
|
828
|
-
// shape they always have.
|
|
829
|
-
person: assignPerson.value?.raw ?? assignPerson.value,
|
|
523
|
+
person: assignPerson.value,
|
|
830
524
|
});
|
|
831
525
|
showToast("Card assigned successfully.", "success");
|
|
832
526
|
cancelAssign();
|
|
@@ -850,15 +544,3 @@ function toggleCard(card: Record<string, any>) {
|
|
|
850
544
|
);
|
|
851
545
|
}
|
|
852
546
|
</script>
|
|
853
|
-
|
|
854
|
-
<style scoped>
|
|
855
|
-
/* The chosen person, two lines inside the field. `min-width: 0` is what lets
|
|
856
|
-
`text-truncate` work at all here - the selection sits in a flex row, and a
|
|
857
|
-
flex child will not shrink below its content without it, so a long email
|
|
858
|
-
would push the field wide instead of ellipsing. */
|
|
859
|
-
.assign-selection {
|
|
860
|
-
min-width: 0;
|
|
861
|
-
line-height: 1.25;
|
|
862
|
-
padding-block: 2px;
|
|
863
|
-
}
|
|
864
|
-
</style>
|