@7365admin1/layer-common 1.10.7 → 1.10.8

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