@7365admin1/layer-common 3.0.27 → 3.0.29

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>
@@ -217,7 +217,7 @@
217
217
  :selected-nfc-cards="passType === 'NFC' ? passCards : []" />
218
218
  </v-col>
219
219
 
220
- <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
220
+ <v-col v-if="formMode === 'add'" cols="12" class="mt-2">
221
221
  <v-checkbox v-model="createMore" density="comfortable" hide-details>
222
222
  <template #label>
223
223
  <span class="text-subtitle-2 font-weight-bold">
@@ -239,11 +239,11 @@
239
239
  <v-row no-gutters>
240
240
  <v-col cols="6">
241
241
  <v-btn v-if="
242
- prop.mode === 'add' &&
242
+ formMode === 'add' &&
243
243
  (withStep2 || withStep3) &&
244
244
  contractorStep > 1
245
245
  " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
246
- <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"
247
247
  size="48" @click="backToSelection" text="Back to Selection" />
248
248
  <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
249
249
  </v-col>
@@ -252,7 +252,7 @@
252
252
  :disabled="processing" @click="handleNextPage" :text="'Next'" />
253
253
  <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
254
254
  :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
255
- :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
255
+ :text="formMode == 'add' ? 'Submit' : 'Update'" />
256
256
  </v-col>
257
257
  </v-row>
258
258
  </v-toolbar>
@@ -264,37 +264,167 @@
264
264
  @update:people="handleAutofillDataViaVehicleNumber" />
265
265
  </v-dialog>
266
266
 
267
- <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="700" persistent>
268
- <v-card :loading="loading.checkingOut">
269
- <v-toolbar>
270
- <v-toolbar-title>
271
- <v-row no-gutters class="d-flex align-center justify-space-between">
272
- <span class="font-weight-bold">
273
- You have an unchecked-out vehicle for: {{ visitor.plateNumber }}
274
- </span>
275
- </v-row>
276
- </v-toolbar-title>
277
- </v-toolbar>
278
-
279
- <v-card-text>
280
- <v-list lines="three">
281
- <v-list-item v-if="matchingPlateNumberNonCheckedOutArr.length > 0"
282
- v-for="v in matchingPlateNumberNonCheckedOutArr" :key="v._id" class="cursor-pointer">
283
- <v-list-item-title>
284
- {{ v.plateNumber }} - {{ v.name }}
285
- </v-list-item-title>
286
-
287
- <v-list-item-subtitle>
288
- Checked In at : {{ UTCToLocalTIme(v.checkIn) }}
289
- </v-list-item-subtitle>
290
-
291
- <template #append>
292
- <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout" :loading="loading.checkingOut && v?._id === prop.visitorData?._id
293
- " @click.stop="handleCheckout(v._id)" />
294
- </template>
295
- </v-list-item>
296
- </v-list>
267
+ <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="1200">
268
+ <v-card :loading="loading.checkingOut || loading.searchingNonCheckedOut">
269
+ <v-card-actions class="justify-end bg-grey-lighten-3 pa-4">
270
+ <v-btn icon="mdi-close" variant="text" size="large" @click="dialog.showNonCheckedOutDialog = false" />
271
+ </v-card-actions>
272
+
273
+ <v-card-text class="pa-6">
274
+ <p class="text-subtitle-1 font-weight-medium mb-8">
275
+ The following list needs to check-out before proceeding to add related to the NRIC/Phone Number below:
276
+ </p>
277
+
278
+ <div class="text-subtitle-1 mb-8">
279
+ <div>NRIC: {{ visitor.nric || "N/A" }}</div>
280
+ <div>Phone Number: {{ visitor.contact || "N/A" }}</div>
281
+ </div>
282
+
283
+ <v-data-table
284
+ class="border rounded"
285
+ :headers="nonCheckedOutVisitorHeaders"
286
+ :items="matchingNonCheckedOutVisitors"
287
+ item-value="_id"
288
+ :items-per-page="5"
289
+ hide-default-footer
290
+ >
291
+ <template #item.name="{ item }">
292
+ <div class="d-flex align-center ga-3">
293
+ <v-avatar color="red" size="32">
294
+ <span class="text-white text-subtitle-2">{{ getVisitorInitial(item.name) }}</span>
295
+ </v-avatar>
296
+ <div>
297
+ <div class="font-weight-medium">{{ item.name || "N/A" }}</div>
298
+ <div class="text-caption">
299
+ <v-icon icon="mdi-card-account-details-outline" size="16" class="mr-1" />
300
+ {{ maskNRIC(item.nric) || "N/A" }}
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </template>
305
+
306
+ <template #item.typeCompany="{ item }">
307
+ <div>
308
+ <v-icon icon="mdi-account" size="18" class="mr-2" />
309
+ {{ formatVisitorType(item.type) }}
310
+ </div>
311
+ <div>
312
+ <v-icon icon="mdi-office-building" size="18" class="mr-2" />
313
+ {{ item.company || "N/A" }}
314
+ </div>
315
+ </template>
316
+
317
+ <template #item.location="{ item }">
318
+ <v-icon icon="mdi-calendar-range" size="18" class="mr-2" />
319
+ {{ formatVisitorLocation(item) }}
320
+ </template>
321
+
322
+ <template #item.contactVehicle="{ item }">
323
+ <div>
324
+ <v-icon icon="mdi-phone" size="18" class="mr-2" />
325
+ {{ item.contact || "N/A" }}
326
+ </div>
327
+ <div>
328
+ <v-icon icon="mdi-car" size="18" class="mr-2" />
329
+ {{ item.plateNumber || "N/A" }}
330
+ </div>
331
+ </template>
332
+
333
+ <template #item.checkInOut="{ item }">
334
+ <div class="py-2">
335
+ <div class="mb-2">
336
+ <v-icon icon="mdi-clock-check-outline" color="green" size="20" class="mr-1" />
337
+ {{ formatNonCheckedOutDate(item.checkIn) }}
338
+ </div>
339
+ <v-btn size="small" class="text-none" color="red" variant="flat" :loading="loading.checkingOut && selectedCheckoutVisitorId === item._id" :disabled="!item._id" @click.stop="item._id && handleCheckout(item._id)">
340
+ Check Out
341
+ </v-btn>
342
+ </div>
343
+ </template>
344
+ </v-data-table>
345
+
346
+ <div class="d-flex justify-end mt-8">
347
+ <v-btn color="red" variant="flat" size="large" class="text-none px-10" :loading="loading.searchingNonCheckedOut" @click="handleSkipNricNonCheckedOut">
348
+ Skip NRIC: {{ visitor.nric || "N/A" }}
349
+ </v-btn>
350
+ </div>
351
+ </v-card-text>
352
+ </v-card>
353
+ </v-dialog>
354
+
355
+ <v-dialog
356
+ v-if="enableUnregistered"
357
+ v-model="dialog.showUnregisteredPlateDialog"
358
+ max-width="900"
359
+ persistent
360
+ >
361
+ <v-card color="background" class="pa-4">
362
+ <v-card-actions class="justify-end pa-0">
363
+ <v-btn
364
+ icon="mdi-close"
365
+ variant="text"
366
+ size="large"
367
+ @click="closeUnregisteredPlateDialog"
368
+ />
369
+ </v-card-actions>
370
+
371
+ <v-card-text class="text-center pt-2">
372
+ <v-icon color="orange" size="96">mdi-alert-circle-outline</v-icon>
373
+
374
+ <div :class="`text-body-${$vuetify.display.xs ? 'mobile' : 'tablet'} mt-8`">
375
+ Vehicle Number {{ unregisteredPlateSearchText }} is similar to below
376
+ <br />
377
+ Unregistered visitors last 24 hours. Please select to
378
+ <br />
379
+ Update
380
+ </div>
381
+
382
+ <v-table class="mt-8 border rounded">
383
+ <thead>
384
+ <tr>
385
+ <th class="text-left">Plate Number</th>
386
+ <th class="text-left">Check In</th>
387
+ <th class="text-center">Action</th>
388
+ </tr>
389
+ </thead>
390
+ <tbody>
391
+ <tr
392
+ v-for="match in unregisteredPlateMatches"
393
+ :key="match._id || match.plateNumber"
394
+ >
395
+ <td class="text-left font-weight-bold text-uppercase">
396
+ {{ match.plateNumber || "N/A" }}
397
+ </td>
398
+ <td class="text-left">
399
+ {{ formatUnregisteredCheckIn(match.checkIn) }}
400
+ </td>
401
+ <td class="text-center">
402
+ <v-btn
403
+ color="red"
404
+ class="text-capitalize"
405
+ density="compact"
406
+ size="large"
407
+ @click="selectUnregisteredPlateMatch(match)"
408
+ >
409
+ Update
410
+ </v-btn>
411
+ </td>
412
+ </tr>
413
+ </tbody>
414
+ </v-table>
297
415
  </v-card-text>
416
+
417
+ <v-card-actions>
418
+ <v-btn
419
+ block
420
+ color="red"
421
+ variant="flat"
422
+ size="x-large"
423
+ @click="closeUnregisteredPlateDialog"
424
+ >
425
+ Cancel & Add New
426
+ </v-btn>
427
+ </v-card-actions>
298
428
  </v-card>
299
429
  </v-dialog>
300
430
  </v-card>
@@ -338,12 +468,16 @@ const prop = defineProps({
338
468
  type: String,
339
469
  default: "",
340
470
  },
471
+ enableUnregistered: {
472
+ type: Boolean,
473
+ default: false,
474
+ }
341
475
  });
342
476
 
343
477
  type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
344
478
  const currentAutofillSource = ref<AutofillSource>(null);
345
479
 
346
- const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
480
+ const { requiredRule, debounce, UTCToLocalTIme, maskNRIC } = useUtils();
347
481
  const { getSiteById } = useSiteSettings();
348
482
  const { currentUser } = useLocalAuth();
349
483
  const { getBlocksSelection, getLevelsSelection, getUnitSelection } =
@@ -352,7 +486,8 @@ const {
352
486
  createVisitor,
353
487
  typeFieldMap,
354
488
  contractorTypes,
355
- getVisitors,
489
+ checkSearchUncheckedOutUnregisteredVisitor,
490
+ searchVisitorObject,
356
491
  updateVisitor,
357
492
  } = useVisitor();
358
493
  const { getBySiteId: getEntryPassSettingsBySiteId } =
@@ -408,10 +543,13 @@ const registeredUnitCompanyName = ref("N/A");
408
543
  const dialog = reactive({
409
544
  vehicleNumberUsersList: false,
410
545
  showNonCheckedOutDialog: false,
546
+ showUnregisteredPlateDialog: false,
411
547
  });
412
548
 
413
549
  const loading = reactive({
414
550
  checkingOut: false,
551
+ checkingUnregisteredPlate: false,
552
+ searchingNonCheckedOut: false,
415
553
  });
416
554
 
417
555
  const validForm = ref(false);
@@ -436,10 +574,28 @@ const deliveryCompany = ref("");
436
574
  const deliveryCompanyInput = ref("");
437
575
  const deliveryCompanyList = ref<string[]>([]);
438
576
 
439
- const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([]);
577
+ const matchingNonCheckedOutVisitors = ref<TVisitor[]>([]);
578
+ const selectedCheckoutVisitorId = ref<string | null>(null);
579
+ const skipNonCheckedOutNric = ref(false);
580
+ const nonCheckedOutVisitorHeaders = [
581
+ { title: "Name/NRIC", key: "name", align: "start", sortable: false },
582
+ { title: "Type / Company", key: "typeCompany", align: "start", sortable: false },
583
+ { title: "Location", key: "location", align: "start", sortable: false },
584
+ { title: "Contact / Vehicle No.", key: "contactVehicle", align: "start", sortable: false },
585
+ { title: "Check In / Out", key: "checkInOut", align: "start", sortable: false, width: "200px" },
586
+ ] as const;
587
+ const unregisteredPlateMatches = ref<TVisitor[]>([]);
588
+ const unregisteredPlateSearchText = ref("");
589
+ const selectedUnregisteredVisitor = ref<TVisitor | null>(null);
590
+ const skipUnregisteredPlateCheck = ref(false);
591
+ const pendingUnregisteredAction = ref<"next" | "submit" | null>(null);
440
592
 
441
593
  const vehicleNumberUserItems = ref<TPeople[]>([]);
442
594
 
595
+ const formMode = computed<"add" | "edit" | "register">(() => {
596
+ return selectedUnregisteredVisitor.value ? "register" : prop.mode;
597
+ });
598
+
443
599
  const shouldShowField = (
444
600
  fieldKey: keyof TVisitorPayload | "delivery-company"
445
601
  ) => {
@@ -467,6 +623,10 @@ const showNextButton = computed(() => {
467
623
  } else return false;
468
624
  });
469
625
 
626
+ const enableUnregistered = computed(() => {
627
+ return prop.mode === "add" && prop.enableUnregistered
628
+ });
629
+
470
630
  const showShowKeysField = computed(() => {
471
631
  return ["guest", "contractor", "walk-in"].includes(prop.type);
472
632
  });
@@ -613,6 +773,7 @@ watch(fetchPersonByNRICReq, (obj) => {
613
773
  watch(
614
774
  () => visitor.nric,
615
775
  (value) => {
776
+ skipNonCheckedOutNric.value = false;
616
777
  if (!value || value.length < 4) {
617
778
  nricSearchMenu.value = false;
618
779
  nricSearchResults.value = [];
@@ -620,6 +781,13 @@ watch(
620
781
  }
621
782
  );
622
783
 
784
+ watch(
785
+ () => visitor.contact,
786
+ () => {
787
+ skipNonCheckedOutNric.value = false;
788
+ }
789
+ );
790
+
623
791
  function selectNricSearchResult(item: Partial<TPeople>) {
624
792
  nricSearchMenu.value = false;
625
793
  nricSearchResults.value = [];
@@ -660,48 +828,89 @@ watch(fetchCompanyListReq, (arr) => {
660
828
  companyNames.value = arr?.flatMap((x) => x?.companyName);
661
829
  }
662
830
  });
663
- const {
664
- data: fetchVisitorListByVehicleNumberReq,
665
- refresh: fetchVisitorListByVehicleNumberRefresh,
666
- pending: fetchVisitorListByVehicleNumberPending,
667
- } = useLazyAsyncData(`fetch-visitor-list-by-vehicle-number`, () => {
668
- if (!visitor.plateNumber) return Promise.resolve(null);
669
- return getVisitors({
670
- page: 1,
671
- limit: 20,
672
- site: prop.site,
673
- plateNumber: visitor.plateNumber,
674
- checkedOut: false,
675
- status: "registered",
676
- });
677
- });
831
+ function getVisitorSearchItems(res: any): TVisitor[] {
832
+ const items =
833
+ res?.items ||
834
+ res?.data?.items ||
835
+ res?.data?.result?.items ||
836
+ res?.result?.items ||
837
+ [];
838
+ return Array.isArray(items) ? items : [];
839
+ }
678
840
 
679
- watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
680
- const itemsArray = arr?.items || [];
681
- const isValidArray = Array.isArray(itemsArray);
682
- matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : [];
683
- if (matchingPlateNumberNonCheckedOutArr.value.length > 0) {
684
- dialog.showNonCheckedOutDialog = true;
685
- } else {
686
- dialog.showNonCheckedOutDialog = false;
841
+ function getVisitorInitial(name?: string | null) {
842
+ return name?.trim()?.charAt(0)?.toUpperCase() || "?";
843
+ }
844
+
845
+ function formatLocationPart(value: unknown) {
846
+ if (!value) return "";
847
+ if (typeof value === "string") return value;
848
+ if (typeof value === "object" && "name" in value) {
849
+ return String((value as { name?: string }).name || "");
687
850
  }
688
- });
851
+ return "";
852
+ }
853
+
854
+ function formatVisitorLocation(v: TVisitor) {
855
+ return [
856
+ formatLocationPart(v.block),
857
+ formatLocationPart(v.level),
858
+ v.unitName || formatLocationPart(v.unit),
859
+ ].filter(Boolean).join(" / ") || "N/A";
860
+ }
861
+
862
+ function formatNonCheckedOutDate(date?: string) {
863
+ return date ? UTCToLocalTIme(date) : "N/A";
864
+ }
865
+
866
+ async function checkNonCheckedOutVisitorsBeforeSubmit(
867
+ options: { skipNric?: boolean; showDialog?: boolean } = {}
868
+ ) {
869
+ const { skipNric = skipNonCheckedOutNric.value, showDialog = true } = options;
870
+ const nric = skipNric ? null : visitor.nric || null;
871
+ const contact = visitor.contact || null;
689
872
 
690
- const debounceSearchVisitorsByPlateNumbers = debounce(() => {
691
- if (!visitor.plateNumber) {
692
- matchingPlateNumberNonCheckedOutArr.value = [];
873
+ if (!nric && !contact) {
874
+ matchingNonCheckedOutVisitors.value = [];
693
875
  dialog.showNonCheckedOutDialog = false;
694
- return;
876
+ return true;
695
877
  }
696
- fetchVisitorListByVehicleNumberRefresh();
697
- }, 300);
698
878
 
699
- watch(
700
- () => visitor.plateNumber,
701
- (newVal) => {
702
- debounceSearchVisitorsByPlateNumbers();
879
+ try {
880
+ loading.searchingNonCheckedOut = true;
881
+ const res = await searchVisitorObject({
882
+ page: 1,
883
+ limit: 1000,
884
+ site: prop.site,
885
+ searchObject: {
886
+ nric,
887
+ contact,
888
+ },
889
+ notCheckedOut: true,
890
+ });
891
+ const items = getVisitorSearchItems(res);
892
+ matchingNonCheckedOutVisitors.value = items;
893
+ dialog.showNonCheckedOutDialog = showDialog && items.length > 0;
894
+ return items.length === 0;
895
+ } catch (error: any) {
896
+ errorMessage.value =
897
+ error?.data?.message ||
898
+ error?.response?._data?.message ||
899
+ "Unable to check unchecked-out visitors. Please try again.";
900
+ return false;
901
+ } finally {
902
+ loading.searchingNonCheckedOut = false;
703
903
  }
704
- );
904
+ }
905
+
906
+ async function handleSkipNricNonCheckedOut() {
907
+ const canSubmit = await checkNonCheckedOutVisitorsBeforeSubmit({
908
+ skipNric: true,
909
+ });
910
+ if (!canSubmit) return;
911
+ skipNonCheckedOutNric.value = true;
912
+ dialog.showNonCheckedOutDialog = false;
913
+ }
705
914
 
706
915
  async function handleCheckout(visitorId: string) {
707
916
  if (!visitorId) {
@@ -711,19 +920,21 @@ async function handleCheckout(visitorId: string) {
711
920
 
712
921
  try {
713
922
  loading.checkingOut = true;
923
+ selectedCheckoutVisitorId.value = visitorId;
714
924
  const res = await updateVisitor(visitorId as string, {
715
925
  checkOut: new Date().toISOString(),
716
926
  });
717
927
  if (res) {
718
- await fetchVisitorListByVehicleNumberRefresh();
928
+ await checkNonCheckedOutVisitorsBeforeSubmit();
719
929
  }
720
930
  } catch (error: any) {
721
- const errorMessage = error?.response?._data?.message;
931
+ const checkoutErrorMessage = error?.response?._data?.message;
722
932
  console.log("[ERROR]", error);
723
933
  errorMessage.value =
724
- errorMessage || "An error occurred while checking out the vehicle.";
934
+ checkoutErrorMessage || "An error occurred while checking out the vehicle.";
725
935
  } finally {
726
936
  loading.checkingOut = false;
937
+ selectedCheckoutVisitorId.value = null;
727
938
  }
728
939
  }
729
940
 
@@ -988,6 +1199,95 @@ function formatVisitorType(type: TVisitorType): string {
988
1199
  }
989
1200
  }
990
1201
 
1202
+ async function checkUnregisteredPlateBeforeContinue(action: "next" | "submit") {
1203
+ if (
1204
+ !enableUnregistered.value ||
1205
+ skipUnregisteredPlateCheck.value ||
1206
+ selectedUnregisteredVisitor.value ||
1207
+ !visitor.plateNumber
1208
+ ) {
1209
+ return true;
1210
+ }
1211
+
1212
+ try {
1213
+ loading.checkingUnregisteredPlate = true;
1214
+ unregisteredPlateSearchText.value = visitor.plateNumber;
1215
+ const res = await checkSearchUncheckedOutUnregisteredVisitor({
1216
+ site: prop.site,
1217
+ plateNumber: visitor.plateNumber,
1218
+ hourAgo: 24,
1219
+ page: 1,
1220
+ limit: 100,
1221
+ });
1222
+ const items = Array.isArray(res?.items)
1223
+ ? res.items
1224
+ : Array.isArray(res?.data?.items)
1225
+ ? res.data.items
1226
+ : Array.isArray(res?.data)
1227
+ ? res.data
1228
+ : Array.isArray(res)
1229
+ ? res
1230
+ : [];
1231
+
1232
+ unregisteredPlateMatches.value = items;
1233
+ if (items.length === 0) return true;
1234
+
1235
+ pendingUnregisteredAction.value = action;
1236
+ dialog.showUnregisteredPlateDialog = true;
1237
+ return false;
1238
+ } catch (error: any) {
1239
+ errorMessage.value =
1240
+ error?.data?.message ||
1241
+ error?.response?._data?.message ||
1242
+ "Unable to check unregistered visitors. Please try again.";
1243
+ return false;
1244
+ } finally {
1245
+ loading.checkingUnregisteredPlate = false;
1246
+ }
1247
+ }
1248
+
1249
+ function formatUnregisteredCheckIn(checkIn?: string) {
1250
+ return checkIn ? UTCToLocalTIme(checkIn) : "N/A";
1251
+ }
1252
+
1253
+ async function continuePendingUnregisteredAction() {
1254
+ const action = pendingUnregisteredAction.value;
1255
+ pendingUnregisteredAction.value = null;
1256
+ if (action === "next") {
1257
+ await handleNextPage();
1258
+ } else if (action === "submit") {
1259
+ await submit();
1260
+ }
1261
+ }
1262
+
1263
+ async function closeUnregisteredPlateDialog() {
1264
+ dialog.showUnregisteredPlateDialog = false;
1265
+ unregisteredPlateMatches.value = [];
1266
+ skipUnregisteredPlateCheck.value = true;
1267
+ }
1268
+
1269
+ async function selectUnregisteredPlateMatch(match: TVisitor) {
1270
+ selectedUnregisteredVisitor.value = match;
1271
+ skipUnregisteredPlateCheck.value = true;
1272
+ dialog.showUnregisteredPlateDialog = false;
1273
+ unregisteredPlateMatches.value = [];
1274
+
1275
+ visitor.plateNumber = match.plateNumber;
1276
+ visitor.attachments = match.attachments || visitor.attachments;
1277
+ visitor.block = match.block || visitor.block;
1278
+ visitor.level = match.level || visitor.level;
1279
+ visitor.unit = match.unit || visitor.unit;
1280
+ visitor.company = match.company || visitor.company;
1281
+ visitor.contractorType = match.contractorType || visitor.contractorType;
1282
+ visitor.name = match.name || visitor.name;
1283
+ visitor.contact = match.contact || visitor.contact;
1284
+ visitor.nric = match.nric || visitor.nric;
1285
+ visitor.company = match.company || visitor.company;
1286
+ visitor.remarks = match.remarks || visitor.remarks;
1287
+
1288
+ pendingUnregisteredAction.value = null;
1289
+ }
1290
+
991
1291
  async function handleNextPage() {
992
1292
  errorMessage.value = "";
993
1293
  formRef.value!.validate();
@@ -1010,11 +1310,12 @@ async function handleNextPage() {
1010
1310
  errorMessage.value = "Please select at least one NFC card before proceeding.";
1011
1311
  return;
1012
1312
  }
1313
+ if (!(await checkUnregisteredPlateBeforeContinue("next"))) return;
1013
1314
  if (step < 3) contractorStep.value++;
1014
1315
  else await submit();
1015
1316
  }
1016
1317
 
1017
- async function submit() {
1318
+ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1018
1319
  formRef.value!.validate();
1019
1320
  errorMessage.value = "";
1020
1321
  if (!validForm.value) {
@@ -1023,6 +1324,14 @@ async function submit() {
1023
1324
  }
1024
1325
 
1025
1326
  if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1327
+ if (
1328
+ prop.mode === "add" &&
1329
+ !options.skipNonCheckedOutCheck &&
1330
+ !(await checkNonCheckedOutVisitorsBeforeSubmit())
1331
+ ) {
1332
+ return;
1333
+ }
1334
+ if (!(await checkUnregisteredPlateBeforeContinue("submit"))) return;
1026
1335
 
1027
1336
  processing.value = true;
1028
1337
 
@@ -1033,7 +1342,7 @@ async function submit() {
1033
1342
  createdBy: prop.currentUser,
1034
1343
  };
1035
1344
 
1036
- if (prop.mode === "add" || prop.mode === "register") {
1345
+ if (formMode.value === "add" || formMode.value === "register") {
1037
1346
  const allowedFields = typeFieldMap[prop.type];
1038
1347
  for (const key of allowedFields as (keyof TVisitorPayload)[]) {
1039
1348
  if (visitor[key] !== undefined) {
@@ -1071,7 +1380,7 @@ async function submit() {
1071
1380
  }
1072
1381
 
1073
1382
  // add register status when registering unregistered visitor
1074
- if (prop.mode === "register") {
1383
+ if (formMode.value === "register") {
1075
1384
  payload.status = "registered";
1076
1385
  const currentUserId = currentUser.value?._id;
1077
1386
  if(currentUserId) {
@@ -1081,7 +1390,18 @@ async function submit() {
1081
1390
 
1082
1391
  try {
1083
1392
  let res;
1084
- if (prop.mode === "add") {
1393
+ if (prop.mode === "add" && selectedUnregisteredVisitor.value?._id) {
1394
+ payload.status = "registered";
1395
+ const currentUserId = currentUser.value?._id;
1396
+ if (currentUserId) {
1397
+ payload.updatedBy = currentUserId;
1398
+ }
1399
+ const { unitName, ...rest } = payload;
1400
+ res = await updateVisitor(
1401
+ selectedUnregisteredVisitor.value._id,
1402
+ rest as TVisitorPayload
1403
+ );
1404
+ } else if (formMode.value === "add") {
1085
1405
  res = await createVisitor(payload as TVisitorPayload);
1086
1406
  } else if (
1087
1407
  (prop.mode === "edit" || prop.mode === "register") &&
@@ -1224,7 +1544,7 @@ async function submit() {
1224
1544
  const err = error?.data?.message;
1225
1545
  errorMessage.value =
1226
1546
  err ||
1227
- `Failed to ${prop.mode === "add" ? "add" : "update"
1547
+ `Failed to ${formMode.value === "add" ? "add" : "update"
1228
1548
  } visitor. Please try again.`;
1229
1549
  } finally {
1230
1550
  processing.value = false;
@@ -1368,7 +1688,22 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
1368
1688
  watch(
1369
1689
  () => visitor.plateNumber,
1370
1690
  (newVal) => {
1371
- visitor.plateNumber = newVal?.toUpperCase();
1691
+ const normalizedPlateNumber = newVal?.toUpperCase();
1692
+ if (newVal !== normalizedPlateNumber) {
1693
+ visitor.plateNumber = normalizedPlateNumber;
1694
+ return;
1695
+ }
1696
+
1697
+ if (
1698
+ selectedUnregisteredVisitor.value &&
1699
+ normalizedPlateNumber !== selectedUnregisteredVisitor.value.plateNumber?.toUpperCase()
1700
+ ) {
1701
+ selectedUnregisteredVisitor.value = null;
1702
+ }
1703
+
1704
+ skipUnregisteredPlateCheck.value = false;
1705
+ unregisteredPlateMatches.value = [];
1706
+ dialog.showUnregisteredPlateDialog = false;
1372
1707
  }
1373
1708
  );
1374
1709