@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.
- package/CHANGELOG.md +10 -4
- package/assets/css/primitives.css +0 -43
- package/assets/css/robot-hero.css +2 -2
- package/components/AccessCardAddForm.vue +4 -5
- package/components/AccessCardAssignToUnitForm.vue +2 -3
- package/components/AccessCardPreviewDialog.vue +3 -4
- package/components/AppSelect.vue +3 -77
- package/components/CameraForm.vue +6 -20
- package/components/DashboardMain.vue +60 -66
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/EntryPass/QrTemplatePreview.vue +25 -21
- package/components/EntryPassInformation.vue +24 -119
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -180
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +155 -845
- package/components/InvitationClientForm.vue +1 -19
- package/components/Layout/NavigationDrawer.vue +1 -4
- package/components/NotificationBell.vue +7 -59
- package/components/Robot/BuildingMap.vue +9 -16
- package/components/Robot/LiftQueue.vue +4 -9
- package/components/Robot/MapEditor.vue +8 -1
- package/components/RolePermissionFormCreate.vue +3 -4
- package/components/RolePermissionFormPreviewUpdate.vue +5 -12
- package/components/RolePermissionMain.vue +0 -1
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +30 -178
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +10 -285
- package/components/VisitorSocketPopUp.vue +1 -2
- package/composables/useANPRSettings.ts +15 -45
- package/composables/useAccessManagement.ts +0 -5
- package/composables/useCleaningPermission.ts +0 -2
- package/composables/useCommonPermission.ts +0 -32
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -159
- package/composables/useHidNavigation.ts +1 -25
- package/composables/usePeople.ts +0 -19
- package/composables/usePropertyPermission.ts +0 -19
- package/composables/useRobotBuilding.ts +3 -22
- package/composables/useRobots.ts +0 -13
- package/composables/useSecurityPermission.ts +0 -19
- package/composables/useServiceProvider.ts +0 -3
- package/composables/useServiceProviderInvitation.ts +3 -101
- package/composables/useSiteEntryPassSettings.ts +0 -26
- package/composables/useWebUsb.ts +11 -14
- package/package.json +2 -2
- 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/plugins/API.ts +0 -11
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/utils/module-routes.ts +1 -8
- package/utils/nav-sections.ts +2 -8
- package/utils/notification-target.ts +8 -277
- package/utils/permission-labels.ts +2 -20
- package/components/ClientInvitations.vue +0 -291
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- 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/dashboard-widget-prefs.ts +0 -54
- package/utils/hid-permission-assignments.ts +0 -190
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/occupancy-role.ts +0 -109
- package/utils/robot-fleet-core.ts +0 -57
- 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"
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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,
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
date.
|
|
925
|
-
date.
|
|
926
|
-
|
|
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, """)
|
|
1600
1549
|
.replace(/'/g, "'");
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
}
|