@7365admin1/layer-common 1.10.5 → 1.10.7

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardDetailsDialog.vue +144 -0
  3. package/components/AccessCardPreviewDialog.vue +7 -2
  4. package/components/AccessCardQrTagging.vue +314 -34
  5. package/components/AccessCardQrTaggingPrintQr.vue +75 -0
  6. package/components/AccessManagement.vue +5 -1
  7. package/components/AreaChecklistHistoryLogs.vue +9 -0
  8. package/components/BuildingForm.vue +36 -5
  9. package/components/BuildingManagement/buildings.vue +18 -9
  10. package/components/BuildingManagement/units.vue +12 -114
  11. package/components/BuildingUnitFormAdd.vue +39 -30
  12. package/components/BuildingUnitFormEdit.vue +265 -116
  13. package/components/CleaningScheduleMain.vue +60 -13
  14. package/components/Dialog/DeleteConfirmation.vue +2 -2
  15. package/components/Dialog/UpdateMoreAction.vue +2 -2
  16. package/components/EntryPassInformation.vue +215 -0
  17. package/components/Input/InputPhoneNumberV2.vue +11 -0
  18. package/components/ManageChecklistMain.vue +29 -3
  19. package/components/PlateNumberDisplay.vue +9 -1
  20. package/components/ScheduleAreaMain.vue +56 -0
  21. package/components/TableHygiene.vue +265 -113
  22. package/components/UnitPersonCard.vue +63 -0
  23. package/components/VehicleAddSelection.vue +2 -2
  24. package/components/VehicleForm.vue +169 -47
  25. package/components/VehicleManagement.vue +169 -43
  26. package/components/VisitorForm.vue +19 -0
  27. package/components/VisitorManagement.vue +1 -1
  28. package/components/WorkOrder/Main.vue +2 -1
  29. package/composables/useAccessManagement.ts +63 -0
  30. package/composables/useFeedback.ts +2 -2
  31. package/composables/usePeople.ts +14 -3
  32. package/composables/useVehicle.ts +15 -1
  33. package/composables/useWorkOrder.ts +2 -2
  34. package/package.json +3 -2
  35. package/types/cleaner-schedule.d.ts +1 -0
  36. package/types/html2pdf.d.ts +19 -0
  37. package/types/people.d.ts +4 -0
  38. package/types/site.d.ts +8 -0
  39. package/types/vehicle.d.ts +3 -4
  40. package/.playground/app.vue +0 -41
  41. package/.playground/eslint.config.mjs +0 -6
  42. package/.playground/nuxt.config.ts +0 -22
  43. package/.playground/pages/feedback.vue +0 -30
  44. package/components/AccessCardHistoryDialog.vue +0 -133
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.7
4
+
5
+ ### Patch Changes
6
+
7
+ - a17cf02: Update Changes for march 11, 2026
8
+
9
+ ## 1.10.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 49fefa8: Update layer-common package for March 9,2026
14
+
3
15
  ## 1.10.5
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <v-dialog :model-value="modelValue" width="480" persistent>
3
+ <v-card width="100%">
4
+ <v-toolbar density="compact" color="black">
5
+ <v-toolbar-title class="text-subtitle-1 font-weight-medium">
6
+ Card Details — {{ card?.cardNo ?? "N/A" }}
7
+ </v-toolbar-title>
8
+ <v-btn icon @click="emit('update:modelValue', false)">
9
+ <v-icon>mdi-close</v-icon>
10
+ </v-btn>
11
+ </v-toolbar>
12
+
13
+ <v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
14
+ <div v-if="pending" class="d-flex justify-center align-center py-8">
15
+ <v-progress-circular indeterminate color="black" />
16
+ </div>
17
+
18
+ <div v-else-if="!details" class="text-center text-grey py-8">
19
+ No details available.
20
+ </div>
21
+
22
+ <div v-else class="d-flex flex-column ga-3">
23
+ <div>
24
+ <div class="text-caption text-grey">Card No</div>
25
+ <div class="text-body-2 font-weight-medium">{{ details.cardNo ?? "N/A" }}</div>
26
+ </div>
27
+
28
+ <div>
29
+ <div class="text-caption text-grey">Type</div>
30
+ <div class="text-body-2 font-weight-medium">{{ details.type ?? "N/A" }}</div>
31
+ </div>
32
+
33
+ <div>
34
+ <div class="text-caption text-grey">Status</div>
35
+ <v-chip
36
+ :color="statusColor(details.status)"
37
+ variant="flat"
38
+ size="x-small"
39
+ class="text-capitalize font-weight-medium mt-1"
40
+ >
41
+ {{ details.status ?? "N/A" }}
42
+ </v-chip>
43
+ </div>
44
+
45
+ <div>
46
+ <div class="text-caption text-grey">Activated</div>
47
+ <div class="text-body-2 font-weight-medium">{{ details.isActivated ? "Yes" : "No" }}</div>
48
+ </div>
49
+
50
+ <div>
51
+ <div class="text-caption text-grey">Site</div>
52
+ <div class="text-body-2 font-weight-medium">{{ details.site?.name ?? "N/A" }}</div>
53
+ </div>
54
+
55
+ <div>
56
+ <div class="text-caption text-grey">User</div>
57
+ <div class="text-body-2 font-weight-medium">{{ details.user?.name ?? "N/A" }} ({{ details.user?.email ?? "N/A" }})</div>
58
+ </div>
59
+
60
+ <div>
61
+ <div class="text-caption text-grey">Created At</div>
62
+ <div class="text-body-2 font-weight-medium">{{ formatDate(details.createdAt) }}</div>
63
+ </div>
64
+
65
+ <div>
66
+ <div class="text-caption text-grey">Updated At</div>
67
+ <div class="text-body-2 font-weight-medium">{{ formatDate(details.updatedAt) }}</div>
68
+ </div>
69
+
70
+ <div v-if="details.remarks">
71
+ <div class="text-caption text-grey">Remarks</div>
72
+ <div class="text-body-2 font-weight-medium">{{ details.remarks }}</div>
73
+ </div>
74
+ </div>
75
+ </v-card-text>
76
+ </v-card>
77
+ </v-dialog>
78
+ </template>
79
+
80
+ <script setup lang="ts">
81
+ const props = defineProps({
82
+ modelValue: {
83
+ type: Boolean,
84
+ default: false,
85
+ },
86
+ card: {
87
+ type: Object as PropType<Record<string, any> | null>,
88
+ default: null,
89
+ },
90
+ siteId: {
91
+ type: String,
92
+ default: "",
93
+ },
94
+ });
95
+
96
+ const emit = defineEmits<{
97
+ "update:modelValue": [value: boolean];
98
+ }>();
99
+
100
+ const { getCardDetails } = useAccessManagement();
101
+
102
+ const details = ref<Record<string, any> | null>(null);
103
+ const pending = ref(false);
104
+
105
+ watch(
106
+ () => props.modelValue,
107
+ async (val) => {
108
+ if (val && props.card?._id && props.siteId) {
109
+ pending.value = true;
110
+ try {
111
+ const res = await getCardDetails({ siteId: props.siteId, cardId: props.card._id });
112
+ details.value = res?.data ?? null;
113
+ } finally {
114
+ pending.value = false;
115
+ }
116
+ } else {
117
+ details.value = null;
118
+ }
119
+ }
120
+ );
121
+
122
+ function statusColor(status: string) {
123
+ const map: Record<string, string> = {
124
+ active: "success",
125
+ assigned: "primary",
126
+ replaced: "orange",
127
+ deleted: "error",
128
+ available: "grey",
129
+ };
130
+ return map[status] ?? "grey";
131
+ }
132
+
133
+ function formatDate(date: string) {
134
+ if (!date) return "N/A";
135
+ return new Intl.DateTimeFormat("en-GB", {
136
+ day: "2-digit",
137
+ month: "short",
138
+ year: "numeric",
139
+ hour: "2-digit",
140
+ minute: "2-digit",
141
+ hour12: true,
142
+ }).format(new Date(date));
143
+ }
144
+ </script>
@@ -1,7 +1,8 @@
1
1
  <template>
2
- <AccessCardHistoryDialog
2
+ <AccessCardDetailsDialog
3
3
  v-model="historyDialog"
4
4
  :card="selectedCardInUnit"
5
+ :site-id="siteId"
5
6
  />
6
7
 
7
8
  <v-dialog :model-value="modelValue" width="450" persistent>
@@ -221,7 +222,7 @@
221
222
  </v-list-item>
222
223
  <v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
223
224
  <v-list-item-title class="text-subtitle-2 cursor-pointer">
224
- Card History
225
+ Card Details
225
226
  </v-list-item-title>
226
227
  </v-list-item>
227
228
  <v-list-item
@@ -277,6 +278,10 @@ const props = defineProps({
277
278
  type: Boolean,
278
279
  default: false,
279
280
  },
281
+ siteId: {
282
+ type: String,
283
+ default: "",
284
+ },
280
285
  });
281
286
 
282
287
  const emit = defineEmits<{
@@ -9,6 +9,7 @@
9
9
  variant="tonal"
10
10
  size="large"
11
11
  :disabled="!items.length"
12
+ @click="handlePrintAll"
12
13
  >
13
14
  Print All
14
15
  </v-btn>
@@ -17,7 +18,8 @@
17
18
  rounded="pill"
18
19
  variant="tonal"
19
20
  size="large"
20
- :disabled="!selected.length"
21
+ :disabled="!qrCodeAccessCards.length"
22
+ @click="generateQrCodes"
21
23
  >
22
24
  Generate QR Code
23
25
  </v-btn>
@@ -26,23 +28,14 @@
26
28
  rounded="pill"
27
29
  variant="tonal"
28
30
  size="large"
29
- :disabled="!selected.length"
31
+ :disabled="!qrCodeAccessCardsToPrint.length"
32
+ @click="handlePrintQrCode"
30
33
  >
31
34
  Print QR Code
32
35
  </v-btn>
33
36
  </v-row>
34
37
 
35
38
  <v-row no-gutters class="ga-2" justify="end" style="max-width: 420px">
36
- <v-select
37
- v-model="typeFilter"
38
- :items="typeOptions"
39
- placeholder="Type"
40
- variant="outlined"
41
- density="comfortable"
42
- hide-details
43
- clearable
44
- style="max-width: 160px"
45
- />
46
39
  <v-text-field
47
40
  v-model="searchText"
48
41
  placeholder="Search Card..."
@@ -86,55 +79,133 @@
86
79
  </v-toolbar>
87
80
 
88
81
  <v-data-table
89
- v-model="selected"
90
82
  :headers="tableHeaders"
91
83
  :items="items"
92
84
  item-value="_id"
93
85
  items-per-page="10"
94
86
  fixed-header
95
87
  hide-default-footer
96
- show-select
97
88
  style="max-height: calc(100vh - 200px)"
98
89
  >
99
- <template #item.card="{ item }">
90
+ <template #header.selectGenerate>
91
+ <v-checkbox
92
+ :model-value="isAllGenerateSelected"
93
+ @update:model-value="toggleSelectAllGenerate"
94
+ hide-details
95
+ density="compact"
96
+ :disabled="!hasGeneratableItems"
97
+ />
98
+ </template>
99
+
100
+ <template #header.selectPrint>
101
+ <v-checkbox
102
+ :model-value="isAllPrintSelected"
103
+ @update:model-value="toggleSelectAllPrint"
104
+ hide-details
105
+ density="compact"
106
+ :disabled="!hasPrintableItems"
107
+ style="margin-left: -12px"
108
+ />
109
+ </template>
110
+
111
+ <template #item.selectGenerate="{ item }">
112
+ <v-checkbox
113
+ v-if="!item.qrTag"
114
+ :model-value="qrCodeAccessCards.includes(item._id)"
115
+ @update:model-value="toggleGenerateSelection(item._id)"
116
+ hide-details
117
+ density="compact"
118
+ />
119
+ </template>
120
+
121
+ <template #item.cardNo="{ item }">
100
122
  <v-row no-gutters align="center" class="ga-2">
101
123
  <v-icon size="18" color="grey-darken-1">mdi-card-account-details</v-icon>
102
- <span>{{ item.hid ?? "N/A" }}</span>
124
+ <span>{{ item.cardNo ?? "N/A" }}</span>
103
125
  </v-row>
104
126
  </template>
105
127
 
106
- <template #item.qrCode="{ item }">
128
+ <template #item.qrTagCardNo="{ item }">
129
+ <span>{{ item.qrTagCardNo != null ? `C${item.qrTagCardNo}` : "N/A" }}</span>
130
+ </template>
131
+
132
+ <template #item.qrTag="{ item }">
107
133
  <v-icon
108
- :color="item.qrCode ? 'success' : 'grey-lighten-1'"
134
+ :color="item.qrTag ? 'success' : 'grey-lighten-1'"
109
135
  size="22"
110
136
  >
111
- {{ item.qrCode ? "mdi-check-circle" : "mdi-circle-outline" }}
137
+ {{ item.qrTag ? "mdi-check-circle" : "mdi-circle-outline" }}
112
138
  </v-icon>
113
139
  </template>
140
+
141
+ <template #item.selectPrint="{ item }">
142
+ <v-checkbox
143
+ v-if="item.qrTag"
144
+ :model-value="qrCodeAccessCardsToPrint.includes(item._id)"
145
+ @update:model-value="togglePrintSelection(item._id)"
146
+ hide-details
147
+ density="compact"
148
+ style="margin-left: -12px"
149
+ />
150
+ </template>
114
151
  </v-data-table>
115
152
  </v-card>
116
153
  </v-col>
117
154
 
155
+ <!-- Hidden QR canvases for generation -->
156
+ <ClientOnly>
157
+ <div style="display: none">
158
+ <qrcode-vue
159
+ v-for="qrCode in qrCodesToGenerate"
160
+ :key="qrCode._id"
161
+ :value="qrCode.qrData"
162
+ :size="150"
163
+ :data-card-id="qrCode._id"
164
+ />
165
+ </div>
166
+ </ClientOnly>
167
+
168
+ <!-- Print dialog -->
169
+ <v-dialog v-model="showPrintDialog" max-width="fit-content">
170
+ <AccessCardQrTaggingPrintQr
171
+ ref="printQrRef"
172
+ :access-cards="printPayload"
173
+ @close="showPrintDialog = false"
174
+ />
175
+ </v-dialog>
176
+
177
+ <!-- Processing loader -->
178
+ <v-dialog v-model="isGenerating" persistent width="300">
179
+ <v-card>
180
+ <v-card-text class="text-center">
181
+ <v-progress-circular indeterminate color="deep-purple" size="50" />
182
+ <div class="mt-3">Processing QR Codes...</div>
183
+ </v-card-text>
184
+ </v-card>
185
+ </v-dialog>
186
+
118
187
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
119
188
  </v-row>
120
189
  </template>
121
190
 
122
191
  <script setup lang="ts">
192
+ import QrcodeVue from "qrcode.vue";
193
+
123
194
  definePageMeta({
124
195
  middleware: ["01-auth", "02-org"],
125
196
  memberOnly: true,
126
197
  });
127
198
 
128
199
  const tableHeaders = [
129
- { title: "Card", key: "card", sortable: false },
130
- { title: "Location", key: "location", sortable: false },
131
- { title: "Card No.", key: "cardNo", sortable: false },
132
- { title: "HID", key: "hid", sortable: false },
133
- { title: "QR Code", key: "qrCode", sortable: false },
200
+ { title: "", key: "selectGenerate", sortable: false, width: "50px" },
201
+ { title: "Card", key: "cardNo", sortable: false },
202
+ { title: "Location", key: "accessLevel", sortable: false },
203
+ { title: "Card No.", key: "qrTagCardNo", sortable: false },
204
+ { title: "HID", key: "extractedCardNo", sortable: false },
205
+ { title: "QR Code", key: "qrTag", sortable: false },
206
+ { title: "", key: "selectPrint", sortable: false, width: "50px" },
134
207
  ];
135
208
 
136
- const typeOptions = ["Physical", "Non-Physical"];
137
-
138
209
  const route = useRoute();
139
210
  const siteId = computed(() => route.params.site as string);
140
211
  const orgId = computed(() => route.params.org as string);
@@ -148,9 +219,24 @@ const messageSnackbar = ref(false);
148
219
  const messageColor = ref("");
149
220
 
150
221
  const items = ref<Record<string, any>[]>([]);
151
- const selected = ref<string[]>([]);
152
222
  const searchText = ref("");
153
- const typeFilter = ref<string | null>(null);
223
+
224
+ const qrCodeAccessCards = ref<string[]>([]);
225
+ const qrCodeAccessCardsToPrint = ref<string[]>([]);
226
+ const qrCodesToGenerate = ref<{ _id: string; cardNo: string; qrData: string; qrTagCardNo: string }[]>([]);
227
+
228
+ const isGenerating = ref(false);
229
+ const showPrintDialog = ref(false);
230
+ const printPayload = ref<any[]>([]);
231
+ const printQrRef = ref();
232
+
233
+ const {
234
+ getVisitorAccessCards,
235
+ saveVisitorAccessCardQrTag,
236
+ getAllVisitorAccessCardsQrTags,
237
+ } = useAccessManagement();
238
+
239
+ const { addFile, getFileUrl } = useFile();
154
240
 
155
241
  const {
156
242
  data: qrTaggingReq,
@@ -158,23 +244,217 @@ const {
158
244
  status: fetchStatus,
159
245
  } = useLazyAsyncData(
160
246
  "get-qr-tagging",
161
- () => {
162
- // TODO: wire up QR tagging API
163
- return Promise.resolve({ data: { items: [], pages: 0, pageRange: "0 - 0 of 0" } });
164
- },
165
- { watch: [page, searchText, typeFilter] }
247
+ () =>
248
+ getVisitorAccessCards({
249
+ page: page.value,
250
+ limit: 10,
251
+ site: siteId.value,
252
+ search: searchText.value || undefined,
253
+ }),
254
+ { watch: [page, searchText] }
166
255
  );
167
256
 
168
257
  const loading = computed(() => fetchStatus.value === "pending");
169
258
 
170
259
  watchEffect(() => {
171
260
  if (qrTaggingReq.value?.data) {
172
- items.value = qrTaggingReq.value.data.items;
261
+ items.value = qrTaggingReq.value.data.items.map((item: any) => ({
262
+ ...item,
263
+ qrTagCardNo: item.qrTagCardNo ?? item.extractedCardNo,
264
+ }));
173
265
  pages.value = qrTaggingReq.value.data.pages;
174
266
  pageRange.value = qrTaggingReq.value.data.pageRange;
175
267
  }
176
268
  });
177
269
 
270
+ const hasGeneratableItems = computed(() =>
271
+ items.value.some((item) => !item.qrTag)
272
+ );
273
+ const hasPrintableItems = computed(() =>
274
+ items.value.some((item) => !!item.qrTag)
275
+ );
276
+
277
+ const isAllGenerateSelected = computed(() => {
278
+ const generatable = items.value.filter((item) => !item.qrTag);
279
+ return (
280
+ generatable.length > 0 &&
281
+ generatable.every((item) => qrCodeAccessCards.value.includes(item._id))
282
+ );
283
+ });
284
+
285
+ const isAllPrintSelected = computed(() => {
286
+ const printable = items.value.filter((item) => !!item.qrTag);
287
+ return (
288
+ printable.length > 0 &&
289
+ printable.every((item) =>
290
+ qrCodeAccessCardsToPrint.value.includes(item._id)
291
+ )
292
+ );
293
+ });
294
+
295
+ function toggleSelectAllGenerate(value: boolean | null) {
296
+ if (value) {
297
+ qrCodeAccessCards.value = items.value
298
+ .filter((item) => !item.qrTag)
299
+ .map((item) => item._id);
300
+ } else {
301
+ qrCodeAccessCards.value = [];
302
+ }
303
+ }
304
+
305
+ function toggleSelectAllPrint(value: boolean | null) {
306
+ if (value) {
307
+ qrCodeAccessCardsToPrint.value = items.value
308
+ .filter((item) => !!item.qrTag)
309
+ .map((item) => item._id);
310
+ } else {
311
+ qrCodeAccessCardsToPrint.value = [];
312
+ }
313
+ }
314
+
315
+ function toggleGenerateSelection(id: string) {
316
+ if (qrCodeAccessCards.value.includes(id)) {
317
+ qrCodeAccessCards.value = qrCodeAccessCards.value.filter((i) => i !== id);
318
+ } else {
319
+ qrCodeAccessCards.value.push(id);
320
+ }
321
+ }
322
+
323
+ function togglePrintSelection(id: string) {
324
+ if (qrCodeAccessCardsToPrint.value.includes(id)) {
325
+ qrCodeAccessCardsToPrint.value = qrCodeAccessCardsToPrint.value.filter(
326
+ (i) => i !== id
327
+ );
328
+ } else {
329
+ qrCodeAccessCardsToPrint.value.push(id);
330
+ }
331
+ }
332
+
333
+ async function generateQrCodes() {
334
+ isGenerating.value = true;
335
+
336
+ const filteredCards = items.value.filter((card) =>
337
+ qrCodeAccessCards.value.includes(card._id)
338
+ );
339
+
340
+ qrCodesToGenerate.value = filteredCards.map((card) => ({
341
+ _id: card._id,
342
+ cardNo: card.cardNo,
343
+ qrData: card.qrData,
344
+ qrTagCardNo: card.qrTagCardNo ?? "",
345
+ }));
346
+
347
+ await nextTick();
348
+ await nextTick();
349
+
350
+ const payloads: Array<{ _id: string; cardNo: string; qrTag: string; qrTagCardNo: string }> = [];
351
+
352
+ await Promise.all(
353
+ qrCodesToGenerate.value.map(async (qrCode) => {
354
+ const fileUrl = await saveQrCodeToStorage(qrCode._id);
355
+ if (fileUrl) {
356
+ payloads.push({
357
+ _id: qrCode._id,
358
+ cardNo: qrCode.cardNo,
359
+ qrTag: fileUrl,
360
+ qrTagCardNo: qrCode.qrTagCardNo,
361
+ });
362
+ }
363
+ })
364
+ );
365
+
366
+ try {
367
+ if (payloads.length > 0) {
368
+ await saveVisitorAccessCardQrTag(siteId.value, payloads);
369
+ showMessage(`${payloads.length} QR code(s) generated successfully`, "success");
370
+ } else {
371
+ showMessage("No QR codes could be generated", "warning");
372
+ }
373
+ } catch {
374
+ showMessage("Failed to save QR codes", "error");
375
+ }
376
+
377
+ await fetchItems();
378
+
379
+ qrCodesToGenerate.value = [];
380
+ qrCodeAccessCards.value = [];
381
+ isGenerating.value = false;
382
+ }
383
+
384
+ async function saveQrCodeToStorage(_id: string): Promise<string | null> {
385
+ const canvas = document.querySelector<HTMLCanvasElement>(
386
+ `canvas[data-card-id="${_id}"]`
387
+ );
388
+ if (!canvas) return null;
389
+
390
+ return new Promise((resolve) => {
391
+ canvas.toBlob(async (blob) => {
392
+ if (!blob) return resolve(null);
393
+ const file = new File([blob], `${_id}.png`, { type: "image/png" });
394
+ try {
395
+ const res = await addFile(file);
396
+ resolve(res?.id ? getFileUrl(res.id) : null);
397
+ } catch {
398
+ resolve(null);
399
+ }
400
+ }, "image/png");
401
+ });
402
+ }
403
+
404
+ async function handlePrintQrCode() {
405
+ const payload = items.value
406
+ .filter((card) => qrCodeAccessCardsToPrint.value.includes(card._id))
407
+ .map((card) => ({
408
+ accessLevel: card.accessLevel,
409
+ cardNumber: card.extractedCardNo,
410
+ qrDataImage: card.qrTag,
411
+ qrTagCardNo: card.qrTagCardNo,
412
+ }));
413
+
414
+ if (!payload.length) return;
415
+
416
+ printPayload.value = payload;
417
+ showPrintDialog.value = true;
418
+
419
+ await nextTick();
420
+ await nextTick();
421
+
422
+ printQrRef.value?.downloadPDF();
423
+ qrCodeAccessCardsToPrint.value = [];
424
+ showMessage("Printing QR codes...", "success");
425
+ }
426
+
427
+ async function handlePrintAll() {
428
+ isGenerating.value = true;
429
+ try {
430
+ const response: any = await getAllVisitorAccessCardsQrTags(siteId.value);
431
+
432
+ if (!response?.data?.items?.length) {
433
+ showMessage("No QR codes found to print", "warning");
434
+ return;
435
+ }
436
+
437
+ printPayload.value = response.data.items.map((card: any) => ({
438
+ accessLevel: card.accessLevel,
439
+ cardNumber: card.extractedCardNo,
440
+ qrDataImage: card.qrTag,
441
+ qrTagCardNo: card.qrTagCardNo,
442
+ }));
443
+
444
+ showPrintDialog.value = true;
445
+
446
+ await nextTick();
447
+ await nextTick();
448
+
449
+ printQrRef.value?.downloadPDF();
450
+ showMessage("Printing all QR codes...", "success");
451
+ } catch (error: any) {
452
+ showMessage(error || "Failed to fetch QR codes", "error");
453
+ } finally {
454
+ isGenerating.value = false;
455
+ }
456
+ }
457
+
178
458
  function showMessage(msg: string, color: string) {
179
459
  message.value = msg;
180
460
  messageColor.value = color;