@7365admin1/layer-common 3.0.21 → 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.
@@ -90,7 +90,7 @@
90
90
  v-if="props.cancelInvitation"
91
91
  >
92
92
  <template v-slot:activator="{ props }">
93
- <v-icon v-bind="props">mdi-dots-horizontal</v-icon>
93
+ <v-icon v-bind="props">mdi-dots-vertical</v-icon>
94
94
  </template>
95
95
  <v-list>
96
96
  <v-list-item @click="openConfirmDialog(item._id ?? '')" v-if="item.status === 'pending'">
@@ -99,6 +99,9 @@
99
99
  </v-list>
100
100
  </v-menu>
101
101
  </template>
102
+ <template #item.status="{value}">
103
+ {{ formatStatus(value) }}
104
+ </template>
102
105
  </v-data-table>
103
106
  </v-card>
104
107
  </v-col>
@@ -198,14 +201,27 @@ authenticate();
198
201
 
199
202
  const headers = computed(() => {
200
203
  const baseHeaders = [
204
+ {
205
+ title: "E-mail",
206
+ value: "email",
207
+ },
208
+ {
209
+ title: "Roles and Permission",
210
+ value: "roleName",
211
+ },
201
212
  {
202
213
  title: "Date",
203
214
  value: "createdAt",
204
215
  },
205
216
  {
206
- title: "E-mail",
207
- value: "email",
217
+ title: "Site Invited To",
218
+ value: "metadata.siteName",
219
+ },
220
+ {
221
+ title: "Status",
222
+ value: "status",
208
223
  },
224
+
209
225
  ];
210
226
 
211
227
  if (props.status === "pending" && props.cancelInvitation) {
@@ -318,6 +334,19 @@ watchEffect(() => {
318
334
  }
319
335
  });
320
336
 
337
+ const formatStatus = (status: string) => {
338
+ switch (status) {
339
+ case "pending":
340
+ return "Pending Invite";
341
+ case "expired":
342
+ return "Invitation expired";
343
+ case "accepted":
344
+ return "Accepted";
345
+ default:
346
+ return status?.[0]?.toUpperCase() + status?.slice(1) || "";
347
+ }
348
+ };
349
+
321
350
  const dialogMember = ref(false);
322
351
 
323
352
  function inviteMember() {
@@ -90,7 +90,7 @@
90
90
  width="150"
91
91
  >
92
92
  <template v-slot:activator="{ props }">
93
- <v-icon v-bind="props">mdi-dots-horizontal</v-icon>
93
+ <v-icon v-bind="props">mdi-dots-vertical</v-icon>
94
94
  </template>
95
95
  <v-list>
96
96
  <v-list-item
@@ -13,18 +13,7 @@
13
13
  Patrol Report Log
14
14
  </v-btn>
15
15
  </v-col>
16
- <v-col cols="12" md="6" class="d-flex justify-end">
17
- <v-btn
18
- color="primary"
19
-
20
- variant="flat"
21
- size="large"
22
- class="text-none px-8"
23
- prepend-icon="mdi-plus"
24
- >
25
- Create Routes & Schedule
26
- </v-btn>
27
- </v-col>
16
+
28
17
  </v-row>
29
18
 
30
19
  <!-- Tabs -->
@@ -74,18 +63,7 @@
74
63
  bg-color="white"
75
64
  />
76
65
  </v-col>
77
- <v-col cols="12" md="3" class="px-md-2 mb-2 mb-md-0">
78
- <v-btn
79
- color="primary"
80
- block
81
- size="large"
82
- rounded="lg"
83
- class="text-none"
84
- @click="loadReport"
85
- >
86
- Select Routes & Schedule
87
- </v-btn>
88
- </v-col>
66
+
89
67
  <v-col cols="12" md="2" class="pl-md-2">
90
68
  <v-btn
91
69
  variant="outlined"
@@ -312,7 +290,7 @@
312
290
  size="small"
313
291
  @click="dialogSelectRoute = false"
314
292
  />
315
- <span class="text-h6 ml-2">Select Routes & Schedule</span>
293
+ <span class="text-h6 ml-2">Select Routes & Schedule </span>
316
294
  </v-card-title>
317
295
 
318
296
  <v-card-text class="px-6 pb-6">
@@ -162,7 +162,9 @@
162
162
  class="mb-2"
163
163
  >
164
164
  <v-col cols="12">
165
- <div class="text-subtitle-2 font-weight-bold mb-2">Units Under Selected Areas:</div>
165
+ <div class="text-subtitle-2 font-weight-bold mb-2">
166
+ Units Under Selected Areas:
167
+ </div>
166
168
  <v-card variant="outlined" rounded>
167
169
  <v-list density="compact" nav>
168
170
  <template
@@ -179,33 +181,62 @@
179
181
  <v-icon size="16">
180
182
  {{
181
183
  openViewAreas.includes(area.value || area._id)
182
- ? 'mdi-chevron-down'
183
- : 'mdi-chevron-right'
184
+ ? "mdi-chevron-down"
185
+ : "mdi-chevron-right"
184
186
  }}
185
187
  </v-icon>
186
188
  </template>
187
189
  <template #append>
188
190
  <v-progress-circular
189
- v-if="loadingViewUnits && !(area.value || area._id in fetchedViewAreaUnits)"
191
+ v-if="
192
+ loadingViewUnits &&
193
+ !(
194
+ area.value ||
195
+ area._id in fetchedViewAreaUnits
196
+ )
197
+ "
190
198
  size="14"
191
199
  width="2"
192
200
  indeterminate
193
201
  color="primary"
194
202
  />
195
- <v-chip v-else size="x-small" variant="tonal" color="primary">
196
- {{ (fetchedViewAreaUnits[area.value || area._id] || []).length }}
203
+ <v-chip
204
+ v-else
205
+ size="x-small"
206
+ variant="tonal"
207
+ color="primary"
208
+ >
209
+ {{
210
+ (
211
+ fetchedViewAreaUnits[
212
+ area.value || area._id
213
+ ] || []
214
+ ).length
215
+ }}
197
216
  </v-chip>
198
217
  </template>
199
218
  </v-list-item>
200
219
  <v-expand-transition>
201
- <div v-show="openViewAreas.includes(area.value || area._id)">
220
+ <div
221
+ v-show="
222
+ openViewAreas.includes(area.value || area._id)
223
+ "
224
+ >
202
225
  <v-card-text class="px-3 py-2">
203
226
  <div
204
- v-if="(fetchedViewAreaUnits[area.value || area._id] || []).length > 0"
227
+ v-if="
228
+ (
229
+ fetchedViewAreaUnits[
230
+ area.value || area._id
231
+ ] || []
232
+ ).length > 0
233
+ "
205
234
  class="d-flex flex-wrap ga-3"
206
235
  >
207
236
  <v-chip
208
- v-for="unit in fetchedViewAreaUnits[area.value || area._id]"
237
+ v-for="unit in fetchedViewAreaUnits[
238
+ area.value || area._id
239
+ ]"
209
240
  :key="unit.unit"
210
241
  size="small"
211
242
  variant="tonal"
@@ -213,7 +244,9 @@
213
244
  {{ unit.name }}
214
245
  </v-chip>
215
246
  </div>
216
- <span v-else class="text-caption text-disabled">No units configured</span>
247
+ <span v-else class="text-caption text-disabled"
248
+ >No units configured</span
249
+ >
217
250
  </v-card-text>
218
251
  </div>
219
252
  </v-expand-transition>
@@ -371,7 +404,9 @@ const messageSnackbar = ref(false);
371
404
  const messageColor = ref("success");
372
405
 
373
406
  const loadingViewUnits = ref(false);
374
- const fetchedViewAreaUnits = ref<Record<string, { name: string; unit: string }[]>>({});
407
+ const fetchedViewAreaUnits = ref<
408
+ Record<string, { name: string; unit: string }[]>
409
+ >({});
375
410
  const openViewAreas = ref<string[]>([]);
376
411
 
377
412
  const toggleViewArea = (id: string) => {
@@ -528,7 +563,7 @@ async function _deleteScheduleTask() {
528
563
 
529
564
  const response = await deleteScheduleTask(id);
530
565
  dialogDeleteTask.value = false;
531
- showMessage(response?.message, "success");
566
+ showMessage("Scheduled task successfully deleted.", "success");
532
567
 
533
568
  await getTaskRefresh();
534
569
  } catch (error: any) {
@@ -197,7 +197,11 @@
197
197
  </v-card>
198
198
  </v-dialog>
199
199
 
200
- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
200
+ <Snackbar
201
+ v-model="messageSnackbar"
202
+ :text="snackBarMessage"
203
+ :color="messageColor"
204
+ />
201
205
 
202
206
  <input
203
207
  ref="fileInput"
@@ -297,8 +301,11 @@ const dialogMode = ref<"add" | "edit">("add");
297
301
  const editingUnit = ref<Record<string, any>>({});
298
302
  const areaName = ref("");
299
303
 
304
+ const snackBarMessage = ref("");
305
+
300
306
  function showMessage(msg: string, color: string = "error") {
301
307
  message.value = msg;
308
+ snackBarMessage.value = msg;
302
309
  messageColor.value = color;
303
310
  messageSnackbar.value = true;
304
311
  }
@@ -1269,6 +1269,8 @@ async function submit() {
1269
1269
  payload.visitorPass = visitor.visitorPass;
1270
1270
  payload.passKeys = visitor.passKeys;
1271
1271
  }
1272
+
1273
+ delete payload.createdBy;
1272
1274
  }
1273
1275
 
1274
1276
  if (prop.type === "contractor") {
@@ -2034,6 +2034,29 @@ onMounted(() => {
2034
2034
  (route.query.checkedOut as string) == "true" || false;
2035
2035
  }
2036
2036
  });
2037
+
2038
+ const { socketUnregisteredVisitorTrigger, visitorSocketData } =
2039
+ useVisitorSocket();
2040
+
2041
+ watchEffect(async () => {
2042
+ if (visitorSocketData.value && visitorSocketData.value.reload && activeTab.value === "unregistered") {
2043
+ await getVisitorRefresh();
2044
+ visitorSocketData.value = null;
2045
+ socketUnregisteredVisitorTrigger.value = false;
2046
+ }
2047
+
2048
+ if(visitorSocketData.value && visitorSocketData.value.plateNumber && socketUnregisteredVisitorTrigger.value && activeTab.value === "unregistered" && dialog.showSelection === false) {
2049
+ // await getVisitorRefresh();
2050
+ // visitorSocketData.value = null;
2051
+ // socketUnregisteredVisitorTrigger.value = false;
2052
+ setTimeout(async() => {
2053
+ const { _id } = visitorSocketData.value;
2054
+ await handleRegistrationUnregisteredVisitor({ _id });
2055
+ socketUnregisteredVisitorTrigger.value = false;
2056
+ }, 1000)
2057
+ }
2058
+ })
2059
+
2037
2060
  </script>
2038
2061
 
2039
2062
  <style scoped></style>
@@ -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>
@@ -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.21",
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",