@7365admin1/layer-common 3.0.28 → 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.
@@ -264,36 +264,90 @@
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>
297
351
  </v-card-text>
298
352
  </v-card>
299
353
  </v-dialog>
@@ -414,12 +468,16 @@ const prop = defineProps({
414
468
  type: String,
415
469
  default: "",
416
470
  },
471
+ enableUnregistered: {
472
+ type: Boolean,
473
+ default: false,
474
+ }
417
475
  });
418
476
 
419
477
  type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
420
478
  const currentAutofillSource = ref<AutofillSource>(null);
421
479
 
422
- const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
480
+ const { requiredRule, debounce, UTCToLocalTIme, maskNRIC } = useUtils();
423
481
  const { getSiteById } = useSiteSettings();
424
482
  const { currentUser } = useLocalAuth();
425
483
  const { getBlocksSelection, getLevelsSelection, getUnitSelection } =
@@ -428,8 +486,8 @@ const {
428
486
  createVisitor,
429
487
  typeFieldMap,
430
488
  contractorTypes,
431
- getVisitors,
432
489
  checkSearchUncheckedOutUnregisteredVisitor,
490
+ searchVisitorObject,
433
491
  updateVisitor,
434
492
  } = useVisitor();
435
493
  const { getBySiteId: getEntryPassSettingsBySiteId } =
@@ -491,6 +549,7 @@ const dialog = reactive({
491
549
  const loading = reactive({
492
550
  checkingOut: false,
493
551
  checkingUnregisteredPlate: false,
552
+ searchingNonCheckedOut: false,
494
553
  });
495
554
 
496
555
  const validForm = ref(false);
@@ -515,7 +574,16 @@ const deliveryCompany = ref("");
515
574
  const deliveryCompanyInput = ref("");
516
575
  const deliveryCompanyList = ref<string[]>([]);
517
576
 
518
- 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;
519
587
  const unregisteredPlateMatches = ref<TVisitor[]>([]);
520
588
  const unregisteredPlateSearchText = ref("");
521
589
  const selectedUnregisteredVisitor = ref<TVisitor | null>(null);
@@ -556,7 +624,7 @@ const showNextButton = computed(() => {
556
624
  });
557
625
 
558
626
  const enableUnregistered = computed(() => {
559
- return prop.mode === "add" && shouldShowField("plateNumber");
627
+ return prop.mode === "add" && prop.enableUnregistered
560
628
  });
561
629
 
562
630
  const showShowKeysField = computed(() => {
@@ -705,6 +773,7 @@ watch(fetchPersonByNRICReq, (obj) => {
705
773
  watch(
706
774
  () => visitor.nric,
707
775
  (value) => {
776
+ skipNonCheckedOutNric.value = false;
708
777
  if (!value || value.length < 4) {
709
778
  nricSearchMenu.value = false;
710
779
  nricSearchResults.value = [];
@@ -712,6 +781,13 @@ watch(
712
781
  }
713
782
  );
714
783
 
784
+ watch(
785
+ () => visitor.contact,
786
+ () => {
787
+ skipNonCheckedOutNric.value = false;
788
+ }
789
+ );
790
+
715
791
  function selectNricSearchResult(item: Partial<TPeople>) {
716
792
  nricSearchMenu.value = false;
717
793
  nricSearchResults.value = [];
@@ -752,52 +828,89 @@ watch(fetchCompanyListReq, (arr) => {
752
828
  companyNames.value = arr?.flatMap((x) => x?.companyName);
753
829
  }
754
830
  });
755
- const {
756
- data: fetchVisitorListByVehicleNumberReq,
757
- refresh: fetchVisitorListByVehicleNumberRefresh,
758
- pending: fetchVisitorListByVehicleNumberPending,
759
- } = useLazyAsyncData(`fetch-visitor-list-by-vehicle-number`, () => {
760
- if (!visitor.plateNumber) return Promise.resolve(null);
761
- return getVisitors({
762
- page: 1,
763
- limit: 20,
764
- site: prop.site,
765
- plateNumber: visitor.plateNumber,
766
- checkedOut: false,
767
- status: "registered",
768
- });
769
- });
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
+ }
770
840
 
771
- watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
772
- const itemsArray = arr?.items || [];
773
- const isValidArray = Array.isArray(itemsArray);
774
- matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : [];
775
- if (matchingPlateNumberNonCheckedOutArr.value.length > 0) {
776
- dialog.showNonCheckedOutDialog = true;
777
- } else {
778
- 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 || "");
779
850
  }
780
- });
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;
781
872
 
782
- const debounceSearchVisitorsByPlateNumbers = debounce(() => {
783
- if (!visitor.plateNumber) {
784
- matchingPlateNumberNonCheckedOutArr.value = [];
873
+ if (!nric && !contact) {
874
+ matchingNonCheckedOutVisitors.value = [];
785
875
  dialog.showNonCheckedOutDialog = false;
786
- return;
876
+ return true;
877
+ }
878
+
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;
787
903
  }
788
- fetchVisitorListByVehicleNumberRefresh();
789
- }, 300);
790
-
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
- // );
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
+ }
801
914
 
802
915
  async function handleCheckout(visitorId: string) {
803
916
  if (!visitorId) {
@@ -807,19 +920,21 @@ async function handleCheckout(visitorId: string) {
807
920
 
808
921
  try {
809
922
  loading.checkingOut = true;
923
+ selectedCheckoutVisitorId.value = visitorId;
810
924
  const res = await updateVisitor(visitorId as string, {
811
925
  checkOut: new Date().toISOString(),
812
926
  });
813
927
  if (res) {
814
- await fetchVisitorListByVehicleNumberRefresh();
928
+ await checkNonCheckedOutVisitorsBeforeSubmit();
815
929
  }
816
930
  } catch (error: any) {
817
- const errorMessage = error?.response?._data?.message;
931
+ const checkoutErrorMessage = error?.response?._data?.message;
818
932
  console.log("[ERROR]", error);
819
933
  errorMessage.value =
820
- errorMessage || "An error occurred while checking out the vehicle.";
934
+ checkoutErrorMessage || "An error occurred while checking out the vehicle.";
821
935
  } finally {
822
936
  loading.checkingOut = false;
937
+ selectedCheckoutVisitorId.value = null;
823
938
  }
824
939
  }
825
940
 
@@ -1200,7 +1315,7 @@ async function handleNextPage() {
1200
1315
  else await submit();
1201
1316
  }
1202
1317
 
1203
- async function submit() {
1318
+ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1204
1319
  formRef.value!.validate();
1205
1320
  errorMessage.value = "";
1206
1321
  if (!validForm.value) {
@@ -1209,6 +1324,13 @@ async function submit() {
1209
1324
  }
1210
1325
 
1211
1326
  if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1327
+ if (
1328
+ prop.mode === "add" &&
1329
+ !options.skipNonCheckedOutCheck &&
1330
+ !(await checkNonCheckedOutVisitorsBeforeSubmit())
1331
+ ) {
1332
+ return;
1333
+ }
1212
1334
  if (!(await checkUnregisteredPlateBeforeContinue("submit"))) return;
1213
1335
 
1214
1336
  processing.value = true;
@@ -579,6 +579,7 @@
579
579
  :visitor-data="selectedVisitorDataObject"
580
580
  :current-user="currentUser._id"
581
581
  :type="activeVisitorFormType"
582
+ :enable-unregistered="enableUnregistered"
582
583
  @back="handleClickBack"
583
584
  @done="handleVisitorFormDone"
584
585
  @done:more="handleVisitorFormCreateMore"
@@ -1824,7 +1825,7 @@ const nfcCardReturnStatuses = ref<
1824
1825
  const canConfirmCheckout = computed(() => {
1825
1826
  const allEntries = [...passReturnStatuses.value, ...keyReturnStatuses.value];
1826
1827
  return allEntries.every((entry) => {
1827
- if (!entry.status || entry.status === "In Use") return false;
1828
+ if (!entry.status || ["In Use","Not Returned"].includes(entry.status)) return false;
1828
1829
  if (
1829
1830
  (entry.status === "Lost" || entry.status === "Damaged") &&
1830
1831
  !entry.remarks.trim()
@@ -2190,29 +2191,60 @@ watch([searchInput, dateFrom, dateTo, filterTypes, activeTab], () => {
2190
2191
  page.value = 1;
2191
2192
  });
2192
2193
 
2193
- onMounted(() => {
2194
+ const syncVisitorFiltersFromRoute = () => {
2194
2195
  const requestedTab = (route.query.tab as string) || "registered";
2195
- activeTab.value = enableUnregisteredTab.value
2196
+ const nextTab = enableUnregisteredTab.value
2196
2197
  ? requestedTab
2197
2198
  : requestedTab === "unregistered"
2198
2199
  ? "registered"
2199
2200
  : requestedTab;
2200
- searchInput.value = (route.query.search as string) || "";
2201
+
2202
+ if (activeTab.value !== nextTab) {
2203
+ activeTab.value = nextTab;
2204
+ }
2205
+
2206
+ const nextSearch = (route.query.search as string) || "";
2207
+ if (searchInput.value !== nextSearch) {
2208
+ searchInput.value = nextSearch;
2209
+ }
2210
+
2201
2211
  if (route.query.dateFrom) {
2202
- dateFrom.value = normalizeDateOnly(route.query.dateFrom as string);
2212
+ const nextDateFrom = normalizeDateOnly(route.query.dateFrom as string);
2213
+ if (dateFrom.value !== nextDateFrom) {
2214
+ dateFrom.value = nextDateFrom;
2215
+ }
2203
2216
  }
2217
+
2204
2218
  if (route.query.dateTo) {
2205
- dateTo.value = normalizeDateOnly(route.query.dateTo as string);
2219
+ const nextDateTo = normalizeDateOnly(route.query.dateTo as string);
2220
+ if (dateTo.value !== nextDateTo) {
2221
+ dateTo.value = nextDateTo;
2222
+ }
2206
2223
  }
2224
+
2207
2225
  if (activeTab.value === "registered") {
2208
- filterTypes.value = ((route.query.type as string)?.split(",") || []).filter(
2209
- Boolean
2210
- ) as TVisitorType[];
2226
+ const nextFilterTypes = (
2227
+ (route.query.type as string)?.split(",") || []
2228
+ ).filter(Boolean) as TVisitorType[];
2229
+ if (filterTypes.value.join(",") !== nextFilterTypes.join(",")) {
2230
+ filterTypes.value = nextFilterTypes;
2231
+ }
2211
2232
 
2212
- displayNotCheckedOut.value =
2233
+ const nextDisplayNotCheckedOut =
2213
2234
  (route.query.checkedOut as string) == "true" || false;
2235
+ if (displayNotCheckedOut.value !== nextDisplayNotCheckedOut) {
2236
+ displayNotCheckedOut.value = nextDisplayNotCheckedOut;
2237
+ }
2214
2238
  }
2215
- });
2239
+ };
2240
+
2241
+ onMounted(syncVisitorFiltersFromRoute);
2242
+
2243
+ watch(
2244
+ () => route.query,
2245
+ syncVisitorFiltersFromRoute,
2246
+ { deep: true }
2247
+ );
2216
2248
 
2217
2249
  const { socketUnregisteredVisitorTrigger, visitorSocketData } =
2218
2250
  useVisitorSocket();
@@ -314,7 +314,7 @@ export default function useAccessManagement() {
314
314
  );
315
315
  }
316
316
 
317
- function assignUser(payload: { assignees: string[]; unit: string; type: string; acm_url: string }) {
317
+ function assignUser(payload: { assignees: string[]; unit: string; type: string; acm_url: string; id?: string }) {
318
318
  return useNuxtApp().$api<Record<string, any>>(
319
319
  `/api/access-management/assign-user`,
320
320
  { method: "POST", body: payload }
@@ -102,6 +102,17 @@ export default function () {
102
102
  limit?: number;
103
103
  };
104
104
 
105
+ type SearchVisitorObjectParams = {
106
+ page?: number;
107
+ limit?: number;
108
+ site: string;
109
+ searchObject: {
110
+ nric?: string | null;
111
+ contact?: string | null;
112
+ };
113
+ notCheckedOut?: boolean;
114
+ };
115
+
105
116
  const tab = useState("visitorsTab", () => "visitor");
106
117
 
107
118
  async function getVisitors({
@@ -187,6 +198,28 @@ export default function () {
187
198
  });
188
199
  }
189
200
 
201
+ async function searchVisitorObject({
202
+ page = 1,
203
+ limit = 1000,
204
+ site,
205
+ searchObject,
206
+ notCheckedOut = true,
207
+ }: SearchVisitorObjectParams) {
208
+ return await useNuxtApp().$api<Record<string, any>>(
209
+ "/api/visitors/search-object",
210
+ {
211
+ method: "POST",
212
+ body: {
213
+ page,
214
+ limit,
215
+ site,
216
+ searchObject,
217
+ notCheckedOut,
218
+ },
219
+ }
220
+ );
221
+ }
222
+
190
223
  async function deleteVisitor(_id: string) {
191
224
  return await useNuxtApp().$api<Record<string, any>>(
192
225
  `/api/visitor-transactions/id/${_id}`,
@@ -227,6 +260,7 @@ export default function () {
227
260
  createVisitor,
228
261
  getVisitors,
229
262
  checkSearchUncheckedOutUnregisteredVisitor,
263
+ searchVisitorObject,
230
264
  updateVisitor,
231
265
  deleteVisitor,
232
266
  validateVisitorQrCode,
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.28",
5
+ "version": "3.0.29",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {