@7365admin1/layer-common 1.10.7 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardAddForm.vue +1 -1
  3. package/components/AccessCardAssignToUnitForm.vue +1 -1
  4. package/components/AccessManagement.vue +1 -1
  5. package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
  6. package/components/BuildingManagement/units.vue +2 -2
  7. package/components/BuildingUnitFormAdd.vue +4 -4
  8. package/components/BuildingUnitFormEdit.vue +114 -68
  9. package/components/Carousel.vue +474 -0
  10. package/components/DrawImage.vue +172 -0
  11. package/components/EntryPassInformation.vue +283 -29
  12. package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +95 -87
  13. package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
  14. package/components/Feedback/Form.vue +4 -4
  15. package/components/FeedbackMain.vue +748 -145
  16. package/components/FileInput.vue +289 -0
  17. package/components/Input/DateTimePicker.vue +17 -11
  18. package/components/ManageChecklistMain.vue +379 -41
  19. package/components/StockCard.vue +11 -7
  20. package/components/TableHygiene.vue +42 -452
  21. package/components/UnitPersonCard.vue +74 -14
  22. package/components/VisitorForm.vue +193 -52
  23. package/components/VisitorFormSelection.vue +13 -2
  24. package/components/VisitorManagement.vue +83 -55
  25. package/composables/useAccessManagement.ts +41 -18
  26. package/composables/useCleaningPermission.ts +7 -7
  27. package/composables/useDashboardData.ts +2 -2
  28. package/composables/useEquipment.ts +63 -0
  29. package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
  30. package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
  31. package/composables/{useSupply.ts → useEquipmentManagement.ts} +1 -1
  32. package/composables/useEquipmentManagementPermission.ts +96 -0
  33. package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
  34. package/composables/useFeedback.ts +53 -21
  35. package/composables/useLocalAuth.ts +29 -1
  36. package/composables/useUploadFiles.ts +94 -0
  37. package/composables/useUtils.ts +152 -53
  38. package/composables/useVehicle.ts +21 -2
  39. package/composables/useVisitor.ts +9 -7
  40. package/composables/useWorkOrder.ts +25 -3
  41. package/package.json +2 -1
  42. package/types/building.d.ts +1 -1
  43. package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
  44. package/types/{supply.d.ts → equipment.d.ts} +2 -2
  45. package/types/feedback.d.ts +5 -2
  46. package/types/people.d.ts +3 -1
  47. package/types/user.d.ts +1 -0
  48. package/types/vehicle.d.ts +2 -0
  49. package/types/visitor.d.ts +2 -1
@@ -46,12 +46,19 @@
46
46
  No printer is configured in the settings. QR Pass may not print.
47
47
  </v-alert>
48
48
  <template v-else>
49
- <InputLabel title="Quantity" class="mt-3" />
49
+ <div class="d-flex align-center justify-space-between mt-3">
50
+ <InputLabel title="Quantity" />
51
+ <span v-if="availableCount !== null" class="text-caption text-grey">
52
+ {{ availableCount }} available
53
+ </span>
54
+ <v-progress-circular v-else-if="cardsLoading" size="14" width="2" indeterminate color="grey" />
55
+ </div>
50
56
  <v-text-field
51
57
  v-model.number="quantity"
52
58
  type="number"
53
59
  density="comfortable"
54
60
  :min="1"
61
+ :max="availableCount ?? undefined"
55
62
  hide-details
56
63
  />
57
64
  </template>
@@ -63,9 +70,9 @@
63
70
  <InputLabel title="Select Cards" />
64
71
  <v-select
65
72
  v-model="selectedCards"
66
- :items="cards"
73
+ :items="cardItems"
67
74
  :loading="cardsLoading"
68
- item-title="name"
75
+ item-title="cardNo"
69
76
  item-value="_id"
70
77
  density="comfortable"
71
78
  multiple
@@ -73,11 +80,12 @@
73
80
  closable-chips
74
81
  hide-details
75
82
  placeholder="Select cards..."
83
+ return-object
76
84
  >
77
85
  <template #item="{ props: itemProps, item }">
78
86
  <v-list-item v-bind="itemProps">
79
87
  <template #subtitle>
80
- <span class="text-caption text-grey">{{ item.raw.cardNumber }}</span>
88
+ <span class="text-caption text-grey">{{ item.raw.cardNo }}</span>
81
89
  </template>
82
90
  </v-list-item>
83
91
  </template>
@@ -98,13 +106,14 @@
98
106
  <v-row no-gutters class="ga-3 mt-3">
99
107
  <v-col>
100
108
  <v-btn
109
+ :color="isBarcodeScanningActive ? 'success' : 'primary'"
101
110
  variant="outlined"
102
- color="primary"
103
111
  block
104
112
  prepend-icon="mdi-barcode-scan"
105
- @click="emit('scan:barcode')"
113
+ @click="toggleBarcodeScanning"
106
114
  >
107
- Scan Via BarCode
115
+ <v-icon v-if="isBarcodeScanningActive" start>mdi-check-circle</v-icon>
116
+ {{ isBarcodeScanningActive ? 'Scanning...' : 'Scan Via BarCode' }}
108
117
  </v-btn>
109
118
  </v-col>
110
119
  <v-col>
@@ -113,15 +122,73 @@
113
122
  color="primary"
114
123
  block
115
124
  prepend-icon="mdi-camera"
116
- @click="emit('scan:camera')"
125
+ @click="openCameraDialog"
117
126
  >
118
127
  Scan with Camera
119
128
  </v-btn>
120
129
  </v-col>
121
130
  </v-row>
131
+
132
+ <v-alert
133
+ v-if="isBarcodeScanningActive"
134
+ type="info"
135
+ variant="tonal"
136
+ density="compact"
137
+ class="mt-3 text-center"
138
+ >
139
+ <v-icon class="mr-2 animate-pulse">mdi-radar</v-icon>
140
+ Ready to scan NFC cards. Please scan a card with your barcode scanner.
141
+ </v-alert>
122
142
  </template>
123
143
  </template>
124
144
  <v-divider class="mt-6" />
145
+
146
+ <!-- Hidden canvas used for frame-by-frame barcode detection -->
147
+ <canvas ref="canvasRef" style="display: none" />
148
+
149
+ <!-- Camera scanner dialog -->
150
+ <v-dialog
151
+ v-model="showCameraDialog"
152
+ width="700"
153
+ max-width="700"
154
+ persistent
155
+ transition="dialog-bottom-transition"
156
+ @after-enter="startCamera"
157
+ @after-leave="stopCamera"
158
+ >
159
+ <v-container class="d-flex justify-center" style="max-height: 90vh">
160
+ <v-card elevation="2" class="d-flex flex-column align-center pa-2 w-100">
161
+ <v-toolbar color="transparent">
162
+ <v-card-title class="text-h5">Scan Card</v-card-title>
163
+ <v-spacer />
164
+ <v-btn icon="mdi-close" color="grey-darken-1" @click="closeCameraDialog" />
165
+ </v-toolbar>
166
+
167
+ <div class="position-relative w-100" style="aspect-ratio: 4/3; background: #000">
168
+ <video
169
+ ref="videoRef"
170
+ autoplay
171
+ playsinline
172
+ muted
173
+ class="w-100 h-100"
174
+ style="object-fit: cover"
175
+ />
176
+ <!-- scan overlay -->
177
+ <div class="scan-overlay">
178
+ <div class="scan-box" />
179
+ </div>
180
+ </div>
181
+
182
+ <div v-if="cameraError" class="pa-3 text-center text-error text-caption">
183
+ {{ cameraError }}
184
+ </div>
185
+
186
+ <v-card-actions class="justify-center">
187
+ <v-btn icon="mdi-camera-switch" color="primary" @click="switchCamera" />
188
+ </v-card-actions>
189
+ </v-card>
190
+ </v-container>
191
+ </v-dialog>
125
192
  </div>
126
193
  </template>
127
194
 
@@ -144,26 +211,26 @@ const props = defineProps({
144
211
  default: null,
145
212
  },
146
213
  cards: {
147
- type: Array as PropType<string[]>,
214
+ type: Array as PropType<any[]>,
148
215
  default: () => [],
149
216
  },
217
+ siteId: {
218
+ type: String as PropType<string | null>,
219
+ default: null,
220
+ },
221
+ unitId: {
222
+ type: String as PropType<string | null>,
223
+ default: null,
224
+ },
150
225
  });
151
226
 
152
- const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "scan:barcode", "scan:camera"]);
227
+ const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards"]);
153
228
 
154
- const { getAll: getAllCards } = useCard();
229
+ const { getAvailableContractorCards } = useAccessManagement();
155
230
 
156
- const nfcEnabled = computed(
157
- () => props.settings?.data?.settings?.nfcPass ?? false
158
- );
159
-
160
- const printer = computed(
161
- () => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null }
162
- );
163
-
164
- const hasPrinter = computed(
165
- () => !!printer.value.vendorId && !!printer.value.productId
166
- );
231
+ const nfcEnabled = computed(() => props.settings?.data?.settings?.nfcPass ?? false);
232
+ const printer = computed(() => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null });
233
+ const hasPrinter = computed(() => !!printer.value.vendorId && !!printer.value.productId);
167
234
 
168
235
  const passType = computed({
169
236
  get: () => props.modelValue,
@@ -183,27 +250,188 @@ const selectedCards = computed({
183
250
  },
184
251
  });
185
252
 
186
- const cardItems = ref<TCard[]>([]);
253
+ // ─── Card fetching ────────────────────────────────────────────────
254
+ const cardItems = ref<any[]>([]);
187
255
  const cardsLoading = ref(false);
256
+ const availableCount = ref<number | null>(null);
188
257
 
189
- const cards = computed(() => cardItems.value);
190
-
191
- async function fetchCards() {
258
+ async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
259
+ if (!props.siteId || !props.unitId) return;
192
260
  cardsLoading.value = true;
193
261
  try {
194
- const res = await getAllCards({ assignFilter: "available", limit: 100 });
195
- cardItems.value = res?.data ?? [];
262
+ const res = await getAvailableContractorCards({
263
+ type,
264
+ siteId: props.siteId,
265
+ unitId: props.unitId,
266
+ });
267
+ cardItems.value = res?.data?.[0]?.items ?? [];
268
+ availableCount.value = (res?.data?.[2] as any)?.count ?? cardItems.value.length;
196
269
  } finally {
197
270
  cardsLoading.value = false;
198
271
  }
199
272
  }
200
273
 
274
+ function addCard(card: any) {
275
+ const already = selectedCards.value.some((c) => c._id === card._id);
276
+ if (!already) selectedCards.value = [...selectedCards.value, card];
277
+ }
278
+
279
+ // ─── Barcode scanning ─────────────────────────────────────────────
280
+ const isBarcodeScanningActive = ref(false);
281
+ const barcodeBuffer = ref("");
282
+ const barcodeTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
283
+
284
+ const toggleBarcodeScanning = () => {
285
+ isBarcodeScanningActive.value = !isBarcodeScanningActive.value;
286
+ if (!isBarcodeScanningActive.value) {
287
+ barcodeBuffer.value = "";
288
+ if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
289
+ }
290
+ };
291
+
292
+ const validateScannedBarcode = (scannedCode: string) => {
293
+ const card = cardItems.value.find((c) => c.cardNo === scannedCode);
294
+ if (card) addCard(card);
295
+ barcodeBuffer.value = "";
296
+ };
297
+
298
+ const handleBarcodeInput = (event: KeyboardEvent) => {
299
+ if (!isBarcodeScanningActive.value || passType.value !== "NFC") return;
300
+
301
+ if (event.key === "Enter") {
302
+ event.preventDefault();
303
+ if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
304
+ if (barcodeBuffer.value.trim()) validateScannedBarcode(barcodeBuffer.value.trim());
305
+ return;
306
+ }
307
+
308
+ if (event.key.length === 1) {
309
+ barcodeBuffer.value += event.key;
310
+ if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
311
+ barcodeTimeout.value = setTimeout(() => { barcodeBuffer.value = ""; }, 200);
312
+ }
313
+ };
314
+
315
+ // ─── Camera scanner ───────────────────────────────────────────────
316
+ const videoRef = ref<HTMLVideoElement | null>(null);
317
+ const canvasRef = ref<HTMLCanvasElement | null>(null);
318
+ const showCameraDialog = ref(false);
319
+ const cameraError = ref("");
320
+ const cameraFacingMode = ref<"environment" | "user">("environment");
321
+ let mediaStream: MediaStream | null = null;
322
+ let scanIntervalId: ReturnType<typeof setInterval> | null = null;
323
+ let barcodeDetector: any = null;
324
+
325
+ const openCameraDialog = () => {
326
+ cameraError.value = "";
327
+ showCameraDialog.value = true;
328
+ };
329
+
330
+ const closeCameraDialog = () => {
331
+ showCameraDialog.value = false;
332
+ };
333
+
334
+ const startCamera = async () => {
335
+ cameraError.value = "";
336
+
337
+ if (!("BarcodeDetector" in window)) {
338
+ cameraError.value = "BarcodeDetector is not supported in this browser. Use Chrome or Edge.";
339
+ return;
340
+ }
341
+
342
+ try {
343
+ mediaStream = await navigator.mediaDevices.getUserMedia({
344
+ video: { facingMode: cameraFacingMode.value, width: { ideal: 1280 }, height: { ideal: 720 } },
345
+ });
346
+
347
+ if (videoRef.value) {
348
+ videoRef.value.srcObject = mediaStream;
349
+ await videoRef.value.play();
350
+ }
351
+
352
+ barcodeDetector = new (window as any).BarcodeDetector({
353
+ formats: ["qr_code", "code_128", "code_39", "ean_13", "ean_8"],
354
+ });
355
+
356
+ // Wait a tick for the video to have real frame data before scanning
357
+ await new Promise((resolve) => setTimeout(resolve, 500));
358
+ startScanLoop();
359
+ } catch (err: any) {
360
+ cameraError.value = `Camera error: ${err.message}`;
361
+ }
362
+ };
363
+
364
+ const startScanLoop = () => {
365
+ if (scanIntervalId !== null) clearInterval(scanIntervalId);
366
+ scanIntervalId = setInterval(scanFrame, 300);
367
+ };
368
+
369
+ const scanFrame = async () => {
370
+ const video = videoRef.value;
371
+ const canvas = canvasRef.value;
372
+ if (!video || !canvas || !barcodeDetector || !showCameraDialog.value) return;
373
+ if (video.readyState < 2 || video.videoWidth === 0) return;
374
+
375
+ const ctx = canvas.getContext("2d");
376
+ if (!ctx) return;
377
+
378
+ canvas.width = video.videoWidth;
379
+ canvas.height = video.videoHeight;
380
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
381
+
382
+ try {
383
+ const barcodes = await barcodeDetector.detect(canvas);
384
+ for (const barcode of barcodes) {
385
+ const card = cardItems.value.find((c) => c.cardNo === barcode.rawValue);
386
+ if (card) {
387
+ addCard(card);
388
+ closeCameraDialog();
389
+ return;
390
+ }
391
+ }
392
+ } catch {
393
+ // silent — detector throws on blank frames
394
+ }
395
+ };
396
+
397
+ const stopCamera = () => {
398
+ if (scanIntervalId !== null) { clearInterval(scanIntervalId); scanIntervalId = null; }
399
+ if (mediaStream) { mediaStream.getTracks().forEach((t) => t.stop()); mediaStream = null; }
400
+ if (videoRef.value) videoRef.value.srcObject = null;
401
+ barcodeDetector = null;
402
+ };
403
+
404
+ const switchCamera = async () => {
405
+ stopCamera();
406
+ cameraFacingMode.value = cameraFacingMode.value === "environment" ? "user" : "environment";
407
+ await startCamera();
408
+ };
409
+
410
+ // ─── Watchers & lifecycle ─────────────────────────────────────────
201
411
  watch(
202
412
  () => passType.value,
203
413
  (val) => {
204
- if (val === "NFC") fetchCards();
414
+ if (val === "NFC") {
415
+ fetchCards("NFC");
416
+ window.addEventListener("keydown", handleBarcodeInput);
417
+ } else if (val === "QR") {
418
+ availableCount.value = null;
419
+ fetchCards("QRCODE");
420
+ } else {
421
+ availableCount.value = null;
422
+ isBarcodeScanningActive.value = false;
423
+ barcodeBuffer.value = "";
424
+ if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
425
+ window.removeEventListener("keydown", handleBarcodeInput);
426
+ }
205
427
  }
206
428
  );
429
+
430
+ onUnmounted(() => {
431
+ window.removeEventListener("keydown", handleBarcodeInput);
432
+ if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
433
+ stopCamera();
434
+ });
207
435
  </script>
208
436
 
209
437
  <style scoped>
@@ -212,4 +440,30 @@ watch(
212
440
  top: 6px;
213
441
  right: 6px;
214
442
  }
443
+
444
+ .animate-pulse {
445
+ animation: pulse 1.5s ease-in-out infinite;
446
+ }
447
+
448
+ @keyframes pulse {
449
+ 0%, 100% { opacity: 1; }
450
+ 50% { opacity: 0.3; }
451
+ }
452
+
453
+ .scan-overlay {
454
+ position: absolute;
455
+ inset: 0;
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: center;
459
+ pointer-events: none;
460
+ }
461
+
462
+ .scan-box {
463
+ width: 220px;
464
+ height: 220px;
465
+ border: 3px solid rgba(255, 255, 255, 0.85);
466
+ border-radius: 12px;
467
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
468
+ }
215
469
  </style>