@7365admin1/layer-common 3.0.26 → 3.0.28

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.
@@ -3,7 +3,7 @@
3
3
  <v-toolbar>
4
4
  <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
5
5
  <span class="font-weight-bold text-h5 text-capitalize">
6
- {{ prop.mode }} {{ formatVisitorType(type) }}
6
+ {{ formMode }} {{ formatVisitorType(type) }}
7
7
  </span>
8
8
  <ButtonClose @click="emit('close:all')" />
9
9
  </v-row>
@@ -204,15 +204,20 @@
204
204
  v-model:cards="passCards" :settings="entryPassSettings"
205
205
  :loading="entryPassSettingsPending || siteDataPending" :site-id="prop.site"
206
206
  :unit-id="visitor.unit || null" :visitor-type="prop.type" :hid-qr-code-enabled="hidQrCodePassAllowed"
207
- :hid-qr-printer-configured="hasHidQrPrinterConfig" />
207
+ :hid-qr-printer-configured="hasHidQrPrinterConfig"
208
+ @update:available-qr-count="(val) => availableQrCount = val" />
208
209
  </v-col>
209
210
 
210
211
  <v-col v-show="withStep3 && contractorStep === 3" cols="12">
211
212
  <MemberInformation v-model="visitor.members" :type="prop.type" :contractor-type="visitor.contractorType"
212
- :site="prop.site" :selected-visitor-pass="visitor.visitorPass" :clearable="true" />
213
+ :site="prop.site" :selected-visitor-pass="visitor.visitorPass" :clearable="true"
214
+ :settings="entryPassSettings" :settings-loading="entryPassSettingsPending || siteDataPending"
215
+ :unit-id="visitor.unit || null"
216
+ :hid-qr-code-enabled="hidQrCodePassAllowed" :hid-qr-printer-configured="hasHidQrPrinterConfig"
217
+ :selected-nfc-cards="passType === 'NFC' ? passCards : []" />
213
218
  </v-col>
214
219
 
215
- <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
220
+ <v-col v-if="formMode === 'add'" cols="12" class="mt-2">
216
221
  <v-checkbox v-model="createMore" density="comfortable" hide-details>
217
222
  <template #label>
218
223
  <span class="text-subtitle-2 font-weight-bold">
@@ -234,11 +239,11 @@
234
239
  <v-row no-gutters>
235
240
  <v-col cols="6">
236
241
  <v-btn v-if="
237
- prop.mode === 'add' &&
242
+ formMode === 'add' &&
238
243
  (withStep2 || withStep3) &&
239
244
  contractorStep > 1
240
245
  " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
241
- <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
246
+ <v-btn v-else-if="formMode === 'add' || formMode === 'register'" tile block variant="text" class="text-none"
242
247
  size="48" @click="backToSelection" text="Back to Selection" />
243
248
  <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
244
249
  </v-col>
@@ -247,7 +252,7 @@
247
252
  :disabled="processing" @click="handleNextPage" :text="'Next'" />
248
253
  <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
249
254
  :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
250
- :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
255
+ :text="formMode == 'add' ? 'Submit' : 'Update'" />
251
256
  </v-col>
252
257
  </v-row>
253
258
  </v-toolbar>
@@ -292,6 +297,82 @@
292
297
  </v-card-text>
293
298
  </v-card>
294
299
  </v-dialog>
300
+
301
+ <v-dialog
302
+ v-if="enableUnregistered"
303
+ v-model="dialog.showUnregisteredPlateDialog"
304
+ max-width="900"
305
+ persistent
306
+ >
307
+ <v-card color="background" class="pa-4">
308
+ <v-card-actions class="justify-end pa-0">
309
+ <v-btn
310
+ icon="mdi-close"
311
+ variant="text"
312
+ size="large"
313
+ @click="closeUnregisteredPlateDialog"
314
+ />
315
+ </v-card-actions>
316
+
317
+ <v-card-text class="text-center pt-2">
318
+ <v-icon color="orange" size="96">mdi-alert-circle-outline</v-icon>
319
+
320
+ <div :class="`text-body-${$vuetify.display.xs ? 'mobile' : 'tablet'} mt-8`">
321
+ Vehicle Number {{ unregisteredPlateSearchText }} is similar to below
322
+ <br />
323
+ Unregistered visitors last 24 hours. Please select to
324
+ <br />
325
+ Update
326
+ </div>
327
+
328
+ <v-table class="mt-8 border rounded">
329
+ <thead>
330
+ <tr>
331
+ <th class="text-left">Plate Number</th>
332
+ <th class="text-left">Check In</th>
333
+ <th class="text-center">Action</th>
334
+ </tr>
335
+ </thead>
336
+ <tbody>
337
+ <tr
338
+ v-for="match in unregisteredPlateMatches"
339
+ :key="match._id || match.plateNumber"
340
+ >
341
+ <td class="text-left font-weight-bold text-uppercase">
342
+ {{ match.plateNumber || "N/A" }}
343
+ </td>
344
+ <td class="text-left">
345
+ {{ formatUnregisteredCheckIn(match.checkIn) }}
346
+ </td>
347
+ <td class="text-center">
348
+ <v-btn
349
+ color="red"
350
+ class="text-capitalize"
351
+ density="compact"
352
+ size="large"
353
+ @click="selectUnregisteredPlateMatch(match)"
354
+ >
355
+ Update
356
+ </v-btn>
357
+ </td>
358
+ </tr>
359
+ </tbody>
360
+ </v-table>
361
+ </v-card-text>
362
+
363
+ <v-card-actions>
364
+ <v-btn
365
+ block
366
+ color="red"
367
+ variant="flat"
368
+ size="x-large"
369
+ @click="closeUnregisteredPlateDialog"
370
+ >
371
+ Cancel & Add New
372
+ </v-btn>
373
+ </v-card-actions>
374
+ </v-card>
375
+ </v-dialog>
295
376
  </v-card>
296
377
  </template>
297
378
 
@@ -348,6 +429,7 @@ const {
348
429
  typeFieldMap,
349
430
  contractorTypes,
350
431
  getVisitors,
432
+ checkSearchUncheckedOutUnregisteredVisitor,
351
433
  updateVisitor,
352
434
  } = useVisitor();
353
435
  const { getBySiteId: getEntryPassSettingsBySiteId } =
@@ -394,6 +476,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
394
476
  const passType = ref<string | null>(null);
395
477
  const passQuantity = ref<number | null>(1);
396
478
  const passCards = ref<{ _id: string; cardNo: string }[]>([]);
479
+ const availableQrCount = ref<number | null>(null);
397
480
  const hidQrPrintPayload = ref<string | null>(null);
398
481
  const hidQrPrinted = ref(false);
399
482
 
@@ -402,10 +485,12 @@ const registeredUnitCompanyName = ref("N/A");
402
485
  const dialog = reactive({
403
486
  vehicleNumberUsersList: false,
404
487
  showNonCheckedOutDialog: false,
488
+ showUnregisteredPlateDialog: false,
405
489
  });
406
490
 
407
491
  const loading = reactive({
408
492
  checkingOut: false,
493
+ checkingUnregisteredPlate: false,
409
494
  });
410
495
 
411
496
  const validForm = ref(false);
@@ -431,9 +516,18 @@ const deliveryCompanyInput = ref("");
431
516
  const deliveryCompanyList = ref<string[]>([]);
432
517
 
433
518
  const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([]);
519
+ const unregisteredPlateMatches = ref<TVisitor[]>([]);
520
+ const unregisteredPlateSearchText = ref("");
521
+ const selectedUnregisteredVisitor = ref<TVisitor | null>(null);
522
+ const skipUnregisteredPlateCheck = ref(false);
523
+ const pendingUnregisteredAction = ref<"next" | "submit" | null>(null);
434
524
 
435
525
  const vehicleNumberUserItems = ref<TPeople[]>([]);
436
526
 
527
+ const formMode = computed<"add" | "edit" | "register">(() => {
528
+ return selectedUnregisteredVisitor.value ? "register" : prop.mode;
529
+ });
530
+
437
531
  const shouldShowField = (
438
532
  fieldKey: keyof TVisitorPayload | "delivery-company"
439
533
  ) => {
@@ -461,6 +555,10 @@ const showNextButton = computed(() => {
461
555
  } else return false;
462
556
  });
463
557
 
558
+ const enableUnregistered = computed(() => {
559
+ return prop.mode === "add" && shouldShowField("plateNumber");
560
+ });
561
+
464
562
  const showShowKeysField = computed(() => {
465
563
  return ["guest", "contractor", "walk-in"].includes(prop.type);
466
564
  });
@@ -628,10 +726,10 @@ function selectNricSearchResult(item: Partial<TPeople>) {
628
726
  if (!visitor.company) {
629
727
  visitor.company = companyNames.value?.[0] || visitor.company;
630
728
  }
631
- visitor.block = (item.block as any) ?? (visitor.block as any) ?? "";
632
- visitor.level = (item.level as any) ?? (visitor.level as any) ?? "";
633
- visitor.unit = item.unit ?? visitor.unit ?? "";
634
- visitor.unitName = item.unitName ?? visitor.unitName;
729
+ // visitor.block = (item.block as any) ?? (visitor.block as any) ?? "";
730
+ // visitor.level = (item.level as any) ?? (visitor.level as any) ?? "";
731
+ // visitor.unit = item.unit ?? visitor.unit ?? "";
732
+ // visitor.unitName = item.unitName ?? visitor.unitName;
635
733
 
636
734
  setTimeout(() => {
637
735
  currentAutofillSource.value = null;
@@ -690,12 +788,16 @@ const debounceSearchVisitorsByPlateNumbers = debounce(() => {
690
788
  fetchVisitorListByVehicleNumberRefresh();
691
789
  }, 300);
692
790
 
693
- watch(
694
- () => visitor.plateNumber,
695
- (newVal) => {
696
- debounceSearchVisitorsByPlateNumbers();
697
- }
698
- );
791
+ // watch(
792
+ // () => visitor.plateNumber,
793
+ // (newVal) => {
794
+ // skipUnregisteredPlateCheck.value = false;
795
+ // selectedUnregisteredVisitor.value = null;
796
+ // unregisteredPlateMatches.value = [];
797
+ // dialog.showUnregisteredPlateDialog = false;
798
+ // debounceSearchVisitorsByPlateNumbers();
799
+ // }
800
+ // );
699
801
 
700
802
  async function handleCheckout(visitorId: string) {
701
803
  if (!visitorId) {
@@ -982,6 +1084,95 @@ function formatVisitorType(type: TVisitorType): string {
982
1084
  }
983
1085
  }
984
1086
 
1087
+ async function checkUnregisteredPlateBeforeContinue(action: "next" | "submit") {
1088
+ if (
1089
+ !enableUnregistered.value ||
1090
+ skipUnregisteredPlateCheck.value ||
1091
+ selectedUnregisteredVisitor.value ||
1092
+ !visitor.plateNumber
1093
+ ) {
1094
+ return true;
1095
+ }
1096
+
1097
+ try {
1098
+ loading.checkingUnregisteredPlate = true;
1099
+ unregisteredPlateSearchText.value = visitor.plateNumber;
1100
+ const res = await checkSearchUncheckedOutUnregisteredVisitor({
1101
+ site: prop.site,
1102
+ plateNumber: visitor.plateNumber,
1103
+ hourAgo: 24,
1104
+ page: 1,
1105
+ limit: 100,
1106
+ });
1107
+ const items = Array.isArray(res?.items)
1108
+ ? res.items
1109
+ : Array.isArray(res?.data?.items)
1110
+ ? res.data.items
1111
+ : Array.isArray(res?.data)
1112
+ ? res.data
1113
+ : Array.isArray(res)
1114
+ ? res
1115
+ : [];
1116
+
1117
+ unregisteredPlateMatches.value = items;
1118
+ if (items.length === 0) return true;
1119
+
1120
+ pendingUnregisteredAction.value = action;
1121
+ dialog.showUnregisteredPlateDialog = true;
1122
+ return false;
1123
+ } catch (error: any) {
1124
+ errorMessage.value =
1125
+ error?.data?.message ||
1126
+ error?.response?._data?.message ||
1127
+ "Unable to check unregistered visitors. Please try again.";
1128
+ return false;
1129
+ } finally {
1130
+ loading.checkingUnregisteredPlate = false;
1131
+ }
1132
+ }
1133
+
1134
+ function formatUnregisteredCheckIn(checkIn?: string) {
1135
+ return checkIn ? UTCToLocalTIme(checkIn) : "N/A";
1136
+ }
1137
+
1138
+ async function continuePendingUnregisteredAction() {
1139
+ const action = pendingUnregisteredAction.value;
1140
+ pendingUnregisteredAction.value = null;
1141
+ if (action === "next") {
1142
+ await handleNextPage();
1143
+ } else if (action === "submit") {
1144
+ await submit();
1145
+ }
1146
+ }
1147
+
1148
+ async function closeUnregisteredPlateDialog() {
1149
+ dialog.showUnregisteredPlateDialog = false;
1150
+ unregisteredPlateMatches.value = [];
1151
+ skipUnregisteredPlateCheck.value = true;
1152
+ }
1153
+
1154
+ async function selectUnregisteredPlateMatch(match: TVisitor) {
1155
+ selectedUnregisteredVisitor.value = match;
1156
+ skipUnregisteredPlateCheck.value = true;
1157
+ dialog.showUnregisteredPlateDialog = false;
1158
+ unregisteredPlateMatches.value = [];
1159
+
1160
+ visitor.plateNumber = match.plateNumber;
1161
+ visitor.attachments = match.attachments || visitor.attachments;
1162
+ visitor.block = match.block || visitor.block;
1163
+ visitor.level = match.level || visitor.level;
1164
+ visitor.unit = match.unit || visitor.unit;
1165
+ visitor.company = match.company || visitor.company;
1166
+ visitor.contractorType = match.contractorType || visitor.contractorType;
1167
+ visitor.name = match.name || visitor.name;
1168
+ visitor.contact = match.contact || visitor.contact;
1169
+ visitor.nric = match.nric || visitor.nric;
1170
+ visitor.company = match.company || visitor.company;
1171
+ visitor.remarks = match.remarks || visitor.remarks;
1172
+
1173
+ pendingUnregisteredAction.value = null;
1174
+ }
1175
+
985
1176
  async function handleNextPage() {
986
1177
  errorMessage.value = "";
987
1178
  formRef.value!.validate();
@@ -990,10 +1181,21 @@ async function handleNextPage() {
990
1181
  return;
991
1182
  }
992
1183
  const step = contractorStep.value;
1184
+ if (step === 2 && passType.value === "QR") {
1185
+ if (availableQrCount.value === null) {
1186
+ errorMessage.value = "Please wait while QR passes are being generated...";
1187
+ return;
1188
+ }
1189
+ if (availableQrCount.value === 0) {
1190
+ errorMessage.value = "No QR passes available. Add a non-physical card to this unit in Access Management first.";
1191
+ return;
1192
+ }
1193
+ }
993
1194
  if (step === 2 && passType.value === "NFC" && passCards.value.length === 0) {
994
1195
  errorMessage.value = "Please select at least one NFC card before proceeding.";
995
1196
  return;
996
1197
  }
1198
+ if (!(await checkUnregisteredPlateBeforeContinue("next"))) return;
997
1199
  if (step < 3) contractorStep.value++;
998
1200
  else await submit();
999
1201
  }
@@ -1007,6 +1209,7 @@ async function submit() {
1007
1209
  }
1008
1210
 
1009
1211
  if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1212
+ if (!(await checkUnregisteredPlateBeforeContinue("submit"))) return;
1010
1213
 
1011
1214
  processing.value = true;
1012
1215
 
@@ -1017,7 +1220,7 @@ async function submit() {
1017
1220
  createdBy: prop.currentUser,
1018
1221
  };
1019
1222
 
1020
- if (prop.mode === "add" || prop.mode === "register") {
1223
+ if (formMode.value === "add" || formMode.value === "register") {
1021
1224
  const allowedFields = typeFieldMap[prop.type];
1022
1225
  for (const key of allowedFields as (keyof TVisitorPayload)[]) {
1023
1226
  if (visitor[key] !== undefined) {
@@ -1055,7 +1258,7 @@ async function submit() {
1055
1258
  }
1056
1259
 
1057
1260
  // add register status when registering unregistered visitor
1058
- if (prop.mode === "register") {
1261
+ if (formMode.value === "register") {
1059
1262
  payload.status = "registered";
1060
1263
  const currentUserId = currentUser.value?._id;
1061
1264
  if(currentUserId) {
@@ -1065,7 +1268,18 @@ async function submit() {
1065
1268
 
1066
1269
  try {
1067
1270
  let res;
1068
- if (prop.mode === "add") {
1271
+ if (prop.mode === "add" && selectedUnregisteredVisitor.value?._id) {
1272
+ payload.status = "registered";
1273
+ const currentUserId = currentUser.value?._id;
1274
+ if (currentUserId) {
1275
+ payload.updatedBy = currentUserId;
1276
+ }
1277
+ const { unitName, ...rest } = payload;
1278
+ res = await updateVisitor(
1279
+ selectedUnregisteredVisitor.value._id,
1280
+ rest as TVisitorPayload
1281
+ );
1282
+ } else if (formMode.value === "add") {
1069
1283
  res = await createVisitor(payload as TVisitorPayload);
1070
1284
  } else if (
1071
1285
  (prop.mode === "edit" || prop.mode === "register") &&
@@ -1208,7 +1422,7 @@ async function submit() {
1208
1422
  const err = error?.data?.message;
1209
1423
  errorMessage.value =
1210
1424
  err ||
1211
- `Failed to ${prop.mode === "add" ? "add" : "update"
1425
+ `Failed to ${formMode.value === "add" ? "add" : "update"
1212
1426
  } visitor. Please try again.`;
1213
1427
  } finally {
1214
1428
  processing.value = false;
@@ -1352,7 +1566,22 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
1352
1566
  watch(
1353
1567
  () => visitor.plateNumber,
1354
1568
  (newVal) => {
1355
- visitor.plateNumber = newVal?.toUpperCase();
1569
+ const normalizedPlateNumber = newVal?.toUpperCase();
1570
+ if (newVal !== normalizedPlateNumber) {
1571
+ visitor.plateNumber = normalizedPlateNumber;
1572
+ return;
1573
+ }
1574
+
1575
+ if (
1576
+ selectedUnregisteredVisitor.value &&
1577
+ normalizedPlateNumber !== selectedUnregisteredVisitor.value.plateNumber?.toUpperCase()
1578
+ ) {
1579
+ selectedUnregisteredVisitor.value = null;
1580
+ }
1581
+
1582
+ skipUnregisteredPlateCheck.value = false;
1583
+ unregisteredPlateMatches.value = [];
1584
+ dialog.showUnregisteredPlateDialog = false;
1356
1585
  }
1357
1586
  );
1358
1587