@7365admin1/layer-common 1.11.21 → 1.11.23

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,262 @@
1
+ <template>
2
+ <v-card width="100%" :loading="processing">
3
+ <v-toolbar>
4
+ <v-row
5
+ no-gutters
6
+ class="fill-height px-6 d-flex justify-space-between align-center"
7
+ align="center"
8
+ >
9
+ <span class="font-weight-bold text-h5 text-capitalize">
10
+ Visitor Information
11
+ </span>
12
+ <ButtonClose @click="emit('close')" />
13
+ </v-row>
14
+ </v-toolbar>
15
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-3">
16
+ <div class="w-100 d-flex justify-space-between ga-2">
17
+ <!-- <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
18
+ <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span> -->
19
+ </div>
20
+ <!-- <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''"> -->
21
+ <v-row no-gutters class="pt-4">
22
+ <v-col cols="12">
23
+ <InputLabel class="text-capitalize" title="Full Name" />
24
+ <v-text-field
25
+ v-model="visitorInfo.name"
26
+ density="comfortable"
27
+ readonly
28
+ />
29
+ </v-col>
30
+
31
+ <v-col cols="12">
32
+ <InputLabel class="text-capitalize" title="NRIC/Passport/ID No." />
33
+ <v-text-field
34
+ v-if="!visitorInfo.checkIn"
35
+ v-model="visitorInfo.nric"
36
+ density="comfortable"
37
+ hide-details
38
+ clearable
39
+ />
40
+ <v-text-field
41
+ v-else
42
+ v-model="visitorNric"
43
+ density="comfortable"
44
+ hide-details
45
+ readonly
46
+ />
47
+ </v-col>
48
+
49
+ <v-col cols="12" class="mt-4">
50
+ <InputLabel
51
+ class="text-capitalize"
52
+ title="Vehicle Number (Optional)"
53
+ />
54
+ <InputVehicleNumber
55
+ v-if="!visitorInfo.checkIn"
56
+ v-model="visitorInfo.plateNumber"
57
+ density="comfortable"
58
+ hide-details
59
+ clearable
60
+ />
61
+ <InputVehicleNumber
62
+ v-else
63
+ v-model="visitorPlateNumber"
64
+ density="comfortable"
65
+ hide-details
66
+ readonly
67
+ />
68
+ </v-col>
69
+
70
+ <v-col cols="12" class="mt-4">
71
+ <InputLabel class="text-capitalize" title="Type" />
72
+ <v-text-field
73
+ v-model="visitorInfo.type"
74
+ density="comfortable"
75
+ hide-details
76
+ readonly
77
+ />
78
+ </v-col>
79
+
80
+ <v-col cols="12" class="mt-4">
81
+ <InputLabel class="text-capitalize" title="Phone Number" />
82
+ <InputPhoneNumberV2
83
+ v-model="visitorInfo.contact"
84
+ density="comfortable"
85
+ :readonly="true"
86
+ />
87
+ </v-col>
88
+
89
+ <v-col cols="12">
90
+ <InputLabel class="text-capitalize" title="Purpose" />
91
+ <v-textarea
92
+ v-model="visitorInfo.purpose"
93
+ density="comfortable"
94
+ :rows="3"
95
+ no-resize
96
+ readonly
97
+ />
98
+ </v-col>
99
+
100
+ <v-col cols="12">
101
+ <InputLabel class="text-capitalize" title="Location" />
102
+ <v-text-field v-model="location" density="comfortable" />
103
+ </v-col>
104
+
105
+ <v-col cols="12">
106
+ <InputLabel class="text-capitalize" title="Visitor Pass" />
107
+ <PassInformation
108
+ v-model:pass="visitorInfo.visitorPass"
109
+ :site="prop.site"
110
+ :type="visitorInfo.type"
111
+ :clearable="true"
112
+ />
113
+ </v-col>
114
+
115
+ <v-col cols="12" class="mt-4">
116
+ <v-row no-gutters class="pt-2">
117
+ <v-col cols="6" class="pr-1">
118
+ <v-card elevation="3" height="100%" width="100%">
119
+ <v-col cols="12" class="d-flex justify-center pb-0">
120
+ <span
121
+ class="text-h6 font-weight-bold d-flex text-center text-success"
122
+ >
123
+ {{
124
+ visitorInfo.checkIn
125
+ ? standardFormatDateTime(visitorInfo.checkIn)
126
+ : "N/A"
127
+ }}
128
+ </span>
129
+ </v-col>
130
+ <v-col cols="12" class="d-flex justify-center">
131
+ Actual Check In
132
+ </v-col>
133
+ </v-card>
134
+ </v-col>
135
+ <v-col cols="6" class="pl-1">
136
+ <v-card elevation="3" height="100%" width="100%">
137
+ <v-col cols="12" class="d-flex justify-center pb-0">
138
+ <span
139
+ class="text-h6 font-weight-bold d-flex text-center text-error"
140
+ >
141
+ {{
142
+ visitorInfo.checkOut
143
+ ? standardFormatDateTime(visitorInfo.checkOut)
144
+ : "N/A"
145
+ }}
146
+ </span>
147
+ </v-col>
148
+ <v-col cols="12" class="d-flex justify-center">
149
+ Actual Check Out
150
+ </v-col>
151
+ </v-card>
152
+ </v-col>
153
+ </v-row>
154
+ </v-col>
155
+ </v-row>
156
+ <!-- </v-form> -->
157
+ </v-card-text>
158
+
159
+ <v-toolbar density="compact">
160
+ <v-row no-gutters>
161
+ <v-col cols="6">
162
+ <v-btn text="Close" block size="48" tile @click="emit('close')" />
163
+ </v-col>
164
+ <v-col cols="6">
165
+ <v-btn
166
+ v-if="!visitorInfo.checkIn && !visitorInfo.checkOut"
167
+ block
168
+ text="Check In"
169
+ variant="flat"
170
+ color="success"
171
+ size="48"
172
+ tile
173
+ :disabled="processing"
174
+ @click="emit('check-in-out', 'checkIn', visitorInfo)"
175
+ :loading="processing"
176
+ />
177
+ <v-btn
178
+ v-if="visitorInfo.checkIn && !visitorInfo.checkOut"
179
+ block
180
+ text="Check Out"
181
+ variant="flat"
182
+ color="error"
183
+ size="48"
184
+ tile
185
+ :disabled="processing"
186
+ @click="emit('check-in-out', 'checkOut', visitorInfo)"
187
+ :loading="processing"
188
+ />
189
+ </v-col>
190
+ </v-row>
191
+ </v-toolbar>
192
+ </v-card>
193
+ </template>
194
+ <script setup lang="ts">
195
+ const prop = defineProps({
196
+ site: {
197
+ type: String,
198
+ required: true,
199
+ },
200
+ visitorData: {
201
+ type: Object as PropType<Partial<TVisitor>>,
202
+ default: {},
203
+ },
204
+ processing: {
205
+ type: Boolean,
206
+ required: false,
207
+ },
208
+ });
209
+
210
+ const emit = defineEmits(["check-in-out", "close"]);
211
+
212
+ const { standardFormatDateTime } = useUtils();
213
+
214
+ const visitorInfo = ref(prop.visitorData);
215
+
216
+ const visitorNric = computed({
217
+ get: () =>
218
+ visitorInfo.value && visitorInfo.value.checkIn
219
+ ? visitorInfo.value.nric
220
+ : null,
221
+ set: () => {}, // readonly, do nothing on set
222
+ });
223
+
224
+ const visitorPlateNumber = computed({
225
+ get: () =>
226
+ visitorInfo.value && visitorInfo.value.checkIn
227
+ ? visitorInfo.value.plateNumber
228
+ : null,
229
+ set: () => {}, // readonly, do nothing on set
230
+ });
231
+
232
+ watchEffect(() => {
233
+ if (visitorInfo.value.plateNumber) {
234
+ visitorInfo.value.type = "guest";
235
+ } else {
236
+ visitorInfo.value.type = "walk-in";
237
+ }
238
+ });
239
+
240
+ const location = computed({
241
+ get: () => {
242
+ if (
243
+ visitorInfo.value &&
244
+ visitorInfo.value.block &&
245
+ visitorInfo.value.level &&
246
+ visitorInfo.value.unitName
247
+ ) {
248
+ return `${visitorInfo.value.block} / ${visitorInfo.value.level} / ${visitorInfo.value.unitName}`;
249
+ } else if (
250
+ visitorInfo.value &&
251
+ visitorInfo.value.block &&
252
+ !visitorInfo.value.level &&
253
+ !visitorInfo.value.unitName
254
+ ) {
255
+ return visitorInfo.value.block;
256
+ } else {
257
+ return "N/A";
258
+ }
259
+ },
260
+ set: () => {}, // readonly, do nothing on set
261
+ });
262
+ </script>
@@ -1012,7 +1012,7 @@ const membersFieldIncomplete = computed(() => {
1012
1012
  });
1013
1013
 
1014
1014
  onMounted(() => {
1015
- contractorStep.value = 2;
1015
+ contractorStep.value = 1;
1016
1016
  currentAutofillSource.value = null;
1017
1017
 
1018
1018
  if (prop.mode === 'register' && prop.visitorData) {
@@ -194,6 +194,7 @@
194
194
  :loading="
195
195
  loading.checkingOut && item?._id === selectedVisitorId
196
196
  "
197
+ @click.stop="handleCheckout(item._id)"
197
198
  />
198
199
  </span>
199
200
  </span>
@@ -271,6 +272,7 @@
271
272
  </template>
272
273
  <v-list density="compact">
273
274
  <v-list-item
275
+ v-if="!item?.checkOut"
274
276
  prepend-icon="mdi-logout"
275
277
  title="Checkout"
276
278
  @click.stop="handleCheckout(item._id)"
@@ -644,14 +646,34 @@
644
646
 
645
647
  <ScanVisitorQRCode
646
648
  :dialog="dialog.scanVisitorQRCode"
649
+ :site="siteId"
650
+ @open-visitor-data-from-scanned-qr-code-dialog="
651
+ handleShowVisitorDataFromScannedQRCodeDialog
652
+ "
647
653
  @close-dialog="dialog.scanVisitorQRCode = false"
648
654
  />
649
655
 
656
+ <v-dialog
657
+ v-model="dialog.showVisitorDataFromScannedQRCode"
658
+ width="450"
659
+ persistent
660
+ >
661
+ <VisitorDataFromScannedQRCodeForm
662
+ :site="siteId"
663
+ :visitor-data="visitorDataFromScannedQRCode"
664
+ @check-in-out="handleVisitorDataFromScannedQRCodeCheckInOut"
665
+ :processing="isVisitorDataFromScannedQRCodeCheckingInOut"
666
+ @close="handleCloseVisitorDataFromScannedQRCodeDialog"
667
+ />
668
+ </v-dialog>
669
+
650
670
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
651
671
  </v-row>
652
672
  </template>
653
673
 
654
674
  <script setup lang="ts">
675
+ import { errorConverter } from "../utils/data";
676
+
655
677
  definePageMeta({
656
678
  middleware: ["01-auth", "02-org"],
657
679
  });
@@ -679,8 +701,16 @@ const props = defineProps({
679
701
  },
680
702
  });
681
703
 
682
- const { getVisitors, visitorSelection, typeFieldMap, updateVisitor } =
683
- useVisitor();
704
+ const { authenticate, currentUser } = useLocalAuth();
705
+ authenticate();
706
+
707
+ const {
708
+ getVisitors,
709
+ visitorSelection,
710
+ typeFieldMap,
711
+ updateVisitor,
712
+ visitorDataFromScannedQRCodeCheckInOut,
713
+ } = useVisitor();
684
714
  const { debounce, formatCamelCaseToWords, formatDate, UTCToLocalTIme } =
685
715
  useUtils();
686
716
  const { formatLocation } = useSecurityUtils();
@@ -732,6 +762,7 @@ const dialog = reactive({
732
762
  addPassKey: false,
733
763
  editPassKey: false,
734
764
  scanVisitorQRCode: false,
765
+ showVisitorDataFromScannedQRCode: false,
735
766
  });
736
767
 
737
768
  const snapshotImageUrl = ref("");
@@ -913,6 +944,53 @@ function formatValues(key: string, value: any) {
913
944
  return value;
914
945
  }
915
946
 
947
+ const visitorDataFromScannedQRCode = ref<Record<string, any>>({});
948
+
949
+ function handleShowVisitorDataFromScannedQRCodeDialog(
950
+ _visitorDataFromScannedQRCode: Record<string, any>
951
+ ) {
952
+ visitorDataFromScannedQRCode.value = _visitorDataFromScannedQRCode;
953
+ dialog.showVisitorDataFromScannedQRCode = true;
954
+ }
955
+
956
+ const isVisitorDataFromScannedQRCodeCheckingInOut = ref(false);
957
+
958
+ async function handleVisitorDataFromScannedQRCodeCheckInOut(
959
+ type: string,
960
+ visitorData: Record<string, any>
961
+ ) {
962
+ try {
963
+ isVisitorDataFromScannedQRCodeCheckingInOut.value = true;
964
+ await visitorDataFromScannedQRCodeCheckInOut({
965
+ type,
966
+ filter: {
967
+ _id: visitorData._id,
968
+ },
969
+ update: {
970
+ updatedBy: currentUser.value._id,
971
+ visitorPassIds: visitorData.visitorPass.map(
972
+ (i: Record<string, any>) => i.keyId
973
+ ),
974
+ nric: visitorData.nric ?? "",
975
+ plateNumber: visitorData.plateNumber ?? "",
976
+ },
977
+ userType: "site",
978
+ });
979
+
980
+ dialog.scanVisitorQRCode = false;
981
+ handleCloseVisitorDataFromScannedQRCodeDialog();
982
+ } catch (error) {
983
+ showMessage(errorConverter(error), "error");
984
+ } finally {
985
+ isVisitorDataFromScannedQRCodeCheckingInOut.value = false;
986
+ }
987
+ }
988
+
989
+ function handleCloseVisitorDataFromScannedQRCodeDialog() {
990
+ dialog.showVisitorDataFromScannedQRCode = false;
991
+ visitorDataFromScannedQRCode.value = {};
992
+ }
993
+
916
994
  function handleAddNew() {
917
995
  dialog.showSelection = true;
918
996
  activeVisitorFormType.value = null;
@@ -0,0 +1,138 @@
1
+ <template>
2
+ <v-dialog v-model="props.dialog" transition="dialog-bottom-transition" width="700" max-width="700" persistent
3
+ @after-enter="handleScanVisitorQRCode">
4
+ <v-container class="d-flex justify-center" max-height="90vh">
5
+ <!-- QR code reader container -->
6
+ <v-card elevation="2" class="d-flex flex-column align-center pa-2 w-100 h-100">
7
+ <v-toolbar>
8
+ <v-card-title class="text-h5"> Scan QR Code </v-card-title>
9
+ <v-spacer />
10
+ <v-btn color="grey-darken-1" icon="mdi-close" @click="closeDialog()" />
11
+ </v-toolbar>
12
+
13
+ <div id="reader" class="d-flex justify-center align-center w-100 h-100"
14
+ style="height: calc(100vh - 64px)">
15
+ <!-- The QR code scanner box -->
16
+ <!-- This is where the QR code scanner will be displayed -->
17
+ </div>
18
+ <!-- Show Error Messages -->
19
+ <div v-if="showNotFoundMessage" class="text-error mt-8">
20
+ <p>{{ errorMessage }}</p>
21
+ </div>
22
+ <!-- Switch camera button inside the reader box -->
23
+ <v-btn color="primary" icon class="mt-4" @click="switchCamera()">
24
+ <v-icon icon="mdi-camera-switch" large />
25
+ </v-btn>
26
+ </v-card>
27
+ </v-container>
28
+ </v-dialog>
29
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
30
+ </template>
31
+ <script setup lang="ts">
32
+ const { $Html5Qrcode }: any = useNuxtApp();
33
+
34
+ const props = defineProps({
35
+ dialog: {
36
+ type: Boolean,
37
+ default: false,
38
+ },
39
+ });
40
+
41
+ const scannedValue = defineModel<string>("scannedValue", {
42
+ default: "",
43
+ });
44
+
45
+ const emit = defineEmits(["closeDialog"]);
46
+
47
+ const message = ref("");
48
+ const messageColor = ref("");
49
+ const messageSnackbar = ref(false);
50
+
51
+ function showMessage(msg: string, color: string) {
52
+ message.value = msg;
53
+ messageColor.value = color;
54
+ messageSnackbar.value = true;
55
+ }
56
+
57
+ const html5QrCodeInstance = ref<any>(null);
58
+ const cameraFacingMode = ref<string>("environment");
59
+
60
+ const showNotFoundMessage = ref<boolean>(false);
61
+ const errorMessage = ref<string>("");
62
+
63
+ const handleScanVisitorQRCode = async () => {
64
+ if (html5QrCodeInstance.value) return;
65
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
66
+ html5QrCodeInstance.value = new $Html5Qrcode("reader");
67
+ html5QrCodeInstance.value
68
+ .start(
69
+ { facingMode: cameraFacingMode.value },
70
+ config,
71
+ async (qrCodeMessage: string) => {
72
+ showNotFoundMessage.value = false;
73
+ scannedValue.value = qrCodeMessage;
74
+ console.log(`QR Code scanned: ${qrCodeMessage}`);
75
+ setTimeout(() => {
76
+ closeDialog();
77
+ }, 100);
78
+ },
79
+ (msg: string) => {
80
+ if (msg === "QR Code no longer in front") {
81
+ showNotFoundMessage.value = true;
82
+ errorMessage.value = msg;
83
+ showMessage(msg, "error");
84
+ }
85
+ }
86
+ )
87
+ .catch((err: any) => {
88
+ showMessage(`Unable to start scanning, error: ${err}`, "error");
89
+ });
90
+ };
91
+
92
+ const isValidUrl = (string: string) => {
93
+ try {
94
+ new URL(string);
95
+ return true;
96
+ } catch (_) {
97
+ return false;
98
+ }
99
+ };
100
+
101
+ const switchCamera = async () => {
102
+ await stopScanner();
103
+ cameraFacingMode.value =
104
+ cameraFacingMode.value === "environment" ? "user" : "environment";
105
+ showMessage(
106
+ `Switched to ${cameraFacingMode.value === "environment" ? "Back" : "Front"
107
+ } Camera`,
108
+ "info"
109
+ );
110
+ handleScanVisitorQRCode();
111
+ };
112
+
113
+ const closeDialog = () => {
114
+ emit("closeDialog");
115
+ stopScanner();
116
+ };
117
+
118
+ const stopScanner = () => {
119
+ return new Promise<void>((resolve, reject) => {
120
+ if (html5QrCodeInstance.value) {
121
+ html5QrCodeInstance.value
122
+ .stop()
123
+ .then(() => {
124
+ html5QrCodeInstance.value.clear();
125
+ html5QrCodeInstance.value = null;
126
+ showNotFoundMessage.value = false;
127
+ resolve(); // Resolve the promise when the scanner is fully stopped
128
+ })
129
+ .catch((err: any) => {
130
+ showMessage("Unable to stop scanning.", err);
131
+ reject(err); // Reject the promise if there's an error
132
+ });
133
+ } else {
134
+ resolve(); // Resolve immediately if no scanner instance exists
135
+ }
136
+ });
137
+ };
138
+ </script>