@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +16 -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/SiteSettings.vue +0 -48
  28. package/components/TableMain.vue +7 -22
  29. package/components/VehicleManagement.vue +5 -80
  30. package/components/VisitorForm.vue +0 -12
  31. package/components/VisitorManagement.vue +5 -267
  32. package/composables/useAccessManagement.ts +1 -17
  33. package/composables/useDocument.ts +0 -7
  34. package/composables/useFacility.ts +0 -11
  35. package/composables/useHidAmico.ts +0 -165
  36. package/composables/useHidNavigation.ts +1 -25
  37. package/composables/useInventory.ts +1 -4
  38. package/composables/useMember.ts +0 -12
  39. package/composables/useNFCPatrolReportFilters.ts +3 -21
  40. package/composables/useNFCPatrolRoute.ts +1 -8
  41. package/composables/usePeople.ts +0 -19
  42. package/composables/useServiceProvider.ts +2 -45
  43. package/composables/useSettingsPermission.ts +0 -14
  44. package/composables/useUser.ts +2 -6
  45. package/composables/useVehicle.ts +1 -40
  46. package/composables/useVerification.ts +0 -3
  47. package/middleware/01.auth.ts +7 -3
  48. package/package.json +1 -1
  49. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  50. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  51. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  52. package/types/document.d.ts +0 -14
  53. package/types/facility.d.ts +0 -4
  54. package/types/inventory.d.ts +0 -27
  55. package/types/member.d.ts +0 -4
  56. package/types/people.d.ts +1 -28
  57. package/types/service-provider.d.ts +0 -5
  58. package/types/site.d.ts +1 -7
  59. package/components/BuildingUnitManagement.vue +0 -298
  60. package/components/HidAccessPermissions.vue +0 -424
  61. package/components/HidProfileQrCode.vue +0 -332
  62. package/components/InventoryPoDialog.vue +0 -111
  63. package/components/VehicleQrStickerDialog.vue +0 -249
  64. package/composables/useHidReaderSelection.ts +0 -50
  65. package/composables/useSiteCategory.ts +0 -45
  66. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  67. package/utils/hid-enrolment-subject.ts +0 -344
  68. package/utils/hid-permission-assignments.ts +0 -198
  69. package/utils/hid-reader-selection.ts +0 -52
  70. package/utils/inventory-receiving.ts +0 -90
  71. package/utils/invite-group.ts +0 -61
  72. package/utils/invite-sites.ts +0 -25
  73. package/utils/occupancy-role.ts +0 -109
  74. package/utils/service-type.ts +0 -30
  75. 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
- <!-- The card number is the one fact this dialog is about, so it is
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
- WHO the card is being given to. The dialog only ever offered the
34
- unit's residents and tenants; Management and Service Providers are
35
- the other two groups of people who hold cards on a site. A select
36
- rather than a button toggle: three labels this long overflow a 420px
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
- item-title="label"
45
- item-value="value"
46
- hide-details
30
+ variant="outlined"
31
+ rounded="pill"
47
32
  class="mb-4"
48
- @update:model-value="onAssignCategoryChange"
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
- v-model:search="personSearch"
63
- :items="assignPeopleItems"
64
- :custom-filter="filterPerson"
44
+ :items="filteredPeopleItems"
65
45
  density="compact"
66
46
  item-title="name"
67
- item-value="assigneeId"
47
+ item-value="_id"
68
48
  hide-details="auto"
69
- placeholder="Search or select person..."
49
+ placeholder="Select person..."
70
50
  persistent-placeholder
71
51
  :loading="peopleLoading"
72
- :disabled="peopleLoading"
73
52
  return-object
74
- :hide-no-data="false"
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" :title="item.raw.name">
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
- <div class="assign-selection">
102
- <div class="text-truncate">{{ item.raw.name }}</div>
103
- <div
104
- v-if="item.raw.subtitle || item.raw.meta"
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-autocomplete>
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?.assigneeId || assignLoading"
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
- personSearch.value = "";
772
- assignCategory.value = "residents";
471
+ assignPersonType.value = "";
773
472
  assignError.value = "";
774
- assignPeopleItems.value = [];
473
+ peopleItems.value = [];
775
474
  encryptedAcmUrl.value = "";
776
475
 
777
476
  if (!props.unit?._id || !props.orgId || !props.siteId) return;
778
-
779
- // The ACM URL is needed whichever group the card goes to, so it is fetched
780
- // once here rather than with each list.
781
- getEncryptedAcmUrl(props.siteId)
782
- .then((url) => { encryptedAcmUrl.value = url ?? ""; })
783
- .catch(() => { encryptedAcmUrl.value = ""; });
784
-
785
- await loadAssignPeople();
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
- personSearch.value = "";
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
- // `assigneeId` rather than `_id`: the rows now come from three different
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: [assigneeId],
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
- // The original record, not the picker's row, so listeners see the same
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>