@7365admin1/layer-common 3.2.2-staging.180 → 3.2.2-staging.182

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.
@@ -76,7 +76,7 @@
76
76
  class="app-tab"
77
77
  :class="{ 'app-tab--active': activeTab === tab.value }"
78
78
  :aria-selected="activeTab === tab.value"
79
- @click="activeTab = tab.value"
79
+ @click="selectTab(tab.value)"
80
80
  >
81
81
  {{ tab.label }}
82
82
  </button>
@@ -94,7 +94,12 @@
94
94
  @update:model-value="page = 1"
95
95
  />
96
96
  <AppField v-model="search" search placeholder="Search" />
97
- <AppSelect v-model="statusFilter" :items="statusOptions" />
97
+ <AppSelect
98
+ v-model="statusFilter"
99
+ :items="statusOptions"
100
+ placeholder="Status"
101
+ clearable
102
+ />
98
103
  </div>
99
104
  </template>
100
105
 
@@ -126,6 +131,18 @@
126
131
  <StatusChip :status="item.status" />
127
132
  </template>
128
133
 
134
+ <template #[`item.contactStatus`]="{ item }">
135
+ <v-btn
136
+ size="small"
137
+ variant="outlined"
138
+ class="text-none"
139
+ :loading="testingContactReaderId === item.readerId"
140
+ @click="testContactSipStatus(item)"
141
+ >
142
+ {{ getContactReaderStatus(item) }}
143
+ </v-btn>
144
+ </template>
145
+
129
146
  <template #[`item.action-table`]="{ item }">
130
147
  <v-menu location="bottom end">
131
148
  <template #activator="{ props: menuProps }">
@@ -133,13 +150,19 @@
133
150
  </template>
134
151
  <v-list v-if="activeTab === 'intercom'" density="compact" class="row-menu">
135
152
  <v-list-item title="View" @click="openViewDialog(item)" />
136
- <v-list-item title="SIP Settings" @click="openSipDialog(item)" />
153
+ <v-list-item
154
+ title="Test Connection with SIP"
155
+ :disabled="testingSipReaderId === item.reader._id"
156
+ @click="testSipConnection(item)"
157
+ />
137
158
  <v-list-item title="Call Settings" @click="openCallDialog(item)" />
138
159
  <v-list-item title="Dial Code" @click="openDialCodeDialog(item)" />
139
160
  <v-list-item title="Make a call" @click="openMakeCallDialog(item)" />
140
161
  <v-list-item title="Web call" @click="openWebPhoneDialog(item)" />
141
162
  </v-list>
142
163
  <v-list v-else density="compact" class="row-menu">
164
+ <v-list-item title="Call" prepend-icon="mdi-phone" @click="openContactCall(item)" />
165
+ <v-list-item title="Check reader SIP status" prepend-icon="mdi-lan-check" @click="testContactSipStatus(item)" />
143
166
  <v-list-item title="View" @click="openContactView(item)" />
144
167
  <v-list-item title="Edit" @click="openContactDialog(item)" />
145
168
  <v-list-item title="Delete" @click="openDeleteContact(item)" />
@@ -554,12 +577,16 @@
554
577
  <v-card class="form-dialog" rounded="lg">
555
578
  <v-card-title class="dialog-title">{{ contactForm.id ? 'Edit Contact' : 'Add Contact' }}</v-card-title>
556
579
  <v-card-text class="dialog-body">
580
+ <FieldLabel text="HID Reader" required />
581
+ <AppSelect
582
+ v-model="contactForm.readerId"
583
+ :items="readerOptions"
584
+ :disabled="Boolean(contactForm.id)"
585
+ />
557
586
  <FieldLabel text="Name" required />
558
587
  <AppField v-model="contactForm.name" placeholder="Enter name" />
559
- <FieldLabel text="Location" />
560
- <AppField v-model="contactForm.location" placeholder="Location" />
561
- <FieldLabel text="Server Address" required />
562
- <AppField v-model="contactForm.serverAddress" placeholder="192.168.1.1" />
588
+ <FieldLabel text="SIP Number / Address" required />
589
+ <AppField v-model="contactForm.serverAddress" placeholder="7100 or user@example.com" />
563
590
  </v-card-text>
564
591
  <v-card-actions class="dialog-actions pa-0">
565
592
  <v-btn class="text-none action-cancel" variant="flat" height="44" @click="contactDialog = false">Cancel</v-btn>
@@ -693,15 +720,19 @@ const {
693
720
  const activeTab = ref<IntercomTab>("intercom");
694
721
  const readers = ref<Record<string, any>[]>([]);
695
722
  const selectedReaderId = ref("");
723
+ const testingSipReaderId = ref("");
724
+ const testingContactReaderId = ref("");
696
725
  const intercomRows = ref<IntercomRow[]>([]);
697
726
  const contacts = ref<ContactRow[]>([]);
727
+ const intercomLoaded = ref(false);
728
+ const contactsLoaded = ref(false);
698
729
  const loading = ref(false);
699
730
  const saving = ref(false);
700
731
  const contactsSaving = ref(false);
701
732
  const calling = ref(false);
702
733
  const hangingUp = ref(false);
703
734
  const search = ref("");
704
- const statusFilter = ref("Status");
735
+ const statusFilter = ref("");
705
736
  const page = ref(1);
706
737
  const limit = 20;
707
738
  const selectedRow = ref<IntercomRow | null>(null);
@@ -728,7 +759,7 @@ const deleteContactDialog = ref(false);
728
759
 
729
760
  const message = reactive<{ type: MessageType; text: string }>({ type: "info", text: "" });
730
761
 
731
- const statusOptions = ["Status", "Connected", "Not connected", "Connecting", "Disabled", "Failed"];
762
+ const statusOptions = ["Connected", "Not connected", "Connecting", "Disabled", "Failed"];
732
763
 
733
764
  const pjsipKeys = [
734
765
  "enabled",
@@ -839,7 +870,7 @@ const filteredIntercomRows = computed(() => {
839
870
  row.dialCode,
840
871
  row.status,
841
872
  ].some((value) => String(value || "").toLowerCase().includes(query));
842
- const matchesStatus = statusFilter.value === "Status" || row.status === statusFilter.value;
873
+ const matchesStatus = !statusFilter.value || row.status === statusFilter.value;
843
874
  return matchesReader && matchesSearch && matchesStatus;
844
875
  });
845
876
  });
@@ -848,13 +879,16 @@ const filteredContacts = computed(() => {
848
879
  const query = search.value.trim().toLowerCase();
849
880
  return contacts.value.filter((contact) => {
850
881
  const matchesReader = !selectedReaderId.value || contact.readerId === selectedReaderId.value;
882
+ const readerStatus = getContactReaderStatus(contact);
851
883
  const matchesSearch = !query || [
852
884
  contact.name,
853
885
  contact.location,
854
886
  contact.serverAddress,
855
887
  contact.dialCode,
888
+ readerStatus,
856
889
  ].some((value) => String(value || "").toLowerCase().includes(query));
857
- return matchesReader && matchesSearch;
890
+ const matchesStatus = !statusFilter.value || readerStatus === statusFilter.value;
891
+ return matchesReader && matchesSearch && matchesStatus;
858
892
  });
859
893
  });
860
894
 
@@ -890,8 +924,8 @@ const tableHeaders = computed(() =>
890
924
  : [
891
925
  { title: "Name", value: "name", sortable: false },
892
926
  { title: "Location", value: "location", sortable: false },
893
- { title: "SIP Server Address", value: "serverAddress", sortable: false },
894
- { title: "Dial Code", value: "dialCode", sortable: false },
927
+ { title: "SIP Number / Address", value: "serverAddress", sortable: false },
928
+ { title: "Reader SIP Status", value: "contactStatus", sortable: false },
895
929
  { title: "", value: "action-table", sortable: false },
896
930
  ]
897
931
  );
@@ -1034,8 +1068,9 @@ async function reload() {
1034
1068
  if (!readers.value.some((reader) => reader._id === selectedReaderId.value)) {
1035
1069
  selectedReaderId.value = readers.value[0]?._id || "";
1036
1070
  }
1037
- await loadIntercomRows();
1038
- await loadContacts();
1071
+ intercomLoaded.value = false;
1072
+ contactsLoaded.value = false;
1073
+ await loadActiveTab();
1039
1074
  } catch (error: any) {
1040
1075
  showMessage(getErrorMessage(error), "error");
1041
1076
  } finally {
@@ -1043,10 +1078,36 @@ async function reload() {
1043
1078
  }
1044
1079
  }
1045
1080
 
1046
- async function loadIntercomRows() {
1047
- const rows: IntercomRow[] = [];
1081
+ async function loadActiveTab() {
1082
+ if (activeTab.value === "intercom") {
1083
+ await loadIntercomRows();
1084
+ return;
1085
+ }
1086
+
1087
+ await Promise.all([
1088
+ intercomLoaded.value ? Promise.resolve() : loadIntercomRows(),
1089
+ loadContacts(),
1090
+ ]);
1091
+ }
1048
1092
 
1049
- for (const reader of readers.value) {
1093
+ async function selectTab(tab: IntercomTab) {
1094
+ activeTab.value = tab;
1095
+ page.value = 1;
1096
+
1097
+ if ((tab === "intercom" && intercomLoaded.value) || (tab === "contacts" && contactsLoaded.value)) return;
1098
+
1099
+ loading.value = true;
1100
+ try {
1101
+ await loadActiveTab();
1102
+ } catch (error: unknown) {
1103
+ showMessage(getErrorMessage(error), "error");
1104
+ } finally {
1105
+ loading.value = false;
1106
+ }
1107
+ }
1108
+
1109
+ async function loadIntercomRows() {
1110
+ const rows = await Promise.all(readers.value.map(async (reader): Promise<IntercomRow> => {
1050
1111
  const [configurationResult, statusResult] = await Promise.allSettled([
1051
1112
  getReaderConfiguration(reader._id, { pjsip: pjsipKeys }),
1052
1113
  getIntercomStatus(reader._id),
@@ -1058,16 +1119,15 @@ async function loadIntercomRows() {
1058
1119
  const statusPayload = statusResponse?.data ?? statusResponse;
1059
1120
  const pjsip = normalizePjsip(configurationResponse, statusPayload);
1060
1121
  const status = normalizeStatus(statusPayload, pjsip);
1061
- rows.push(toIntercomRow(reader, pjsip, status));
1062
- }
1122
+ return toIntercomRow(reader, pjsip, status);
1123
+ }));
1063
1124
 
1064
1125
  intercomRows.value = rows;
1126
+ intercomLoaded.value = true;
1065
1127
  }
1066
1128
 
1067
1129
  async function loadContacts() {
1068
- const rows: ContactRow[] = [];
1069
-
1070
- for (const reader of readers.value) {
1130
+ const readerContacts = await Promise.all(readers.value.map(async (reader): Promise<ContactRow[]> => {
1071
1131
  try {
1072
1132
  const response = await runObjectOperation(reader._id, {
1073
1133
  operation: "load",
@@ -1075,13 +1135,15 @@ async function loadContacts() {
1075
1135
  fields: ["id", "name", "number"],
1076
1136
  limit: 100,
1077
1137
  });
1078
- rows.push(...normalizeContacts(reader, response?.data ?? response));
1138
+ return normalizeContacts(reader, response?.data ?? response);
1079
1139
  } catch {
1080
1140
  // Some firmware builds may not expose contacts until intercom is configured.
1141
+ return [];
1081
1142
  }
1082
- }
1143
+ }));
1083
1144
 
1084
- contacts.value = rows;
1145
+ contacts.value = readerContacts.flat();
1146
+ contactsLoaded.value = true;
1085
1147
  }
1086
1148
 
1087
1149
  function paginate<T>(items: T[]) {
@@ -1398,6 +1460,39 @@ function openContactView(contact: ContactRow) {
1398
1460
  contactViewDialog.value = true;
1399
1461
  }
1400
1462
 
1463
+ function getContactReaderStatus(contact: ContactRow) {
1464
+ return intercomRows.value.find((row) => row.reader._id === contact.readerId)?.status || "Check status";
1465
+ }
1466
+
1467
+ async function testContactSipStatus(contact: ContactRow) {
1468
+ const reader = intercomRows.value.find((row) => row.reader._id === contact.readerId);
1469
+ if (!reader) {
1470
+ showMessage("The HID reader for this contact is not available.", "error");
1471
+ return;
1472
+ }
1473
+
1474
+ testingContactReaderId.value = contact.readerId;
1475
+ try {
1476
+ await testSipConnection(reader);
1477
+ } finally {
1478
+ testingContactReaderId.value = "";
1479
+ }
1480
+ }
1481
+
1482
+ function openContactCall(contact: ContactRow) {
1483
+ const reader = intercomRows.value.find((row) => row.reader._id === contact.readerId);
1484
+ if (!reader) {
1485
+ showMessage("The HID reader for this contact is not available.", "error");
1486
+ return;
1487
+ }
1488
+
1489
+ selectedRow.value = reader;
1490
+ makeCallForm.target = contact.serverAddress === "N/A" ? "" : contact.serverAddress;
1491
+ makeCallForm.status = "";
1492
+ makeCallForm.statusType = "info";
1493
+ makeCallDialog.value = true;
1494
+ }
1495
+
1401
1496
  function openDeleteContact(contact: ContactRow) {
1402
1497
  selectedContact.value = contact;
1403
1498
  deleteContactDialog.value = true;
@@ -1517,6 +1612,52 @@ async function runHangupCall() {
1517
1612
  }
1518
1613
  }
1519
1614
 
1615
+ async function testSipConnection(row: IntercomRow) {
1616
+ const readerId = toText(row.reader._id);
1617
+ if (!readerId || testingSipReaderId.value) return;
1618
+
1619
+ testingSipReaderId.value = readerId;
1620
+ try {
1621
+ const response = await getIntercomStatus(readerId);
1622
+ const responseRecord = toRecord(response);
1623
+ const statusPayload = toRecord(responseRecord.data || responseRecord);
1624
+ const status = Number(statusPayload.status);
1625
+
1626
+ await reload();
1627
+
1628
+ if (status === 200) {
1629
+ showMessage(`${row.name}: SIP is connected.`, "success");
1630
+ return;
1631
+ }
1632
+ if (status === -1) {
1633
+ showMessage(`${row.name}: SIP is disabled on the HID reader.`, "warning");
1634
+ return;
1635
+ }
1636
+ if (status === 0 || status === 100) {
1637
+ showMessage(`${row.name}: SIP is still connecting. Check the SIP settings on the HID reader.`, "warning");
1638
+ return;
1639
+ }
1640
+ if (status === 401 || status === 403) {
1641
+ showMessage(`${row.name}: SIP authentication failed. Check the account configured on the HID reader.`, "error");
1642
+ return;
1643
+ }
1644
+ if (status === 408) {
1645
+ showMessage(`${row.name}: The HID reader cannot reach the SIP server.`, "error");
1646
+ return;
1647
+ }
1648
+ if (status === 503) {
1649
+ showMessage(`${row.name}: The HID reader reported a SIP network failure.`, "error");
1650
+ return;
1651
+ }
1652
+
1653
+ showMessage(`${row.name}: SIP is not connected. Configure SIP on the HID reader and test again.`, "warning");
1654
+ } catch (error: unknown) {
1655
+ showMessage(getErrorMessage(error), "error");
1656
+ } finally {
1657
+ testingSipReaderId.value = "";
1658
+ }
1659
+ }
1660
+
1520
1661
  async function savePjsip(readerId: string, payload: Record<string, string>, options: { showSuccessMessage?: boolean } = {}) {
1521
1662
  saving.value = true;
1522
1663
  try {
@@ -1540,6 +1681,11 @@ async function saveContact() {
1540
1681
  return;
1541
1682
  }
1542
1683
 
1684
+ if (!isSipCallTarget(contactForm.serverAddress)) {
1685
+ showMessage("Enter a valid extension, SIP address, or IPv4 address.", "warning");
1686
+ return;
1687
+ }
1688
+
1543
1689
  contactsSaving.value = true;
1544
1690
  try {
1545
1691
  if (contactForm.id) {
@@ -705,7 +705,11 @@ async function loadReaderConfiguration() {
705
705
  try {
706
706
  const response = await getReaderConfiguration(form.readerId, {
707
707
  general: ["online"],
708
- identifier: ["pin_identification_enabled", "multi_factor_authentication"],
708
+ identifier: [
709
+ "card_identification_enabled",
710
+ "pin_identification_enabled",
711
+ "multi_factor_authentication",
712
+ ],
709
713
  });
710
714
  const configuration = getConfigurationData(response);
711
715
  const general = isUnknownRecord(configuration.general) ? configuration.general : {};
@@ -719,6 +723,11 @@ async function loadReaderConfiguration() {
719
723
  form.identificationMethods.pin = pinEnabled;
720
724
  form.identificationMethods.idPassword = !pinEnabled;
721
725
  }
726
+ if (identifier.card_identification_enabled !== undefined) {
727
+ form.identificationMethods.card = isEnabledConfiguration(
728
+ identifier.card_identification_enabled,
729
+ );
730
+ }
722
731
  } catch (error: unknown) {
723
732
  console.warn("Unable to load HID reader configuration:", errorConverter(error));
724
733
  }
@@ -790,7 +799,13 @@ async function saveIdentificationMethod(method: HidIdentificationMethod) {
790
799
 
791
800
  savingSection.value = `method-${method}`;
792
801
  try {
793
- if (method === "pin" || method === "idPassword") {
802
+ if (method === "card") {
803
+ await setReaderConfiguration(form.readerId, {
804
+ identifier: {
805
+ card_identification_enabled: form.identificationMethods.card ? "1" : "0",
806
+ },
807
+ });
808
+ } else if (method === "pin" || method === "idPassword") {
794
809
  if (method === "pin" && form.identificationMethods.pin) {
795
810
  form.identificationMethods.idPassword = false;
796
811
  } else if (method === "idPassword" && form.identificationMethods.idPassword) {
@@ -38,7 +38,12 @@
38
38
  <template #extension>
39
39
  <div class="app-filter-row hid-reader-filters">
40
40
  <AppField v-model="search" search placeholder="Search" />
41
- <AppSelect v-model="status" :items="statusOptions" />
41
+ <AppSelect
42
+ v-model="status"
43
+ :items="statusOptions"
44
+ placeholder="Status"
45
+ clearable
46
+ />
42
47
  </div>
43
48
  </template>
44
49
 
@@ -256,7 +261,7 @@ const {
256
261
 
257
262
  const readers = ref<Record<string, any>[]>([]);
258
263
  const search = ref("");
259
- const status = ref("Status");
264
+ const status = ref("");
260
265
  const loading = ref(false);
261
266
  const saving = ref(false);
262
267
  const actionLoading = ref(false);
@@ -280,7 +285,11 @@ const syncPreviewStats = ref({
280
285
  });
281
286
  const resultStats = ref<typeof syncPreviewStats.value | null>(null);
282
287
 
283
- const statusOptions = ["Status", "active", "inactive", "deleted", "offline"];
288
+ const statusOptions = [
289
+ { title: "Active", value: "active" },
290
+ { title: "Inactive", value: "inactive" },
291
+ { title: "Offline", value: "offline" },
292
+ ];
284
293
 
285
294
  /**
286
295
  * The columns. NO `weight`, on purpose.
@@ -324,7 +333,7 @@ const filteredReaders = computed(() => {
324
333
  resolveReaderStatus(reader),
325
334
  ].some((value) => String(value ?? "").toLowerCase().includes(query));
326
335
 
327
- const matchesStatus = status.value === "Status" || resolveReaderStatus(reader) === status.value;
336
+ const matchesStatus = !status.value || resolveReaderStatus(reader) === status.value;
328
337
 
329
338
  return matchesSearch && matchesStatus;
330
339
  });