@7365admin1/layer-common 4.35.1-staging.384 → 4.36.0

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 (76) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +0 -43
  3. package/assets/css/robot-hero.css +2 -2
  4. package/components/AccessCardAddForm.vue +4 -5
  5. package/components/AccessCardAssignToUnitForm.vue +2 -3
  6. package/components/AccessCardPreviewDialog.vue +3 -4
  7. package/components/AppSelect.vue +3 -77
  8. package/components/CameraForm.vue +6 -20
  9. package/components/DashboardMain.vue +60 -66
  10. package/components/Dialog/UpdateMoreAction.vue +1 -11
  11. package/components/DocumentForm.vue +112 -407
  12. package/components/DocumentManagement.vue +12 -213
  13. package/components/EntryPass/QrTemplatePreview.vue +25 -21
  14. package/components/EntryPassInformation.vue +24 -119
  15. package/components/Facility/BookingSetup.vue +0 -63
  16. package/components/HidAccessLogDashboard.vue +17 -74
  17. package/components/HidIntercomManagement.vue +14 -180
  18. package/components/HidQrCodeConfiguration.vue +195 -17
  19. package/components/HidReaderManagement.vue +0 -398
  20. package/components/HidReaderUserRoster.vue +3 -44
  21. package/components/HidUserEnrollment.vue +155 -845
  22. package/components/InvitationClientForm.vue +1 -19
  23. package/components/Layout/NavigationDrawer.vue +1 -4
  24. package/components/NotificationBell.vue +7 -59
  25. package/components/Robot/BuildingMap.vue +9 -16
  26. package/components/Robot/LiftQueue.vue +4 -9
  27. package/components/Robot/MapEditor.vue +8 -1
  28. package/components/RolePermissionFormCreate.vue +3 -4
  29. package/components/RolePermissionFormPreviewUpdate.vue +5 -12
  30. package/components/RolePermissionMain.vue +0 -1
  31. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  32. package/components/ServiceProviderMain.vue +30 -178
  33. package/components/VisitorForm.vue +0 -12
  34. package/components/VisitorManagement.vue +10 -285
  35. package/components/VisitorSocketPopUp.vue +1 -2
  36. package/composables/useANPRSettings.ts +15 -45
  37. package/composables/useAccessManagement.ts +0 -5
  38. package/composables/useCleaningPermission.ts +0 -2
  39. package/composables/useCommonPermission.ts +0 -32
  40. package/composables/useDocument.ts +0 -7
  41. package/composables/useFacility.ts +0 -11
  42. package/composables/useHidAmico.ts +0 -159
  43. package/composables/useHidNavigation.ts +1 -25
  44. package/composables/usePeople.ts +0 -19
  45. package/composables/usePropertyPermission.ts +0 -19
  46. package/composables/useRobotBuilding.ts +3 -22
  47. package/composables/useRobots.ts +0 -13
  48. package/composables/useSecurityPermission.ts +0 -19
  49. package/composables/useServiceProvider.ts +0 -3
  50. package/composables/useServiceProviderInvitation.ts +3 -101
  51. package/composables/useSiteEntryPassSettings.ts +0 -26
  52. package/composables/useWebUsb.ts +11 -14
  53. package/package.json +2 -2
  54. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  55. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  56. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  57. package/plugins/API.ts +0 -11
  58. package/types/document.d.ts +0 -14
  59. package/types/facility.d.ts +0 -4
  60. package/types/people.d.ts +1 -28
  61. package/utils/module-routes.ts +1 -8
  62. package/utils/nav-sections.ts +2 -8
  63. package/utils/notification-target.ts +8 -277
  64. package/utils/permission-labels.ts +2 -20
  65. package/components/ClientInvitations.vue +0 -291
  66. package/components/HidAccessPermissions.vue +0 -424
  67. package/components/HidProfileQrCode.vue +0 -332
  68. package/composables/useHidReaderSelection.ts +0 -50
  69. package/composables/useSiteCategory.ts +0 -45
  70. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  71. package/utils/dashboard-widget-prefs.ts +0 -54
  72. package/utils/hid-permission-assignments.ts +0 -190
  73. package/utils/hid-reader-selection.ts +0 -52
  74. package/utils/occupancy-role.ts +0 -109
  75. package/utils/robot-fleet-core.ts +0 -57
  76. package/utils/sp-inviter-row.ts +0 -86
@@ -376,7 +376,6 @@ const visitorRecords = ref<Record<string, HidAccessRecord>>({});
376
376
  const logs = ref<HidAccessRecord[]>([]);
377
377
  const liveAccessLogs = ref<HidAccessRecord[]>([]);
378
378
  const administratorUserIds = ref<Set<number>>(new Set());
379
- const { selectHidReader, resolveForSite } = useHidReaderSelection();
380
379
  const selectedReaderId = ref("");
381
380
  const activeTab = ref<AccessTab>("all");
382
381
  const search = ref("");
@@ -402,10 +401,7 @@ const exportForm = reactive({
402
401
  });
403
402
 
404
403
  const statusOptions = ["Authorized", "Not Authorized", "Unknown"];
405
- const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card", "Intercom"];
406
-
407
- /** The persisted event the resident app records when it opens a door on a call. */
408
- const INTERCOM_DOOR_OPEN_EVENT = "intercom_door_open";
404
+ const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card"];
409
405
  const accessMethodOptions = ["All", ...accessMethodFilterOptions];
410
406
 
411
407
  const readerOptions = computed(() =>
@@ -593,11 +589,9 @@ async function loadReaders() {
593
589
  if (!props.site) return;
594
590
  const response = await getReaders({ site: props.site, page: 1, limit: 100 });
595
591
  readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
596
- /* Shared with every other HID screen — see `useHidReaderSelection`. The
597
- resolver refuses a remembered reader that is not this site's, so a site
598
- switch or a deleted reader falls back to the first rather than querying
599
- an id the site does not own. */
600
- selectedReaderId.value = resolveForSite(props.site, readers.value);
592
+ if (!selectedReaderId.value && readers.value.length) {
593
+ selectedReaderId.value = readers.value[0]._id;
594
+ }
601
595
  }
602
596
 
603
597
  async function loadDashboard() {
@@ -612,7 +606,7 @@ async function loadDashboard() {
612
606
  resetDashboardData();
613
607
  try {
614
608
  const isVisitorTab = activeTab.value === "visitor";
615
- const [logResponse, intercomLogResponse, hidAccessLogResponse, visitorResponse, readerUserResponse] = await Promise.all([
609
+ const [logResponse, hidAccessLogResponse, visitorResponse, readerUserResponse] = await Promise.all([
616
610
  loadDashboardSource(
617
611
  getLogs(selectedReaderId.value, {
618
612
  page: page.value,
@@ -621,20 +615,6 @@ async function loadDashboard() {
621
615
  null,
622
616
  "persisted access logs",
623
617
  ),
624
- // Door opens from the resident app during an intercom call. The reader
625
- // never logs a DTMF door open, so these exist only as persisted events
626
- // and are fetched by type: the general page above is shared with every
627
- // other event the reader emits and is dropped whenever the reader has
628
- // live logs, which it always has.
629
- loadDashboardSource(
630
- getLogs(selectedReaderId.value, {
631
- page: page.value,
632
- limit,
633
- type: INTERCOM_DOOR_OPEN_EVENT,
634
- }),
635
- null,
636
- "intercom door-open logs",
637
- ),
638
618
  loadDashboardSource(
639
619
  getReaderAccessLogs(selectedReaderId.value, {
640
620
  page: page.value,
@@ -701,14 +681,9 @@ async function loadDashboard() {
701
681
  // identity metadata here: it can contain a stale photo from another user.
702
682
  allIdentities.value = readerUsers;
703
683
  identities.value = readerUsers;
704
- const intercomItems = intercomLogResponse?.items ?? intercomLogResponse?.data?.items ?? intercomLogResponse?.data?.logs ?? [];
705
- logs.value = [
706
- ...(liveItems.length
707
- ? []
708
- : logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? []),
709
- // Always kept: the reader has no live counterpart for these.
710
- ...intercomItems,
711
- ];
684
+ logs.value = liveItems.length
685
+ ? []
686
+ : logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
712
687
  liveAccessLogs.value = liveItems;
713
688
  serverPages.value = Number(hidAccessLogResponse?.pages ?? hidAccessLogResponse?.data?.pages ?? 1);
714
689
  serverPageRange.value = String(
@@ -844,8 +819,6 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
844
819
  id_password: "ID and Password",
845
820
  pin: "PIN",
846
821
  card: "Card",
847
- // Set by the API on intercom_door_open events; no reader field implies it.
848
- intercom: "Intercom",
849
822
  };
850
823
  const normalizedMethod = String(payload.normalizedMethod || "");
851
824
  if (normalizedMethodLabels[normalizedMethod]) return normalizedMethodLabels[normalizedMethod];
@@ -860,36 +833,11 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
860
833
  return identity.metadata?.facialData || identity.metadata?.imageTimestamp ? "Facial" : "N/A";
861
834
  }
862
835
 
863
- /**
864
- * One shape for the whole Access Date and Time column: `15:18:43 23/9/2026`.
865
- *
866
- * Reader rows arrive as a number and went through `formatHidEpochDate`; an
867
- * intercom door open never touches the reader, so the API writes it as an ISO
868
- * string and this branch took `toLocaleString` instead - which put
869
- * "9/23/2026, 2:15:29 PM" in the same column, one row below "15:18:43
870
- * 23/9/2026". Both branches now build the same string.
871
- *
872
- * Local parts here, UTC parts there, on purpose: the reader encodes its own
873
- * wall clock as if it were UTC, while an ISO string is a real instant. Reading
874
- * each the way it was written is what makes both columns show the time that was
875
- * on the clock at the door.
876
- */
877
836
  function formatDate(value?: string | number) {
878
837
  if (!value) return "N/A";
879
838
  if (typeof value === "number") return formatHidEpochDate(value);
880
- const date = new Date(value);
881
- if (Number.isNaN(date.getTime())) return "N/A";
882
- return formatClock(
883
- [date.getHours(), date.getMinutes(), date.getSeconds()],
884
- date.getDate(),
885
- date.getMonth() + 1,
886
- date.getFullYear(),
887
- );
888
- }
889
-
890
- function formatClock(time: number[], day: number, month: number, year: number) {
891
- const clock = time.map((part) => String(part).padStart(2, "0")).join(":");
892
- return `${clock} ${day}/${month}/${year}`;
839
+ const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
840
+ return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
893
841
  }
894
842
 
895
843
  function parseDate(value?: string | number) {
@@ -919,12 +867,13 @@ function formatHidEpochDate(value: number) {
919
867
  const date = new Date(value * 1000);
920
868
  if (Number.isNaN(date.getTime())) return "N/A";
921
869
 
922
- return formatClock(
923
- [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
924
- date.getUTCDate(),
925
- date.getUTCMonth() + 1,
926
- date.getUTCFullYear(),
927
- );
870
+ const time = [
871
+ date.getUTCHours(),
872
+ date.getUTCMinutes(),
873
+ date.getUTCSeconds(),
874
+ ].map((part) => String(part).padStart(2, "0")).join(":");
875
+
876
+ return `${time} ${date.getUTCDate()}/${date.getUTCMonth() + 1}/${date.getUTCFullYear()}`;
928
877
  }
929
878
 
930
879
  function openHistoryDialog(row: AccessRow) {
@@ -1599,12 +1548,6 @@ function escapeHtml(value: unknown) {
1599
1548
  .replace(/"/g, "&quot;")
1600
1549
  .replace(/'/g, "&#039;");
1601
1550
  }
1602
-
1603
- /* Publish every change, including the seed above: a screen that had to fall
1604
- back to the first reader should leave the others agreeing with it. */
1605
- watch(selectedReaderId, (value) => {
1606
- if (value) selectHidReader(props.site, value);
1607
- });
1608
1551
  </script>
1609
1552
 
1610
1553
  <style scoped>
@@ -162,8 +162,11 @@
162
162
  />
163
163
  <v-list-item class="screen-menu__item" title="Call Settings" prepend-icon="mdi-cog" @click="openCallDialog(item)" />
164
164
  <v-list-item class="screen-menu__item" title="Dial Code" prepend-icon="mdi-dialpad" @click="openDialCodeDialog(item)" />
165
+ <v-list-item class="screen-menu__item" title="Make a call" prepend-icon="mdi-phone" @click="openMakeCallDialog(item)" />
166
+ <v-list-item class="screen-menu__item" title="Web call" prepend-icon="mdi-headset" @click="openWebPhoneDialog(item)" />
165
167
  </v-list>
166
168
  <v-list v-else density="compact" class="screen-menu">
169
+ <v-list-item class="screen-menu__item" title="Call" prepend-icon="mdi-phone" @click="openContactCall(item)" />
167
170
  <v-list-item class="screen-menu__item" title="Check reader SIP status" prepend-icon="mdi-lan-check" @click="testContactSipStatus(item)" />
168
171
  <v-list-item class="screen-menu__item" title="View" prepend-icon="mdi-eye" @click="openContactView(item)" />
169
172
  <v-list-item class="screen-menu__item" title="Edit" prepend-icon="mdi-pencil" @click="openContactDialog(item)" />
@@ -586,36 +589,9 @@
586
589
  :disabled="Boolean(contactForm.id)"
587
590
  />
588
591
  <FieldLabel text="Name" required />
589
- <!-- The name is picked from the site's SIP accounts (`hid-sip-accounts`)
590
- rather than typed, so a contact always names a person who holds an
591
- extension. The model stays the plain name string the reader stores,
592
- so `saveContact` is unchanged; an existing contact whose name is not
593
- in the list still displays it (AppSelect falls back to the raw value). -->
594
- <!-- Searchable: this is the one list on the screen that grows with
595
- the estate, and the unit is part of every option's text, so the
596
- box finds a person by name or by where they live. -->
597
- <AppSelect
598
- v-model="contactForm.name"
599
- :items="sipAccountOptions"
600
- :disabled="sipAccountsLoading"
601
- :placeholder="sipAccountsLoading ? 'Loading SIP accounts…' : 'Name shown on calls'"
602
- searchable
603
- search-placeholder="Search name or unit"
604
- class="my-3"
605
- @update:model-value="applySipAccountToContact"
606
- />
607
- <p v-if="sipAccountsError" class="field-hint field-hint--error">{{ sipAccountsError }}</p>
608
- <p v-else-if="!sipAccountsLoading && !sipAccounts.length" class="field-hint">
609
- No SIP accounts on this site yet. An account is created the first time a user opens Web Call.
610
- </p>
592
+ <AppField v-model="contactForm.name" placeholder="Name shown on calls" class="my-3" />
611
593
  <FieldLabel text="Address" required />
612
- <!-- Always read-only: it shows the extension of the chosen SIP account
613
- and is never typed into. -->
614
- <AppField
615
- v-model="contactForm.serverAddress"
616
- :placeholder="contactAddressPlaceholder"
617
- disabled
618
- />
594
+ <AppField v-model="contactForm.serverAddress" :placeholder="contactAddressPlaceholder" />
619
595
  <p class="field-hint">{{ contactAddressHint }}</p>
620
596
  </v-card-text>
621
597
  <v-card-actions class="dialog-actions pa-0">
@@ -660,8 +636,6 @@
660
636
  </template>
661
637
 
662
638
  <script setup lang="ts">
663
- import type { HidSipAccountListItem } from "../composables/useHidAmico";
664
-
665
639
  const props = defineProps({
666
640
  org: {
667
641
  type: String,
@@ -758,8 +732,6 @@ const {
758
732
  makeIntercomCall,
759
733
  finalizeIntercomCall,
760
734
  ensureSipAccount,
761
- getSipAccounts,
762
- notifyIntercomCall,
763
735
  } = useHidAmico();
764
736
 
765
737
  const activeTab = ref<IntercomTab>("intercom");
@@ -788,11 +760,6 @@ const webPhoneActionLoading = ref(false);
788
760
  const sipAccount = ref<SipAccount | null>(null);
789
761
  const sipAccountLoading = ref(false);
790
762
  const sipAccountError = ref("");
791
- // The site's SIP account list for the contact dialog's name picker. Distinct
792
- // from `sipAccount` above, which is the CALLER's own account for the web phone.
793
- const sipAccounts = ref<HidSipAccountListItem[]>([]);
794
- const sipAccountsLoading = ref(false);
795
- const sipAccountsError = ref("");
796
763
  const remoteVideoEl = ref<HTMLVideoElement | null>(null);
797
764
  const remoteAudioEl = ref<HTMLAudioElement | null>(null);
798
765
  const webPhone = useSipWebPhone();
@@ -897,58 +864,6 @@ const readerOptions = computed(() => readers.value.map((reader) => ({
897
864
  value: reader._id,
898
865
  })));
899
866
 
900
- /**
901
- * "Harris - Blk 1/Level 1/Unit 1" - the name a contact is stored under.
902
- *
903
- * The unit belongs in the name itself, not only in the picker's label: the
904
- * reader's contact list is what a visitor reads off the door panel, and
905
- * "Harris" alone does not say which home it rings. An account whose owner has
906
- * no address, and every account until the API ships the `unitLabel` field,
907
- * falls back to the bare name rather than trailing a separator.
908
- */
909
- function buildContactName(account: HidSipAccountListItem) {
910
- const unit = String(account.unitLabel ?? "").trim();
911
- return unit ? `${account.name} - ${unit}` : account.name;
912
- }
913
-
914
- /**
915
- * The contact name picker. `value` is the composed name because that is what
916
- * the reader stores and what `saveContact` sends; the extension goes into the
917
- * Address field, not the label. A name that already sits on the contact being
918
- * edited but is no longer in the SIP list is kept as an option so the picker
919
- * does not silently drop it.
920
- */
921
- const sipAccountOptions = computed(() => {
922
- const options: Array<{ title: string; value: string }> = sipAccounts.value
923
- .filter((account: HidSipAccountListItem) => account.name)
924
- .map((account: HidSipAccountListItem) => {
925
- const name = buildContactName(account);
926
- return { title: name, value: name };
927
- });
928
- const current = contactForm.name.trim();
929
- if (current && !options.some((option) => option.value === current)) {
930
- options.unshift({ title: current, value: current });
931
- }
932
- return options;
933
- });
934
-
935
- /**
936
- * The SIP account behind a stored contact name, or null when the name is not
937
- * one. The bare name is matched as well as the composed one, so a contact
938
- * created before the unit was part of the name still resolves to its account
939
- * on edit instead of looking unrecognised.
940
- */
941
- function findSipAccount(name: string) {
942
- const value = name.trim();
943
- if (!value) return null;
944
- const match = sipAccounts.value.find((account: HidSipAccountListItem) => buildContactName(account) === value)
945
- ?? sipAccounts.value.find((account: HidSipAccountListItem) => account.name === value);
946
- return (match ?? null) as HidSipAccountListItem | null;
947
- }
948
-
949
- /** The SIP account behind the picked name, or null for a legacy / unpicked name. */
950
- const selectedSipAccount = computed(() => findSipAccount(contactForm.name));
951
-
952
867
  const visibleRows = computed(() => activeTab.value === "intercom" ? filteredIntercomRows.value : filteredContacts.value);
953
868
  const pages = computed(() => Math.max(1, Math.ceil(visibleRows.value.length / limit)));
954
869
  const pageRange = computed(() => {
@@ -1061,11 +976,14 @@ const contactReaderRow = computed(() =>
1061
976
  );
1062
977
  const isContactReaderPeerToPeer = computed(() => contactReaderRow.value?.operatingMode === "Peer-to-Peer");
1063
978
  const contactAddressPlaceholder = computed(() => (isContactReaderPeerToPeer.value ? "192.168.0.129" : "7100"));
1064
- const contactAddressHint = computed(() => (
1065
- selectedSipAccount.value
1066
- ? "Extension of the selected SIP account."
1067
- : "Filled from the SIP account of the name selected above."
1068
- ));
979
+ const contactAddressHint = computed(() => {
980
+ if (!contactReaderRow.value) {
981
+ return "Extension number (SIP Client) or local IP address (Peer-to-Peer), depending on the reader's operating mode.";
982
+ }
983
+ return isContactReaderPeerToPeer.value
984
+ ? "Peer-to-Peer mode: enter the contact's local IP address."
985
+ : "SIP Client mode: enter the contact's extension number.";
986
+ });
1069
987
  const areSipCredentialFieldsDisabled = computed(() => !sipForm.enabled || isPeerToPeerMode.value);
1070
988
  const intercomDetailSections = computed(() => {
1071
989
  const row = selectedRow.value;
@@ -1481,37 +1399,6 @@ async function loadSipAccount() {
1481
1399
  }
1482
1400
  }
1483
1401
 
1484
- async function loadSipAccounts() {
1485
- if (!props.org || !props.site) return;
1486
-
1487
- sipAccountsLoading.value = true;
1488
- sipAccountsError.value = "";
1489
- try {
1490
- const response = await getSipAccounts(props.site, props.org);
1491
- const rows = toRecord(response).data;
1492
- sipAccounts.value = Array.isArray(rows) ? rows as HidSipAccountListItem[] : [];
1493
- // An existing contact opened before the list arrived: once its name is
1494
- // matched to an account, show that account's extension in the locked field.
1495
- applySipAccountToContact(contactForm.name.trim());
1496
- } catch (error: unknown) {
1497
- sipAccounts.value = [];
1498
- sipAccountsError.value = getErrorMessage(error);
1499
- } finally {
1500
- sipAccountsLoading.value = false;
1501
- }
1502
- }
1503
-
1504
- /**
1505
- * Picking a SIP account sets the address to its extension. The Address field
1506
- * is always disabled, so this is the only way it gets a value; a name that is
1507
- * not an account (a legacy contact) leaves whatever address it already had.
1508
- */
1509
- function applySipAccountToContact(name: string) {
1510
- const account = findSipAccount(name);
1511
- if (!account) return;
1512
- contactForm.serverAddress = account.extension;
1513
- }
1514
-
1515
1402
  async function connectWebPhone() {
1516
1403
  const account = sipAccount.value;
1517
1404
  if (!account) {
@@ -1555,10 +1442,8 @@ async function disconnectWebPhone() {
1555
1442
 
1556
1443
  async function startWebCall() {
1557
1444
  webPhoneActionLoading.value = true;
1558
- const destination = webPhoneForm.destination;
1559
1445
  try {
1560
- await webPhone.call(destination);
1561
- void ringResidentPhone(destination);
1446
+ await webPhone.call(webPhoneForm.destination);
1562
1447
  } catch (error: unknown) {
1563
1448
  showMessage(getErrorMessage(error), "error");
1564
1449
  } finally {
@@ -1566,50 +1451,6 @@ async function startWebCall() {
1566
1451
  }
1567
1452
  }
1568
1453
 
1569
- /**
1570
- * Wake the called resident's mobile so they are there to answer.
1571
- *
1572
- * Deliberately fired AFTER the INVITE and never awaited. The SIP leg is the
1573
- * call: it is already ringing by the time this runs, and a resident whose
1574
- * phone is asleep is the only thing this fixes. So a failure here is logged
1575
- * and dropped rather than shown — telling the operator "call failed" for a
1576
- * call that is ringing would be worse than a silent miss.
1577
- *
1578
- * A destination with no resident behind it (a staff extension, an external SIP
1579
- * address) comes back `notified: false` with a reason and is not an error.
1580
- */
1581
- async function ringResidentPhone(destination: string) {
1582
- if (!props.org || !props.site) return;
1583
-
1584
- try {
1585
- // No `readerId`: this call came from the web phone, not a door station.
1586
- // The reader selected in the table above is whatever the operator happens
1587
- // to be looking at, and sending it would tell the resident's app the call
1588
- // arrived at a door it never touched.
1589
- await notifyIntercomCall(props.site, {
1590
- orgId: props.org,
1591
- destination,
1592
- callId: createCallId(),
1593
- });
1594
- } catch (error: unknown) {
1595
- console.warn("[intercom] could not send the incoming-call push", error);
1596
- }
1597
- }
1598
-
1599
- /**
1600
- * A per-call id, so a second push for the same call replaces the first on the
1601
- * resident's lock screen rather than stacking a second ringing card.
1602
- * `randomUUID` needs a secure context, which the web phone already requires —
1603
- * getUserMedia does not run without one — but the fallback keeps a call from
1604
- * failing on that account alone.
1605
- */
1606
- function createCallId() {
1607
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1608
- return crypto.randomUUID();
1609
- }
1610
- return `call-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1611
- }
1612
-
1613
1454
  async function answerWebCall() {
1614
1455
  webPhoneActionLoading.value = true;
1615
1456
  try {
@@ -1667,9 +1508,6 @@ function openContactDialog(contact?: ContactRow) {
1667
1508
  contactForm.location = contact?.location === "N/A" ? "" : contact?.location || selectedReader?.location || "";
1668
1509
  contactForm.serverAddress = contact?.serverAddress === "N/A" ? "" : contact?.serverAddress || "";
1669
1510
  contactDialog.value = true;
1670
- // Fetched on every open, not cached: an account is provisioned the first
1671
- // time a user opens Web Call, so the list can grow between two dialogs.
1672
- void loadSipAccounts();
1673
1511
  }
1674
1512
 
1675
1513
  function openContactView(contact: ContactRow) {
@@ -2258,10 +2096,6 @@ export default {
2258
2096
  line-height: 1.4;
2259
2097
  }
2260
2098
 
2261
- .field-hint--error {
2262
- color: var(--err);
2263
- }
2264
-
2265
2099
  .sip-settings-dialog .field-label {
2266
2100
  margin-top: 14px;
2267
2101
  }