@7365admin1/layer-common 1.10.8 → 1.10.9

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,289 @@
1
+ <template>
2
+ <div>
3
+ <v-file-input
4
+ v-model="files"
5
+ :label="label"
6
+ accept="image/*"
7
+ :prepend-icon="prependIcon"
8
+ :loading="isFileUploading"
9
+ hide-details
10
+ chips
11
+ multiple
12
+ clearable
13
+ @update:modelValue="handleFileSelect"
14
+ @click:clear="handleClear"
15
+ :hide-input="hasHideInput"
16
+ >
17
+ <template v-slot:append v-if="hasLabel">
18
+ <slot name="append">
19
+ <v-btn color="primary" height="50px" @click="openCameraDialog">
20
+ <v-icon>mdi-camera</v-icon>
21
+ </v-btn>
22
+ </slot>
23
+ </template>
24
+ </v-file-input>
25
+
26
+ <!-- Camera Dialog -->
27
+ <v-dialog
28
+ v-model="showCameraDialog"
29
+ transition="dialog-bottom-transition"
30
+ width="800"
31
+ max-width="800"
32
+ persistent
33
+ @after-enter="startCamera"
34
+ >
35
+ <v-container
36
+ class="d-flex justify-center"
37
+ max-height="90vh"
38
+ width="800"
39
+ max-width="800"
40
+ >
41
+ <v-card elevation="2" class="d-flex flex-column align-center pa-2">
42
+ <v-toolbar>
43
+ <v-card-title class="text-h5">Take a Picture</v-card-title>
44
+ <v-spacer></v-spacer>
45
+ <v-btn
46
+ color="grey-darken-1"
47
+ icon="mdi-close"
48
+ @click="closeCameraDialog"
49
+ ></v-btn>
50
+ </v-toolbar>
51
+
52
+ <div
53
+ id="reader"
54
+ class="d-flex justify-center align-center"
55
+ style="
56
+ position: relative;
57
+ width: 500px;
58
+ min-width: 400px;
59
+ height: 400px;
60
+ "
61
+ >
62
+ <video
63
+ ref="video"
64
+ style="flex: 1; height: 400px; min-width: 300px"
65
+ class="video-shutter"
66
+ autoplay
67
+ ></video>
68
+ <canvas
69
+ ref="canvas"
70
+ style="flex: 1; height: 400px; min-width: 300px; display: none"
71
+ ></canvas>
72
+ </div>
73
+
74
+ <v-row align="center" justify="center">
75
+ <v-col cols="6">
76
+ <v-btn color="primary" icon class="mt-4" @click="switchCamera">
77
+ <v-icon>mdi-camera-switch</v-icon>
78
+ </v-btn>
79
+ </v-col>
80
+ <v-col cols="6">
81
+ <v-btn
82
+ color="secondary"
83
+ icon
84
+ class="mt-4"
85
+ @click="captureImageFromCamera"
86
+ >
87
+ <v-icon large>mdi-camera-outline</v-icon>
88
+ </v-btn>
89
+ </v-col>
90
+ </v-row>
91
+ </v-card>
92
+ </v-container>
93
+ </v-dialog>
94
+ </div>
95
+
96
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
97
+ </template>
98
+
99
+ <script setup lang="ts">
100
+ interface FileWithPreview {
101
+ name: string;
102
+ data: File;
103
+ progress: number;
104
+ url: string;
105
+ }
106
+
107
+ const props = defineProps({
108
+ label: {
109
+ type: String,
110
+ default: "Select File",
111
+ },
112
+ prependIcon: {
113
+ type: String,
114
+ default: "mdi-paperclip",
115
+ },
116
+ required: {
117
+ type: Boolean,
118
+ default: true,
119
+ },
120
+ initFiles: {
121
+ type: Array,
122
+ },
123
+ hasLabel: {
124
+ type: Boolean,
125
+ default: true,
126
+ },
127
+ hasHideInput: {
128
+ type: Boolean,
129
+ default: false,
130
+ },
131
+ });
132
+
133
+ const emit = defineEmits<{
134
+ (event: "onFileAttach", payload: Array<{ data: File }>): void;
135
+ (event: "update:files", files: FileWithPreview[]): void;
136
+ (event: "onFileRemoved", payload: { index: number; file: File }): void;
137
+ (event: "onClear"): void;
138
+ }>();
139
+
140
+ const { showUploadedFiles } = useUploadFiles();
141
+ const isFileUploading = ref<boolean>(false);
142
+
143
+ const files = ref<File[]>([]);
144
+ const showCameraDialog = ref(false);
145
+ const video = ref<HTMLVideoElement | null>(null);
146
+ const canvas = ref<HTMLCanvasElement | null>(null);
147
+ const cameraFacingMode = ref<"environment" | "user">("environment");
148
+
149
+ const message = ref("");
150
+ const messageColor = ref("");
151
+ const messageSnackbar = ref(false);
152
+
153
+ function showMessage(msg: string, color: string) {
154
+ message.value = msg;
155
+ messageColor.value = color;
156
+ messageSnackbar.value = true;
157
+ }
158
+
159
+ watchEffect(() => {
160
+ files.value = [...(props.initFiles as typeof files.value)];
161
+ });
162
+
163
+ const handleFileSelect = async () => {
164
+ if (files.value && files.value.length > 0) {
165
+ const newFiles = files.value.map((file: File) => ({
166
+ name: file.name,
167
+ data: file,
168
+ progress: 0,
169
+ url: URL.createObjectURL(file),
170
+ }));
171
+
172
+ // attachedFiles.value = [...newFiles];
173
+ showUploadedFiles(newFiles);
174
+
175
+ emit("update:files", newFiles);
176
+ } else {
177
+ files.value = [...(props.initFiles as typeof files.value)];
178
+ }
179
+ };
180
+
181
+ const handleClear = () => {
182
+ files.value = [];
183
+ emit("onClear");
184
+ };
185
+
186
+ const openCameraDialog = () => {
187
+ showCameraDialog.value = true;
188
+ };
189
+
190
+ const closeCameraDialog = () => {
191
+ showCameraDialog.value = false;
192
+ stopCamera();
193
+ };
194
+
195
+ const startCamera = async () => {
196
+ try {
197
+ const constraints = {
198
+ video: {
199
+ facingMode: cameraFacingMode.value,
200
+ },
201
+ };
202
+
203
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
204
+ if (video.value) {
205
+ video.value.srcObject = stream;
206
+ video.value.play();
207
+ }
208
+ } catch (error: any) {
209
+ showMessage(error, "error");
210
+ closeCameraDialog();
211
+ }
212
+ };
213
+
214
+ const stopCamera = () => {
215
+ if (video.value) {
216
+ const stream = video.value.srcObject as MediaStream;
217
+ if (stream) {
218
+ stream.getTracks().forEach((track) => track.stop());
219
+ }
220
+ }
221
+ };
222
+
223
+ const switchCamera = async () => {
224
+ await stopCamera();
225
+ cameraFacingMode.value =
226
+ cameraFacingMode.value === "environment" ? "user" : "environment";
227
+ showMessage(
228
+ `Switched to ${
229
+ cameraFacingMode.value === "environment" ? "Back Camera" : "Front Camera"
230
+ }`,
231
+ "error"
232
+ );
233
+ startCamera();
234
+ };
235
+
236
+ const captureImageFromCamera = () => {
237
+ if (!video.value || !canvas.value) return;
238
+
239
+ const context = canvas.value.getContext("2d");
240
+ if (!context) return;
241
+
242
+ // Set canvas dimensions to match video
243
+ canvas.value.width = video.value.videoWidth;
244
+ canvas.value.height = video.value.videoHeight;
245
+
246
+ // Capture the frame
247
+ context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
248
+
249
+ // Convert to file
250
+ canvas.value.toBlob((blob) => {
251
+ if (!blob) return;
252
+
253
+ const file = new File([blob], `camera-capture-${Date.now()}.png`, {
254
+ type: "image/png",
255
+ });
256
+
257
+ files.value = [file];
258
+ handleFileSelect();
259
+ closeCameraDialog();
260
+ }, "image/png");
261
+ };
262
+
263
+ const removeFile = (index) => {
264
+ const removedFile = files.value[index];
265
+ files.value = files.value.filter((_, i) => i !== index);
266
+ emit("onFileRemoved", { index, file: removedFile }); // Emit when a file is removed
267
+ // emit("onFilesUpdated", files.value); // Emit updated files list
268
+ };
269
+
270
+ // Cleanup
271
+ onUnmounted(() => {
272
+ stopCamera();
273
+ });
274
+ </script>
275
+
276
+ <style scoped>
277
+ .custom-chip {
278
+ max-width: 100%;
279
+ height: auto !important;
280
+ white-space: normal;
281
+ padding: 3px 20px;
282
+ }
283
+
284
+ .chip-text {
285
+ word-break: break-word;
286
+ white-space: normal;
287
+ line-height: 1.2;
288
+ }
289
+ </style>
@@ -110,6 +110,10 @@
110
110
  </template>
111
111
 
112
112
  <script setup lang="ts">
113
+ import useEquipment from "../composables/useEquipment";
114
+ import useStock from "../composables/useStock";
115
+ import useUtils from "../composables/useUtils";
116
+
113
117
  const props = defineProps({
114
118
  orgId: { type: String, default: "" },
115
119
  site: { type: String, default: "" },
@@ -126,18 +130,18 @@ const supply = ref({
126
130
  remarks: "",
127
131
  });
128
132
 
129
- const { getSupplyById } = useSupply();
133
+ const { getEquipmentById } = useEquipment();
130
134
 
131
135
  const supplyId = useRoute().params.id as string;
132
136
 
133
- const { data: getSupplyByIdReq } = await useLazyAsyncData(
134
- `get-supply-by-id-${supplyId}`,
135
- () => getSupplyById(supplyId),
137
+ const { data: getEquipmentByIdReq } = await useLazyAsyncData(
138
+ `get-equipment-by-id-${supplyId}`,
139
+ () => getEquipmentById(supplyId)
136
140
  );
137
141
 
138
142
  watchEffect(() => {
139
- if (getSupplyByIdReq?.value) {
140
- supply.value = getSupplyByIdReq.value as any;
143
+ if (getEquipmentByIdReq?.value) {
144
+ supply.value = getEquipmentByIdReq.value as any;
141
145
  }
142
146
  });
143
147
 
@@ -163,7 +167,7 @@ const {
163
167
  () => getStockBySupply(props.site, supplyId),
164
168
  {
165
169
  watch: [page, () => props.site, () => supplyId],
166
- },
170
+ }
167
171
  );
168
172
 
169
173
  watchEffect(() => {
@@ -144,7 +144,8 @@
144
144
 
145
145
  <v-col v-if="shouldShowField('unit')" cols="12">
146
146
  <InputLabel class="text-capitalize" title="Registered Unit Company Name" required />
147
- <v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable" :loading="buildingUnitDataPending" readonly class="no-pointer" />
147
+ <v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable"
148
+ :loading="buildingUnitDataPending" readonly class="no-pointer" />
148
149
  </v-col>
149
150
 
150
151
  <v-col v-if="shouldShowField('remarks')" cols="12">
@@ -154,10 +155,16 @@
154
155
 
155
156
  <v-col v-if="prop.type === 'contractor' && contractorStep === 2" cols="12">
156
157
  <PassInformation />
157
- <EntryPassInformation v-if="entryPassSettings?.data?.settings?.nfcPass" v-model="passType"
158
- v-model:quantity="passQuantity" v-model:cards="passCards" :settings="entryPassSettings"
159
- :loading="entryPassSettingsPending" @scan:barcode="$emit('scan:barcode')"
160
- @scan:camera="$emit('scan:camera')" />
158
+ <EntryPassInformation
159
+ v-if="entryPassSettings?.data?.settings?.nfcPass"
160
+ v-model="passType"
161
+ v-model:quantity="passQuantity"
162
+ v-model:cards="passCards"
163
+ :settings="entryPassSettings"
164
+ :loading="entryPassSettingsPending"
165
+ :site-id="prop.site"
166
+ :unit-id="visitor.unit || null"
167
+ />
161
168
  </v-col>
162
169
 
163
170
  <v-col v-if="prop.type === 'contractor' && contractorStep === 3" cols="12">
@@ -190,8 +197,8 @@
190
197
  prop.type === 'contractor' &&
191
198
  contractorStep > 1
192
199
  " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
193
- <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none" size="48"
194
- @click="backToSelection" text="Back to Selection" />
200
+ <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
201
+ size="48" @click="backToSelection" text="Back to Selection" />
195
202
  <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
196
203
  </v-col>
197
204
  <v-col cols="6">
@@ -209,6 +216,44 @@
209
216
  :vehicle-number-users-list="vehicleNumberUserItems" @close="dialog.vehicleNumberUsersList = false"
210
217
  @update:people="handleAutofillDataViaVehicleNumber" />
211
218
  </v-dialog>
219
+
220
+ <v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="700" persistent>
221
+ <v-card :loading="loading.checkingOut">
222
+ <v-toolbar>
223
+ <v-toolbar-title>
224
+ <v-row no-gutters class="d-flex align-center justify-space-between">
225
+ <span class="font-weight-bold">
226
+ You have an unchecked-out vehicle for: {{ visitor.plateNumber }}
227
+ </span>
228
+ </v-row>
229
+ </v-toolbar-title>
230
+ </v-toolbar>
231
+
232
+ <v-card-text>
233
+
234
+ <v-list lines="three">
235
+ <v-list-item v-if="matchingPlateNumberNonCheckedOutArr.length > 0"
236
+ v-for="v in matchingPlateNumberNonCheckedOutArr" :key="v._id" class="cursor-pointer">
237
+ <v-list-item-title>
238
+ {{ v.plateNumber }} - {{ v.name }}
239
+ </v-list-item-title>
240
+
241
+ <v-list-item-subtitle>
242
+ Checked In at : {{ UTCToLocalTIme(v.checkIn) }}
243
+ </v-list-item-subtitle>
244
+
245
+ <template #append>
246
+ <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
247
+ :loading="loading.checkingOut && v?._id === prop.visitorData?._id"
248
+ @click.stop="handleCheckout(v._id)" />
249
+ </template>
250
+
251
+ </v-list-item>
252
+ </v-list>
253
+
254
+ </v-card-text>
255
+ </v-card>
256
+ </v-dialog>
212
257
  </v-card>
213
258
  </template>
214
259
 
@@ -243,12 +288,12 @@ type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
243
288
  const currentAutofillSource = ref<AutofillSource>(null);
244
289
 
245
290
 
246
- const { requiredRule, debounce } = useUtils();
291
+ const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
247
292
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
248
- const { createVisitor, typeFieldMap, contractorTypes } = useVisitor();
293
+ const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
249
294
  const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
250
295
  const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
251
- const { getById: getUnitDataById} = useBuildingUnit()
296
+ const { getById: getUnitDataById } = useBuildingUnit()
252
297
 
253
298
  const emit = defineEmits([
254
299
  "back",
@@ -278,15 +323,20 @@ const visitor = reactive<Partial<TVisitorPayload>>({
278
323
  });
279
324
 
280
325
  const passType = ref("");
281
- const passQuantity = ref<number | null>(null);
326
+ const passQuantity = ref<number | null>(1);
282
327
  const passCards = ref<string[]>([]);
283
328
 
284
329
  const registeredUnitCompanyName = ref('N/A')
285
330
 
286
331
  const dialog = reactive({
287
332
  vehicleNumberUsersList: false,
333
+ showNonCheckedOutDialog: false,
288
334
  });
289
335
 
336
+ const loading = reactive({
337
+ checkingOut: false,
338
+ })
339
+
290
340
  const validForm = ref(false);
291
341
  const formRef = ref<HTMLFormElement | null>(null);
292
342
  const processing = ref(false);
@@ -306,6 +356,8 @@ const blocksArray = ref<TDefaultOptionObj[]>([]);
306
356
  const levelsArray = ref<TDefaultOptionObj[]>([]);
307
357
  const unitsArray = ref<TDefaultOptionObj[]>([]);
308
358
 
359
+ const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([])
360
+
309
361
 
310
362
  const vehicleNumberUserItems = ref<TPeople[]>([])
311
363
 
@@ -466,25 +518,70 @@ watch(fetchCompanyListReq, (arr) => {
466
518
  }
467
519
  })
468
520
  const {
469
- data: fetchVehicleNumberUserReq,
470
- refresh: fetchVehicleNumberUserRefresh,
471
- pending: fetchVehicleNumberUserPending,
521
+ data: fetchVisitorListByVehicleNumberReq,
522
+ refresh: fetchVisitorListByVehicleNumberRefresh,
523
+ pending: fetchVisitorListByVehicleNumberPending,
472
524
 
473
- } = useLazyAsyncData(`fetch-vehicle-number-user-list`, () => {
525
+ } = useLazyAsyncData(`fetch-visitor-list-by-vehicle-number`, () => {
474
526
  if (!visitor.plateNumber) return Promise.resolve(null)
475
- return findUsersByPlateNumber(visitor.plateNumber)
527
+ return getVisitors({ page: 1, limit: 20, site: prop.site, plateNumber: visitor.plateNumber, checkedOut: false, status: "registered" })
476
528
  })
477
529
 
478
- watch(fetchVehicleNumberUserReq, (arr) => {
479
- const arrayData = arr?.data
480
- if (Array.isArray(arrayData)) {
481
- vehicleNumberUserItems.value = arrayData;
482
- if (arrayData.length > 0) {
483
- dialog.vehicleNumberUsersList = true
484
- }
530
+ watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
531
+ const itemsArray = arr?.items || []
532
+ const isValidArray = Array.isArray(itemsArray)
533
+ matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : []
534
+ if(matchingPlateNumberNonCheckedOutArr.value.length > 0) {
535
+ dialog.showNonCheckedOutDialog = true;
536
+ } else {
537
+ dialog.showNonCheckedOutDialog = false;
485
538
  }
486
539
  })
487
540
 
541
+
542
+ const debounceSearchVisitorsByPlateNumbers = debounce(() => {
543
+ if (!visitor.plateNumber) {
544
+ matchingPlateNumberNonCheckedOutArr.value = []
545
+ dialog.showNonCheckedOutDialog = false;
546
+ return;
547
+ }
548
+ fetchVisitorListByVehicleNumberRefresh();
549
+ }, 300);
550
+
551
+ watch(() => visitor.plateNumber, (newVal) => {
552
+ debounceSearchVisitorsByPlateNumbers();
553
+ });
554
+
555
+
556
+
557
+ async function handleCheckout(visitorId: string) {
558
+ if (!visitorId) {
559
+ errorMessage.value = "Invalid visitor ID. Cannot proceed with checkout.";
560
+ return;
561
+ }
562
+
563
+ try {
564
+ loading.checkingOut = true;
565
+ const res = await updateVisitor(visitorId as string, {
566
+ checkOut: new Date().toISOString(),
567
+ });
568
+ if (res) {
569
+ await fetchVisitorListByVehicleNumberRefresh();
570
+
571
+ }
572
+ } catch (error: any) {
573
+ const errorMessage = error?.response?._data?.message;
574
+ console.log("[ERROR]", error);
575
+ errorMessage.value =
576
+ errorMessage || "An error occurred while checking out the vehicle.";
577
+ } finally {
578
+ loading.checkingOut = false;
579
+ }
580
+ }
581
+
582
+
583
+
584
+
488
585
  const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
489
586
 
490
587
  watch(companyNameInput, async (val) => {
@@ -496,19 +593,7 @@ watch(companyNameInput, async (val) => {
496
593
  })
497
594
 
498
595
 
499
- const debounceSearchVehicleUsers = debounce(() => {
500
- if (!visitor.plateNumber) {
501
- vehicleNumberUserItems.value = [];
502
- dialog.vehicleNumberUsersList = false;
503
- return;
504
- }
505
- fetchVehicleNumberUserRefresh();
506
- }, 300);
507
596
 
508
- watch(() => visitor.plateNumber, (newVal) => {
509
- if (currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber") return;
510
- debounceSearchVehicleUsers();
511
- });
512
597
 
513
598
 
514
599
  function handleAutofillDataViaVehicleNumber(item: TPeople) {
@@ -766,12 +851,12 @@ async function submit() {
766
851
 
767
852
 
768
853
  if (prop.type === "contractor") {
769
- // contractor type logic payload
770
- payload = {
771
- ...payload,
772
- members: visitor.members,
773
- };
774
- }
854
+ // contractor type logic payload
855
+ payload = {
856
+ ...payload,
857
+ members: visitor.members,
858
+ };
859
+ }
775
860
  try {
776
861
  const res = await createVisitor(payload);
777
862
  if (res) {
@@ -801,9 +886,9 @@ onMounted(() => {
801
886
  contractorStep.value = 1;
802
887
  currentAutofillSource.value = null;
803
888
 
804
- if(prop.mode === 'register' && prop.visitorData) {
805
- console.log('Register mode, prefill visitor data', prop.visitorData?.plateNumber )
806
- visitor.plateNumber = prop.visitorData?.plateNumber || ""
889
+ if (prop.mode === 'register' && prop.visitorData) {
890
+ console.log('Register mode, prefill visitor data', prop.visitorData?.plateNumber)
891
+ visitor.plateNumber = prop.visitorData?.plateNumber || ""
807
892
  }
808
893
  });
809
894
  </script>
@@ -55,10 +55,7 @@ export default function useAccessManagement() {
55
55
  );
56
56
  }
57
57
 
58
- function getAllAccessCardsCounts(params: {
59
- site: string;
60
- userType: string;
61
- }) {
58
+ function getAllAccessCardsCounts(params: { site: string; userType: string }) {
62
59
  return useNuxtApp().$api<Record<string, any>>(
63
60
  `/api/access-management/all-access-cards-counts`,
64
61
  {
@@ -71,14 +68,16 @@ export default function useAccessManagement() {
71
68
  );
72
69
  }
73
70
 
74
- function getUserTypeAccessCards(params: {
75
- page?: number;
76
- limit?: number;
77
- search?: string;
78
- organization?: string;
79
- userType?: string;
80
- site?: string;
81
- } = {}) {
71
+ function getUserTypeAccessCards(
72
+ params: {
73
+ page?: number;
74
+ limit?: number;
75
+ search?: string;
76
+ organization?: string;
77
+ userType?: string;
78
+ site?: string;
79
+ } = {}
80
+ ) {
82
81
  return useNuxtApp().$api<Record<string, any>>(
83
82
  `/api/access-management/user-type-access-cards`,
84
83
  {
@@ -200,12 +199,14 @@ export default function useAccessManagement() {
200
199
  );
201
200
  }
202
201
 
203
- function getVisitorAccessCards(params: {
204
- page?: number;
205
- limit?: number;
206
- site?: string;
207
- search?: string;
208
- } = {}) {
202
+ function getVisitorAccessCards(
203
+ params: {
204
+ page?: number;
205
+ limit?: number;
206
+ site?: string;
207
+ search?: string;
208
+ } = {}
209
+ ) {
209
210
  return useNuxtApp().$api<Record<string, any>>(
210
211
  `/api/access-management/visitor-access-cards`,
211
212
  {
@@ -221,6 +222,27 @@ export default function useAccessManagement() {
221
222
  );
222
223
  }
223
224
 
225
+ function getAvailableContractorCards(params: {
226
+ type: "NFC" | "QRCODE";
227
+ siteId: string;
228
+ unitId: string;
229
+ page?: number;
230
+ limit?: number;
231
+ }) {
232
+ return useNuxtApp().$api<Record<string, any>>(
233
+ `/api/access-management/available-card-contractor/${params.type}`,
234
+ {
235
+ method: "GET",
236
+ query: {
237
+ siteId: params.siteId,
238
+ unitId: params.unitId,
239
+ page: params.page ?? 1,
240
+ limit: params.limit ?? 100,
241
+ },
242
+ }
243
+ );
244
+ }
245
+
224
246
  return {
225
247
  getDoorAccessLevels,
226
248
  getLiftAccessLevels,
@@ -238,5 +260,6 @@ export default function useAccessManagement() {
238
260
  getVisitorAccessCards,
239
261
  saveVisitorAccessCardQrTag,
240
262
  getAllVisitorAccessCardsQrTags,
263
+ getAvailableContractorCards,
241
264
  };
242
265
  }