@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
- <v-select
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="Select person..."
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-select>
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: { value: AssignCategory; label: string }[] = [
475
- { value: "residents", label: "Resident / Tenant" },
476
- { value: "management", label: "Management" },
477
- { value: "providers", label: "Service Provider" },
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: String(useRuntimeConfig().public.APP ?? ""),
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
- function assignUser(payload: { assignees: string[]; unit: string; type: string; acm_url: string; id?: string }) {
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.485",
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.",