@7365admin1/layer-common 3.0.20 → 3.0.22

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.
@@ -0,0 +1,312 @@
1
+ <template>
2
+ <div>
3
+ <!-- <h1>Live Monitoring</h1>
4
+
5
+ Connection Status Indicator
6
+ <div :class="['status-badge', status]">Status: {{ status }}</div>
7
+ <pre>
8
+ path: {{ path }}
9
+ </pre>
10
+ <pre>visitorData: {{ visitorSocketData }}</pre>
11
+ <pre>socketTrigger:{{ socketUnregisteredVisitorTrigger }}</pre>
12
+
13
+ <section v-if="newVisitor">
14
+ <h3>New Plate Detected!</h3>
15
+ <div class="visitor-card">
16
+ <p>
17
+ <strong>Plate Number:</strong>
18
+ {{ newVisitor.plateNumber || "Unknown" }}
19
+ </p>
20
+ <p><strong>Site ID:</strong> {{ newVisitor.site }}</p>
21
+ <p><strong>Time:</strong> {{ new Date().toLocaleTimeString() }}</p>
22
+ </div>
23
+ </section>
24
+
25
+ <section v-if="visitorList.length > 0">
26
+ <h3>Recent History</h3>
27
+ <ul>
28
+ <li v-for="(v, index) in visitorList" :key="index">
29
+ {{ v?.plateNumber }} — {{ new Date().toLocaleTimeString() }}
30
+ </li>
31
+ </ul>
32
+ </section> -->
33
+
34
+ <v-snackbar
35
+ v-if="!isVisitorRoute && !socketUnregisteredVisitorTrigger"
36
+ v-model="snackBarMessage.modal"
37
+ :color="snackBarMessage.color"
38
+ :timeout="snackBarMessage.timeout"
39
+ location="bottom center"
40
+ class="rounded-xl"
41
+ multi-line
42
+ :z-index="100000000"
43
+ >
44
+ <div class="d-flex flex-column align-start ga-2">
45
+ <div class="d-flex align-center ga-2">
46
+ <span class="text-1-4rem mr-2">{{ snackBarMessage.text }}</span>
47
+ </div>
48
+ <v-btn
49
+ v-if="snackBarMessage.isUnregistered"
50
+ rounded="md"
51
+ variant="text"
52
+ color="baseButton"
53
+ class="text-truncate text-decoration-underline px-0"
54
+ @click="goToUnregistered"
55
+ >
56
+ Go to Unregistered
57
+ </v-btn>
58
+ </div>
59
+
60
+ <template #actions>
61
+ <v-btn
62
+ icon="mdi-close"
63
+ variant="text"
64
+ @click="handleCloseSnackbar"
65
+ />
66
+ </template>
67
+ </v-snackbar>
68
+
69
+ <v-dialog v-model="permanentMessageDialog.modal" max-width="480" persistent>
70
+ <v-card>
71
+ <v-card-title class="text-h6">Notice</v-card-title>
72
+ <v-card-text>{{ permanentMessageDialog.text }}</v-card-text>
73
+ <v-card-actions>
74
+ <v-spacer />
75
+ <v-btn
76
+ color="primary"
77
+ variant="elevated"
78
+ @click="permanentMessageDialog.modal = false"
79
+ >
80
+ Close
81
+ </v-btn>
82
+ </v-card-actions>
83
+ </v-card>
84
+ </v-dialog>
85
+ </div>
86
+ </template>
87
+
88
+ <script setup lang="ts">
89
+ import { io, type Socket } from "socket.io-client";
90
+ import type { TVisitorSocketData } from "../composables/useVisitorSocket";
91
+
92
+ const props = defineProps({
93
+ siteId: {
94
+ type: String,
95
+ default: "",
96
+ },
97
+ });
98
+
99
+ const config = useRuntimeConfig();
100
+ const publicConfig = config.public as Record<string, string | undefined>;
101
+ const route = useRoute();
102
+ const { tab } = useVisitor();
103
+ const { socketUnregisteredVisitorTrigger, visitorSocketData } =
104
+ useVisitorSocket();
105
+
106
+ const socket = shallowRef<Socket | null>(null);
107
+ const status = ref("disconnected");
108
+ const newVisitor = ref<TVisitorSocketData | null>(null);
109
+ const visitorList = ref<TVisitorSocketData[]>([]);
110
+ const lastPlateNumber = ref("");
111
+
112
+ const snackBarMessage = reactive({
113
+ timeout: 3000,
114
+ text: "",
115
+ color: "",
116
+ modal: false,
117
+ isUnregistered: false,
118
+ });
119
+
120
+ const permanentMessageDialog = reactive({
121
+ modal: false,
122
+ text: "",
123
+ });
124
+
125
+ const path = computed(() => route.path);
126
+ const isVisitorManagementPath = (path: string) =>
127
+ path.includes("/visitor-management");
128
+ const isVisitorRoute = computed(
129
+ () =>
130
+ path.value === "/visitors" ||
131
+ isVisitorManagementPath(path.value)
132
+ );
133
+ const isUnregisteredVisitorRoute = computed(
134
+ () => isVisitorRoute.value && route.query.tab === "unregistered"
135
+ );
136
+
137
+ const socketBaseUrl = computed(
138
+ () => publicConfig.socketUrl || publicConfig.API_CORE || ""
139
+ );
140
+
141
+ const formatDateQuery = (date: Date) => date.toISOString().slice(0, 10);
142
+
143
+ const getDefaultDateQuery = () => {
144
+ const dateTo = new Date();
145
+ const dateFrom = new Date(dateTo);
146
+ dateFrom.setMonth(dateFrom.getMonth() - 1);
147
+
148
+ return {
149
+ dateFrom: formatDateQuery(dateFrom),
150
+ dateTo: formatDateQuery(dateTo),
151
+ };
152
+ };
153
+
154
+ const handleCloseSnackbar = () => {
155
+ snackBarMessage.modal = false;
156
+ socketUnregisteredVisitorTrigger.value = false;
157
+ newVisitor.value = null;
158
+
159
+ };
160
+
161
+ const showTransientMessage = (
162
+ text: string,
163
+ color: string,
164
+ isUnregistered = false
165
+ ) => {
166
+ snackBarMessage.text = text;
167
+ snackBarMessage.color = color;
168
+ snackBarMessage.modal = true;
169
+ snackBarMessage.timeout = 20000;
170
+ snackBarMessage.isUnregistered = isUnregistered;
171
+ };
172
+
173
+ const connectSocket = () => {
174
+ if (!props.siteId || !socketBaseUrl.value || socket.value) {
175
+ return;
176
+ }
177
+
178
+ const namespace = `/site-${props.siteId}`;
179
+ socket.value = io(`${socketBaseUrl.value}${namespace}`, {
180
+ path: "/socket.io",
181
+ transports: ["websocket"],
182
+ reconnectionAttempts: 5,
183
+ });
184
+
185
+ socket.value.on("connect", () => {
186
+ status.value = "connected";
187
+ });
188
+
189
+ socket.value.on("anpr-messages", (data: TVisitorSocketData) => {
190
+ const plateNumber = data?.plateNumber?.trim() || "";
191
+ const isCheckIn = Boolean(plateNumber);
192
+ const isPlateNumberChanging =
193
+ Boolean(lastPlateNumber.value) && lastPlateNumber.value !== plateNumber;
194
+
195
+ socketUnregisteredVisitorTrigger.value = false;
196
+ visitorSocketData.value = null;
197
+ visitorSocketData.value = data;
198
+ newVisitor.value = data;
199
+
200
+ if (!isVisitorRoute.value && isCheckIn) {
201
+ showTransientMessage(
202
+ `New plate detected: ${plateNumber}`,
203
+ "info",
204
+ true
205
+ );
206
+ lastPlateNumber.value = plateNumber;
207
+ return;
208
+ }
209
+
210
+ if (isUnregisteredVisitorRoute.value && isCheckIn) {
211
+ if (isPlateNumberChanging) {
212
+ showTransientMessage(`New plate detected: ${plateNumber}`, "info", true);
213
+ } else {
214
+ snackBarMessage.modal = false;
215
+ }
216
+
217
+ lastPlateNumber.value = plateNumber;
218
+ return;
219
+ }
220
+
221
+ if (isCheckIn) {
222
+ lastPlateNumber.value = plateNumber;
223
+ }
224
+
225
+ if (data?.message) {
226
+ showTransientMessage(data.message, "error");
227
+ }
228
+
229
+ if (data?.messagePermanent) {
230
+ permanentMessageDialog.text = data.messagePermanent;
231
+ permanentMessageDialog.modal = true;
232
+ }
233
+
234
+ visitorList.value.unshift(data);
235
+ });
236
+
237
+ socket.value.on("connect_error", () => {
238
+ status.value = "disconnected";
239
+ });
240
+ };
241
+
242
+ const disconnectSocket = () => {
243
+ if (socket.value) {
244
+ socket.value.disconnect();
245
+ socket.value = null;
246
+ }
247
+ };
248
+
249
+ const goToUnregistered = async () => {
250
+ tab.value = "unregistered";
251
+ socketUnregisteredVisitorTrigger.value = true;
252
+ snackBarMessage.modal = false;
253
+
254
+ if (route.params.org && route.params.site) {
255
+ const defaultDateQuery = getDefaultDateQuery();
256
+
257
+ await navigateTo({
258
+ path: `/${route.params.org}/${route.params.site}/visitor-management`,
259
+ query: {
260
+ tab: "unregistered",
261
+ dateFrom: route.query.dateFrom || defaultDateQuery.dateFrom,
262
+ dateTo: route.query.dateTo || defaultDateQuery.dateTo,
263
+ },
264
+ });
265
+ return;
266
+ }
267
+
268
+ await navigateTo({
269
+ name: "visitors",
270
+ query: {
271
+ tab: "unregistered",
272
+ },
273
+ });
274
+ };
275
+
276
+ watch(
277
+ () => props.siteId,
278
+ () => {
279
+ disconnectSocket();
280
+ connectSocket();
281
+ },
282
+ { immediate: true }
283
+ );
284
+
285
+ watch(
286
+ () => route.fullPath,
287
+ () => {
288
+ if (isUnregisteredVisitorRoute.value) {
289
+ snackBarMessage.modal = false;
290
+ }
291
+ },
292
+ { immediate: true }
293
+ );
294
+
295
+ onUnmounted(() => {
296
+ disconnectSocket();
297
+ });
298
+
299
+ watch(
300
+ () => route.path,
301
+ (currentPath, previousPath) => {
302
+ if (
303
+ previousPath &&
304
+ isVisitorManagementPath(previousPath) &&
305
+ !isVisitorManagementPath(currentPath)
306
+ ) {
307
+ handleCloseSnackbar();
308
+ }
309
+ }
310
+ );
311
+
312
+ </script>
@@ -132,6 +132,25 @@ export default function useHidAmico() {
132
132
  });
133
133
  }
134
134
 
135
+ function getIntercomStatus(readerId: string) {
136
+ return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/intercom/status`, {
137
+ method: "GET",
138
+ });
139
+ }
140
+
141
+ function makeIntercomCall(readerId: string, target: string) {
142
+ return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/intercom/call`, {
143
+ method: "POST",
144
+ body: { target },
145
+ });
146
+ }
147
+
148
+ function finalizeIntercomCall(readerId: string) {
149
+ return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/intercom/hangup`, {
150
+ method: "POST",
151
+ });
152
+ }
153
+
135
154
  return {
136
155
  getReaders,
137
156
  createReader,
@@ -148,5 +167,8 @@ export default function useHidAmico() {
148
167
  createIdentity,
149
168
  updateIdentity,
150
169
  deleteIdentity,
170
+ getIntercomStatus,
171
+ makeIntercomCall,
172
+ finalizeIntercomCall,
151
173
  };
152
174
  }
@@ -34,6 +34,13 @@ export default function useHidNavigation() {
34
34
  params: { org, site },
35
35
  },
36
36
  },
37
+ {
38
+ title: "Intercom",
39
+ route: {
40
+ name: "org-site-access-mgmt-intercom",
41
+ params: { org, site },
42
+ },
43
+ },
37
44
  {
38
45
  title: "Identity Mapping",
39
46
  route: {
@@ -94,6 +94,8 @@ export default function () {
94
94
  tab?: string;
95
95
  };
96
96
 
97
+ const tab = useState("visitorsTab", () => "visitor");
98
+
97
99
  async function getVisitors({
98
100
  page = 1,
99
101
  limit = 10,
@@ -190,6 +192,7 @@ export default function () {
190
192
  }
191
193
 
192
194
  return {
195
+ tab,
193
196
  typeFieldMap,
194
197
  visitorSelection,
195
198
  contractorTypes,
@@ -0,0 +1,25 @@
1
+ export type TVisitorSocketData = {
2
+ _id: string;
3
+ site: string;
4
+ plateNumber: string;
5
+ message?: string;
6
+ messagePermanent?: string;
7
+ reload?: boolean;
8
+ };
9
+
10
+ export const useVisitorSocket = () => {
11
+ const socketUnregisteredVisitorTrigger = useState(
12
+ "socketUnregisteredVisitorTrigger",
13
+ () => false,
14
+ );
15
+ const visitorSocketData = useState(
16
+ "visitorSocketData",
17
+ (): TVisitorSocketData | null => null,
18
+ );
19
+
20
+
21
+ return {
22
+ socketUnregisteredVisitorTrigger,
23
+ visitorSocketData,
24
+ };
25
+ };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.20",
5
+ "version": "3.0.22",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -40,6 +40,7 @@
40
40
  "qrcode": "^1.5.4",
41
41
  "qrcode.vue": "^3.4.1",
42
42
  "sass": "^1.80.6",
43
+ "socket.io-client": "^4.8.3",
43
44
  "vue-draggable-next": "^2.3.0",
44
45
  "vue3-signature": "^0.2.4",
45
46
  "vuetify-pro-tiptap": "^2.8.2",
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <v-container fluid>
3
+ <HidEnabledGate
4
+ :site="siteId"
5
+ :org="orgId"
6
+ message="Enable HID as a service for this site before configuring HID intercom."
7
+ >
8
+ <HidIntercomManagement :site="siteId" />
9
+ </HidEnabledGate>
10
+ </v-container>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ definePageMeta({
15
+ layout: "default",
16
+ middleware: ["01-auth", "02-org"],
17
+ memberOnly: true,
18
+ });
19
+
20
+ const route = useRoute();
21
+
22
+ const orgId = computed(() => String(route.params.org || ""));
23
+ const siteId = computed(() => String(route.params.site || ""));
24
+ </script>
@@ -1,11 +1,15 @@
1
1
  declare type TVerificationMetadata = {
2
2
  app?: string;
3
3
  role?: string;
4
+ name?: string;
5
+ org?: string;
6
+ siteId?: string;
7
+ siteName?: string;
4
8
  };
5
9
 
6
10
  declare type TMiniVerification = Pick<
7
11
  TVerification,
8
- "createdAt" | "email" | "_id" | "status"
12
+ "createdAt" | "email" | "_id" | "status" | "metadata" | "roleName"
9
13
  >;
10
14
 
11
15
  declare type TVerification = {
@@ -13,6 +17,7 @@ declare type TVerification = {
13
17
  type: string;
14
18
  email: string;
15
19
  metadata?: TVerificationMetadata;
20
+ roleName?: string;
16
21
  status?: string;
17
22
  createdAt?: string;
18
23
  updatedAt?: string | null;