@7365admin1/layer-common 3.2.8-staging.204 → 3.2.8-staging.206

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.
@@ -238,7 +238,7 @@
238
238
  <AppField
239
239
  v-model="sipForm.password"
240
240
  :disabled="areSipCredentialFieldsDisabled"
241
- placeholder="Enter password"
241
+ placeholder="Leave blank to keep current password"
242
242
  :type="showSipPassword ? 'text' : 'password'"
243
243
  autocomplete="new-password"
244
244
  class="sip-input"
@@ -653,6 +653,17 @@ type ContactRow = {
653
653
  dialCode: string;
654
654
  };
655
655
 
656
+ type HidReader = Record<string, unknown> & {
657
+ _id: string;
658
+ name?: string;
659
+ deviceId?: string;
660
+ location?: string;
661
+ monitorPath?: string;
662
+ portalName?: string;
663
+ readerModel?: string;
664
+ capabilities?: unknown;
665
+ };
666
+
656
667
  type SipAccount = {
657
668
  orgId: string;
658
669
  site: string;
@@ -718,7 +729,7 @@ const {
718
729
  } = useHidAmico();
719
730
 
720
731
  const activeTab = ref<IntercomTab>("intercom");
721
- const readers = ref<Record<string, any>[]>([]);
732
+ const readers = ref<HidReader[]>([]);
722
733
  const selectedReaderId = ref("");
723
734
  const testingSipReaderId = ref("");
724
735
  const testingContactReaderId = ref("");
@@ -770,7 +781,6 @@ const pjsipKeys = [
770
781
  "numeric_branch_enabled",
771
782
  "branch",
772
783
  "login",
773
- "password",
774
784
  "peer_to_peer_enabled",
775
785
  "reg_status_query_period",
776
786
  "server_retry_interval",
@@ -1064,14 +1074,21 @@ async function reload() {
1064
1074
  loading.value = true;
1065
1075
  try {
1066
1076
  const response = await getReaders({ site: props.site, page: 1, limit: 100 });
1067
- readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
1077
+ const responseRecord = toRecord(response);
1078
+ const responseData = toRecord(responseRecord.data);
1079
+ const availableReaders = responseRecord.items ?? responseData.items ?? responseData.readers ?? [];
1080
+ readers.value = (Array.isArray(availableReaders) ? availableReaders : [])
1081
+ .map(toHidReader)
1082
+ .filter((reader): reader is HidReader => (
1083
+ reader !== null && reader.readerModel === "VL70LF" && toRecord(reader.capabilities).intercom === true
1084
+ ));
1068
1085
  if (!readers.value.some((reader) => reader._id === selectedReaderId.value)) {
1069
1086
  selectedReaderId.value = readers.value[0]?._id || "";
1070
1087
  }
1071
1088
  intercomLoaded.value = false;
1072
1089
  contactsLoaded.value = false;
1073
1090
  await loadActiveTab();
1074
- } catch (error: any) {
1091
+ } catch (error: unknown) {
1075
1092
  showMessage(getErrorMessage(error), "error");
1076
1093
  } finally {
1077
1094
  loading.value = false;
@@ -1178,7 +1195,7 @@ function toIntercomRow(reader: Record<string, unknown>, pjsip: Record<string, un
1178
1195
  numericExtension: toFlag(getPjsipValue(pjsip, ["numeric_branch_enabled", "numericExtension", "numericExtensionEnabled"])) === "1",
1179
1196
  extension: toText(getPjsipValue(pjsip, ["branch", "extension"]), "N/A"),
1180
1197
  login: toText(getPjsipValue(pjsip, ["login", "username"]), "N/A"),
1181
- password: toText(getPjsipValue(pjsip, ["password"]), ""),
1198
+ password: "",
1182
1199
  operatingMode: peerToPeer ? "Peer-to-Peer" : "SIP Client",
1183
1200
  protocol: toText(getPjsipValue(pjsip, ["protocol"]), "UDP"),
1184
1201
  videoEnabled: toFlag(getPjsipValue(pjsip, ["video_enabled", "videoEnabled"])) === "1",
@@ -1251,9 +1268,13 @@ function normalizeStatus(response: Record<string, unknown>, pjsip: Record<string
1251
1268
  return "Not connected";
1252
1269
  }
1253
1270
 
1254
- function normalizeContacts(reader: Record<string, any>, response: Record<string, any>): ContactRow[] {
1255
- const items = response?.contacts ?? response?.values ?? response?.data?.contacts ?? response?.data ?? [];
1256
- return (Array.isArray(items) ? items : []).map((contact, index) => ({
1271
+ function normalizeContacts(reader: HidReader, response: unknown): ContactRow[] {
1272
+ const responseRecord = toRecord(response);
1273
+ const responseData = toRecord(responseRecord.data);
1274
+ const items = responseRecord.contacts ?? responseRecord.values ?? responseData.contacts ?? responseRecord.data ?? [];
1275
+ return (Array.isArray(items) ? items : []).map((value, index) => {
1276
+ const contact = toRecord(value);
1277
+ return {
1257
1278
  _rowKey: `${reader._id}-${contact.id ?? index}`,
1258
1279
  id: toNumber(contact.id, undefined),
1259
1280
  readerId: reader._id,
@@ -1261,7 +1282,8 @@ function normalizeContacts(reader: Record<string, any>, response: Record<string,
1261
1282
  location: reader.location || "N/A",
1262
1283
  serverAddress: toText(contact.number || contact.serverAddress, "N/A"),
1263
1284
  dialCode: toText(contact.number || contact.serverAddress, "N/A"),
1264
- }));
1285
+ };
1286
+ });
1265
1287
  }
1266
1288
 
1267
1289
  function openViewDialog(row: IntercomRow) {
@@ -1515,7 +1537,7 @@ async function saveSipSettings() {
1515
1537
  return;
1516
1538
  }
1517
1539
 
1518
- const saved = await savePjsip(selectedRow.value.reader._id, {
1540
+ const payload: Record<string, string> = {
1519
1541
  enabled: sipForm.enabled ? "1" : "0",
1520
1542
  server_ip: sipForm.serverIp,
1521
1543
  server_port: sipForm.serverPort,
@@ -1524,10 +1546,15 @@ async function saveSipSettings() {
1524
1546
  numeric_branch_enabled: sipForm.numericExtension ? "1" : "0",
1525
1547
  branch: isPeerToPeer ? "" : sipForm.extension,
1526
1548
  login: isPeerToPeer ? "" : sipForm.login,
1527
- password: isPeerToPeer ? "" : sipForm.password,
1528
1549
  peer_to_peer_enabled: isPeerToPeer ? "1" : "0",
1529
1550
  video_enabled: sipForm.videoEnabled ? "1" : "0",
1530
- });
1551
+ };
1552
+ if (isPeerToPeer) {
1553
+ payload.password = "";
1554
+ } else if (sipForm.password) {
1555
+ payload.password = sipForm.password;
1556
+ }
1557
+ const saved = await savePjsip(selectedRow.value.reader._id, payload);
1531
1558
  if (saved) {
1532
1559
  sipDialog.value = false;
1533
1560
  }
@@ -1579,7 +1606,7 @@ async function runMakeCall() {
1579
1606
  makeCallForm.status = `Calling ${target}.`;
1580
1607
  makeCallForm.statusType = "success";
1581
1608
  showMessage(`Calling ${target}.`, "success");
1582
- } catch (error: any) {
1609
+ } catch (error: unknown) {
1583
1610
  const message = getErrorMessage(error);
1584
1611
  makeCallForm.status = message;
1585
1612
  makeCallForm.statusType = "error";
@@ -1602,7 +1629,7 @@ async function runHangupCall() {
1602
1629
  makeCallForm.status = "Call ended.";
1603
1630
  makeCallForm.statusType = "success";
1604
1631
  showMessage("Intercom call ended.", "success");
1605
- } catch (error: any) {
1632
+ } catch (error: unknown) {
1606
1633
  const message = getErrorMessage(error);
1607
1634
  makeCallForm.status = message;
1608
1635
  makeCallForm.statusType = "error";
@@ -1667,7 +1694,7 @@ async function savePjsip(readerId: string, payload: Record<string, string>, opti
1667
1694
  showMessage("Intercom settings saved.", "success");
1668
1695
  }
1669
1696
  return true;
1670
- } catch (error: any) {
1697
+ } catch (error: unknown) {
1671
1698
  showMessage(getErrorMessage(error), "error");
1672
1699
  return false;
1673
1700
  } finally {
@@ -1711,7 +1738,7 @@ async function saveContact() {
1711
1738
  contactDialog.value = false;
1712
1739
  await loadContacts();
1713
1740
  showMessage("Intercom contact saved.", "success");
1714
- } catch (error: any) {
1741
+ } catch (error: unknown) {
1715
1742
  showMessage(getErrorMessage(error), "error");
1716
1743
  } finally {
1717
1744
  contactsSaving.value = false;
@@ -1730,7 +1757,7 @@ async function deleteContact() {
1730
1757
  deleteContactDialog.value = false;
1731
1758
  await loadContacts();
1732
1759
  showMessage("Intercom contact deleted.", "success");
1733
- } catch (error: any) {
1760
+ } catch (error: unknown) {
1734
1761
  showMessage(getErrorMessage(error), "error");
1735
1762
  } finally {
1736
1763
  contactsSaving.value = false;
@@ -1791,6 +1818,12 @@ function toRecord(value: unknown): Record<string, unknown> {
1791
1818
  return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
1792
1819
  }
1793
1820
 
1821
+ function toHidReader(value: unknown): HidReader | null {
1822
+ const reader = toRecord(value);
1823
+ const id = toText(reader._id);
1824
+ return id ? { ...reader, _id: id } : null;
1825
+ }
1826
+
1794
1827
  function toText(value: unknown, fallback = "") {
1795
1828
  if (value === undefined || value === null || value === "") return fallback;
1796
1829
  return String(value);
@@ -368,6 +368,7 @@ const {
368
368
  getReaders,
369
369
  getReaderConfiguration,
370
370
  setReaderConfiguration,
371
+ setReaderOperatingMode,
371
372
  getSitePermissions,
372
373
  getPermissionCandidates,
373
374
  updateSitePermissions,
@@ -779,9 +780,7 @@ async function saveOnlineMode() {
779
780
 
780
781
  savingSection.value = "online-mode";
781
782
  try {
782
- await setReaderConfiguration(form.readerId, {
783
- general: { online: form.onlineMode ? "1" : "0" },
784
- });
783
+ await setReaderOperatingMode(form.readerId, form.onlineMode ? "online" : "standalone");
785
784
  await updateSitebyId(props.site, buildPayload());
786
785
  showMessage("Online mode updated.", "success");
787
786
  } catch (error: unknown) {
@@ -39,11 +39,15 @@
39
39
  class="mb-3"
40
40
  />
41
41
 
42
- <InputLabel title="Monitor Path" required class="mb-1" />
43
- <v-btn-toggle v-model="monitorEnabled" mandatory divided class="monitor-toggle mb-3">
42
+ <InputLabel title="Monitor Callback" required class="mb-1" />
43
+ <v-btn-toggle v-model="monitorEnabled" mandatory divided class="monitor-toggle">
44
44
  <v-btn :value="true" class="text-none">Enable</v-btn>
45
45
  <v-btn :value="false" class="text-none">Disable</v-btn>
46
46
  </v-btn-toggle>
47
+ <p class="reader-discovery-hint mt-1 mb-3">
48
+ Enable only when this reader can reach the IP address or domain configured on iService365.
49
+ Disabling keeps the reader in Standalone mode and uses manual log pulls instead.
50
+ </p>
47
51
 
48
52
  <InputLabel title="Username" required class="mb-1" />
49
53
  <v-text-field
@@ -98,6 +102,22 @@
98
102
  readonly
99
103
  />
100
104
 
105
+ <InputLabel title="Reader Model" required class="mb-1" />
106
+ <v-select
107
+ v-model="draft.readerModel"
108
+ :items="readerModelOptions"
109
+ aria-label="Reader Model"
110
+ placeholder="Select the model printed on the reader"
111
+ variant="outlined"
112
+ density="compact"
113
+ hide-details="auto"
114
+ class="mb-3"
115
+ />
116
+ <v-alert v-if="draft.readerModel === 'unknown'" type="warning" variant="tonal" density="compact" class="mb-3">
117
+ The Amico system-information API does not return the hardware model. Select VL35LF or VL70LF from the
118
+ label on the physical reader so model-specific features are enabled correctly.
119
+ </v-alert>
120
+
101
121
  <InputLabel title="HID Portal" required class="mb-1" />
102
122
  <v-select
103
123
  v-model="draft.portalId"
@@ -111,6 +131,64 @@
111
131
  hide-details="auto"
112
132
  :readonly="mode === 'edit'"
113
133
  />
134
+
135
+ <InputLabel title="Card Profile Configured on Reader" required class="mt-3 mb-1" />
136
+ <v-select
137
+ v-model="draft.cardReadingMode"
138
+ :items="cardModeOptions"
139
+ aria-label="Card Data Mode"
140
+ placeholder="Select the mode configured on the reader"
141
+ variant="outlined"
142
+ density="compact"
143
+ hide-details="auto"
144
+ class="mb-3"
145
+ />
146
+ <v-alert type="info" variant="tonal" density="compact" class="mb-3">
147
+ Match Menu &gt; Access &gt; Identification Methods &gt; Card Reading on the physical reader.
148
+ The Amico HTTP API does not expose the OMNIKEY card-profile setting for automatic verification.
149
+ </v-alert>
150
+
151
+ <template v-if="draft.cardReadingMode === 'csn' || draft.cardReadingMode === 'csn_for_mifare'">
152
+ <InputLabel title="CSN Byte Order" required class="mb-1" />
153
+ <v-select
154
+ v-model="draft.csnByteOrder"
155
+ :items="csnByteOrderOptions"
156
+ variant="outlined"
157
+ density="compact"
158
+ hide-details="auto"
159
+ class="mb-3"
160
+ />
161
+ </template>
162
+
163
+ <template v-if="draft.cardReadingMode === 'pacs'">
164
+ <InputLabel title="PACS Output Format" required class="mb-1" />
165
+ <v-select
166
+ v-model="draft.pacsFormat"
167
+ :items="pacsFormatOptions"
168
+ variant="outlined"
169
+ density="compact"
170
+ hide-details="auto"
171
+ class="mb-3"
172
+ />
173
+ </template>
174
+
175
+ <InputLabel title="Operating Mode" required class="mb-1" />
176
+ <v-select
177
+ v-model="draft.operatingMode"
178
+ :items="operatingModeOptions"
179
+ variant="outlined"
180
+ density="compact"
181
+ hide-details="auto"
182
+ :disabled="!monitorEnabled"
183
+ />
184
+ <p v-if="!monitorEnabled" class="reader-discovery-hint mt-1 mb-0">
185
+ Online mode requires an enabled and verified Monitor callback.
186
+ </p>
187
+
188
+ <p v-if="draft.readerModel || draft.firmwareVersion" class="reader-discovery-hint mt-3">
189
+ Detected: {{ draft.readerModel || "Unknown model" }}
190
+ <template v-if="draft.firmwareVersion"> · Firmware {{ draft.firmwareVersion }}</template>
191
+ </p>
114
192
  </v-card-text>
115
193
 
116
194
  <v-card-actions class="screen-modal__footer">
@@ -125,7 +203,7 @@
125
203
  class="text-none screen-btn-primary"
126
204
  variant="flat"
127
205
  :loading="loading"
128
- :disabled="mode === 'add' && (!draft.deviceId || !draft.portalId)"
206
+ :disabled="mode === 'add' && (!draft.deviceId || !draft.portalId || draft.cardReadingMode === 'unconfigured')"
129
207
  @click="submit"
130
208
  >
131
209
  Submit
@@ -136,6 +214,42 @@
136
214
  </template>
137
215
 
138
216
  <script setup lang="ts">
217
+ type HidReaderFormRecord = {
218
+ name?: string;
219
+ baseUrl?: string;
220
+ deviceId?: string;
221
+ portalId?: number;
222
+ portalName?: string;
223
+ location?: string;
224
+ username?: string;
225
+ password?: string;
226
+ readerModel?: "VL35LF" | "VL70LF" | "unknown" | "";
227
+ firmwareVersion?: string;
228
+ cardReadingMode?: "unconfigured" | "pacs" | "csn" | "csn_for_mifare";
229
+ csnByteOrder?: "msb" | "lsb";
230
+ pacsFormat?: "raw" | "wiegand";
231
+ operatingMode?: "standalone" | "online";
232
+ monitorStatus?: "unconfigured" | "configured" | "disabled" | "failed";
233
+ };
234
+
235
+ type HidReaderFormPayload = {
236
+ name: string;
237
+ baseUrl: string;
238
+ deviceId: string;
239
+ portalId: number | null;
240
+ portalName: string;
241
+ location: string;
242
+ username: string;
243
+ password: string;
244
+ readerModel: "VL35LF" | "VL70LF" | "unknown" | "";
245
+ firmwareVersion: string;
246
+ cardReadingMode: "unconfigured" | "pacs" | "csn" | "csn_for_mifare";
247
+ csnByteOrder: "msb" | "lsb";
248
+ pacsFormat: "raw" | "wiegand";
249
+ operatingMode: "standalone" | "online";
250
+ monitorEnabled: boolean;
251
+ };
252
+
139
253
  const props = defineProps({
140
254
  modelValue: {
141
255
  type: Boolean,
@@ -146,9 +260,13 @@ const props = defineProps({
146
260
  default: "add",
147
261
  },
148
262
  reader: {
149
- type: Object as () => Record<string, any> | null,
263
+ type: Object as () => HidReaderFormRecord | null,
150
264
  default: null,
151
265
  },
266
+ site: {
267
+ type: String,
268
+ required: true,
269
+ },
152
270
  loading: {
153
271
  type: Boolean,
154
272
  default: false,
@@ -164,7 +282,7 @@ const props = defineProps({
164
282
 
165
283
  const emit = defineEmits<{
166
284
  "update:modelValue": [value: boolean];
167
- submit: [payload: Record<string, any>];
285
+ submit: [payload: HidReaderFormPayload];
168
286
  }>();
169
287
 
170
288
  const dialogModel = computed({
@@ -188,8 +306,36 @@ const draft = reactive({
188
306
  location: "",
189
307
  username: "",
190
308
  password: "",
309
+ readerModel: "" as "VL35LF" | "VL70LF" | "unknown" | "",
310
+ firmwareVersion: "",
311
+ cardReadingMode: "unconfigured" as "unconfigured" | "pacs" | "csn" | "csn_for_mifare",
312
+ csnByteOrder: "msb" as "msb" | "lsb",
313
+ pacsFormat: "raw" as "raw" | "wiegand",
314
+ operatingMode: "standalone" as "standalone" | "online",
191
315
  });
192
316
 
317
+ const cardModeOptions = [
318
+ { title: "PACS (facility code + card number)", value: "pacs" },
319
+ { title: "CSN", value: "csn" },
320
+ { title: "CSN for MIFARE", value: "csn_for_mifare" },
321
+ ];
322
+ const csnByteOrderOptions = [
323
+ { title: "MSB first", value: "msb" },
324
+ { title: "LSB first", value: "lsb" },
325
+ ];
326
+ const pacsFormatOptions = [
327
+ { title: "RAW", value: "raw" },
328
+ { title: "Wiegand", value: "wiegand" },
329
+ ];
330
+ const operatingModeOptions = [
331
+ { title: "Standalone", value: "standalone" },
332
+ { title: "Online", value: "online" },
333
+ ];
334
+ const readerModelOptions = [
335
+ { title: "VL35LF", value: "VL35LF" },
336
+ { title: "VL70LF", value: "VL70LF" },
337
+ ];
338
+
193
339
  watch(
194
340
  () => [props.modelValue, props.reader],
195
341
  () => {
@@ -208,7 +354,13 @@ watch(
208
354
  draft.location = props.reader?.location ?? "";
209
355
  draft.username = props.reader?.username ?? "";
210
356
  draft.password = props.reader?.password ?? "";
211
- monitorEnabled.value = props.reader?.enabled ?? true;
357
+ monitorEnabled.value = props.reader?.monitorStatus !== "disabled";
358
+ draft.readerModel = props.reader?.readerModel ?? "";
359
+ draft.firmwareVersion = props.reader?.firmwareVersion ?? "";
360
+ draft.cardReadingMode = props.reader?.cardReadingMode ?? "unconfigured";
361
+ draft.csnByteOrder = props.reader?.csnByteOrder ?? "msb";
362
+ draft.pacsFormat = props.reader?.pacsFormat ?? "raw";
363
+ draft.operatingMode = props.reader?.operatingMode ?? "standalone";
212
364
  },
213
365
  { immediate: true },
214
366
  );
@@ -235,11 +387,16 @@ watch(
235
387
  },
236
388
  );
237
389
 
390
+ watch(monitorEnabled, (enabled) => {
391
+ if (!enabled) draft.operatingMode = "standalone";
392
+ });
393
+
238
394
  async function connectReader() {
239
395
  discovering.value = true;
240
396
  discoveryError.value = "";
241
397
  try {
242
398
  const response = await discoverReader({
399
+ site: props.site,
243
400
  baseUrl: draft.baseUrl.trim(),
244
401
  username: draft.username.trim(),
245
402
  password: draft.password,
@@ -250,6 +407,13 @@ async function connectReader() {
250
407
  ? nestedData as Record<string, unknown>
251
408
  : response;
252
409
  draft.deviceId = String(data?.deviceId || "");
410
+ const discoveredModel = String(data?.readerModel || "unknown");
411
+ draft.readerModel = (
412
+ discoveredModel === "VL35LF" || discoveredModel === "VL70LF"
413
+ ? discoveredModel
414
+ : draft.readerModel || "unknown"
415
+ ) as typeof draft.readerModel;
416
+ draft.firmwareVersion = String(data?.firmwareVersion || data?.version || "");
253
417
  portals.value = Array.isArray(data?.portals)
254
418
  ? data.portals.flatMap((portal: unknown) => {
255
419
  if (typeof portal !== "object" || portal === null) return [];
@@ -278,13 +442,15 @@ async function connectReader() {
278
442
  }
279
443
 
280
444
  function submit() {
445
+ if (draft.readerModel !== "VL35LF" && draft.readerModel !== "VL70LF") {
446
+ discoveryError.value = "Select the model printed on the physical HID reader.";
447
+ return;
448
+ }
281
449
  const portal = portals.value.find((item) => item.id === Number(draft.portalId));
282
- const payload: Record<string, any> = {
450
+ const payload: HidReaderFormPayload = {
283
451
  ...draft,
284
452
  portalName: portal?.name || draft.portalName,
285
- monitorPath: monitorEnabled.value ? "api/notifications" : "",
286
- enabled: monitorEnabled.value,
287
- status: monitorEnabled.value ? "active" : "inactive",
453
+ monitorEnabled: monitorEnabled.value,
288
454
  };
289
455
 
290
456
  emit("submit", payload);