@7365admin1/layer-common 4.90.2-staging.485 → 4.90.2-staging.486
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.
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
<!-- Visitor / Resident Credentials -->
|
|
69
69
|
<template v-if="details.userCred">
|
|
70
70
|
<v-divider />
|
|
71
|
-
<div class="card-details__section">
|
|
71
|
+
<!-- <div class="card-details__section">
|
|
72
72
|
{{ details.userType ?? "Visitor Information" }}
|
|
73
|
-
</div>
|
|
73
|
+
</div> -->
|
|
74
74
|
|
|
75
75
|
<div v-if="details.userCred.email">
|
|
76
76
|
<div class="card-details__label">Email</div>
|
|
@@ -50,18 +50,28 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- Person Selector -->
|
|
52
52
|
<InputLabel title="Person" required class="mb-1" />
|
|
53
|
-
|
|
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
|
|
54
61
|
v-model="assignPerson"
|
|
62
|
+
v-model:search="personSearch"
|
|
55
63
|
:items="assignPeopleItems"
|
|
64
|
+
:custom-filter="filterPerson"
|
|
56
65
|
density="compact"
|
|
57
66
|
item-title="name"
|
|
58
67
|
item-value="assigneeId"
|
|
59
68
|
hide-details="auto"
|
|
60
|
-
placeholder="
|
|
69
|
+
placeholder="Search or select person..."
|
|
61
70
|
persistent-placeholder
|
|
62
71
|
:loading="peopleLoading"
|
|
63
72
|
:disabled="peopleLoading"
|
|
64
73
|
return-object
|
|
74
|
+
:hide-no-data="false"
|
|
65
75
|
:no-data-text="assignNoDataText"
|
|
66
76
|
>
|
|
67
77
|
<!--
|
|
@@ -98,7 +108,7 @@
|
|
|
98
108
|
</div>
|
|
99
109
|
</div>
|
|
100
110
|
</template>
|
|
101
|
-
</v-
|
|
111
|
+
</v-autocomplete>
|
|
102
112
|
|
|
103
113
|
<!--
|
|
104
114
|
A card is assigned by writing the person's USER id onto it, so a row
|
|
@@ -455,6 +465,20 @@ const { getEncryptedAcmUrl } = useSiteEntryPassSettings();
|
|
|
455
465
|
const { getAll: getMembers } = useMember();
|
|
456
466
|
const { getSiteMembers } = useServiceProvider();
|
|
457
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
|
+
|
|
458
482
|
const historyDialog = ref(false);
|
|
459
483
|
const showAssignForm = ref(false);
|
|
460
484
|
const assignPerson = ref<Record<string, any> | null>(null);
|
|
@@ -471,10 +495,22 @@ const assignPerson = ref<Record<string, any> | null>(null);
|
|
|
471
495
|
*/
|
|
472
496
|
type AssignCategory = "residents" | "management" | "providers";
|
|
473
497
|
|
|
474
|
-
const assignCategories: {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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" },
|
|
478
514
|
];
|
|
479
515
|
|
|
480
516
|
const assignCategory = ref<AssignCategory>("residents");
|
|
@@ -500,6 +536,30 @@ type AssignRow = {
|
|
|
500
536
|
};
|
|
501
537
|
|
|
502
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
|
+
}
|
|
503
563
|
const peopleLoading = ref(false);
|
|
504
564
|
const assignLoading = ref(false);
|
|
505
565
|
const assignError = ref("");
|
|
@@ -597,7 +657,7 @@ async function loadManagementRows(): Promise<AssignRow[]> {
|
|
|
597
657
|
org: props.orgId,
|
|
598
658
|
siteId: props.siteId,
|
|
599
659
|
unit: props.unit._id,
|
|
600
|
-
type:
|
|
660
|
+
type: APP,
|
|
601
661
|
status: "active",
|
|
602
662
|
page: 1,
|
|
603
663
|
limit: 50,
|
|
@@ -678,9 +738,19 @@ async function loadAssignPeople() {
|
|
|
678
738
|
: await loadResidentRows();
|
|
679
739
|
if (seq !== assignLoadSeq) return;
|
|
680
740
|
assignPeopleItems.value = rows;
|
|
681
|
-
} catch {
|
|
741
|
+
} catch (e: any) {
|
|
682
742
|
if (seq !== assignLoadSeq) return;
|
|
683
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.";
|
|
684
754
|
} finally {
|
|
685
755
|
if (seq === assignLoadSeq) peopleLoading.value = false;
|
|
686
756
|
}
|
|
@@ -688,6 +758,9 @@ async function loadAssignPeople() {
|
|
|
688
758
|
|
|
689
759
|
function onAssignCategoryChange() {
|
|
690
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 = "";
|
|
691
764
|
assignError.value = "";
|
|
692
765
|
loadAssignPeople();
|
|
693
766
|
}
|
|
@@ -695,6 +768,7 @@ function onAssignCategoryChange() {
|
|
|
695
768
|
async function openAssignForm() {
|
|
696
769
|
showAssignForm.value = true;
|
|
697
770
|
assignPerson.value = null;
|
|
771
|
+
personSearch.value = "";
|
|
698
772
|
assignCategory.value = "residents";
|
|
699
773
|
assignError.value = "";
|
|
700
774
|
assignPeopleItems.value = [];
|
|
@@ -714,6 +788,7 @@ async function openAssignForm() {
|
|
|
714
788
|
function cancelAssign() {
|
|
715
789
|
showAssignForm.value = false;
|
|
716
790
|
assignPerson.value = null;
|
|
791
|
+
personSearch.value = "";
|
|
717
792
|
assignCategory.value = "residents";
|
|
718
793
|
assignPeopleItems.value = [];
|
|
719
794
|
assignError.value = "";
|
|
@@ -744,6 +819,8 @@ async function confirmAssign() {
|
|
|
744
819
|
type: selectedCardType.value,
|
|
745
820
|
acm_url: encryptedAcmUrl.value,
|
|
746
821
|
id: props.selectedCardInUnit?._id,
|
|
822
|
+
holderType: assignCategories.find((c) => c.value === assignCategory.value)
|
|
823
|
+
?.holderType,
|
|
747
824
|
});
|
|
748
825
|
emit("assign-to-person", {
|
|
749
826
|
card: props.selectedCardInUnit,
|
|
@@ -319,7 +319,23 @@ export default function useAccessManagement() {
|
|
|
319
319
|
);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
/**
|
|
323
|
+
* `holderType` is the GROUP the card was given from — "Visitor/Resident",
|
|
324
|
+
* "Management" or "Service Provider" (core `EAccessCardHolderTypes`).
|
|
325
|
+
*
|
|
326
|
+
* It is what lets the server resolve the holder afterwards: `assignees`
|
|
327
|
+
* carries a `users` id for a resident, but a membership id for a service
|
|
328
|
+
* provider, and only the group says which collection to look in. Omit it and
|
|
329
|
+
* the card stores `holderType: null`, so card details cannot name the holder.
|
|
330
|
+
*/
|
|
331
|
+
function assignUser(payload: {
|
|
332
|
+
assignees: string[];
|
|
333
|
+
unit: string;
|
|
334
|
+
type: string;
|
|
335
|
+
acm_url: string;
|
|
336
|
+
id?: string;
|
|
337
|
+
holderType?: "Visitor/Resident" | "Management" | "Service Provider";
|
|
338
|
+
}) {
|
|
323
339
|
return useNuxtApp().$api<Record<string, any>>(
|
|
324
340
|
`/api/access-management/assign-user`,
|
|
325
341
|
{ method: "POST", body: payload }
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.90.2-staging.
|
|
5
|
+
"version": "4.90.2-staging.486",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|