@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.
- package/CHANGELOG.md +6 -0
- package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
- package/components/BuildingManagement/units.vue +2 -2
- package/components/BuildingUnitFormAdd.vue +4 -4
- package/components/BuildingUnitFormEdit.vue +114 -68
- package/components/EntryPassInformation.vue +251 -23
- package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +88 -85
- package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
- package/components/Input/DateTimePicker.vue +17 -11
- package/components/ManageChecklistMain.vue +379 -41
- package/components/TableHygiene.vue +42 -452
- package/components/UnitPersonCard.vue +74 -14
- package/components/VisitorForm.vue +77 -21
- package/components/VisitorFormSelection.vue +13 -2
- package/components/VisitorManagement.vue +83 -55
- package/composables/useCleaningPermission.ts +7 -7
- package/composables/useDashboardData.ts +2 -2
- package/composables/{useSupply.ts → useEquipment.ts} +11 -11
- package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
- package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
- package/composables/useEquipmentManagementPermission.ts +96 -0
- package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
- package/composables/useVehicle.ts +21 -2
- package/composables/useVisitor.ts +3 -3
- package/composables/useWorkOrder.ts +25 -3
- package/package.json +1 -1
- package/types/building.d.ts +1 -1
- package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
- package/types/{supply.d.ts → equipment.d.ts} +2 -2
- package/types/people.d.ts +3 -1
- package/types/vehicle.d.ts +2 -0
- 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="
|
|
66
|
+
:items="cardItems"
|
|
67
67
|
:loading="cardsLoading"
|
|
68
|
-
item-title="
|
|
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="
|
|
106
|
+
@click="toggleBarcodeScanning"
|
|
106
107
|
>
|
|
107
|
-
|
|
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="
|
|
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<
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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>
|