@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.
- package/CHANGELOG.md +12 -0
- package/components/AccessCardAddForm.vue +1 -1
- package/components/AccessCardAssignToUnitForm.vue +1 -1
- package/components/AccessManagement.vue +1 -1
- 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/Carousel.vue +474 -0
- package/components/DrawImage.vue +172 -0
- package/components/EntryPassInformation.vue +283 -29
- package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +95 -87
- package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
- package/components/Feedback/Form.vue +4 -4
- package/components/FeedbackMain.vue +748 -145
- package/components/FileInput.vue +289 -0
- package/components/Input/DateTimePicker.vue +17 -11
- package/components/ManageChecklistMain.vue +379 -41
- package/components/StockCard.vue +11 -7
- package/components/TableHygiene.vue +42 -452
- package/components/UnitPersonCard.vue +74 -14
- package/components/VisitorForm.vue +193 -52
- package/components/VisitorFormSelection.vue +13 -2
- package/components/VisitorManagement.vue +83 -55
- package/composables/useAccessManagement.ts +41 -18
- package/composables/useCleaningPermission.ts +7 -7
- package/composables/useDashboardData.ts +2 -2
- package/composables/useEquipment.ts +63 -0
- package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
- package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
- package/composables/{useSupply.ts → useEquipmentManagement.ts} +1 -1
- package/composables/useEquipmentManagementPermission.ts +96 -0
- package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
- package/composables/useFeedback.ts +53 -21
- package/composables/useLocalAuth.ts +29 -1
- package/composables/useUploadFiles.ts +94 -0
- package/composables/useUtils.ts +152 -53
- package/composables/useVehicle.ts +21 -2
- package/composables/useVisitor.ts +9 -7
- package/composables/useWorkOrder.ts +25 -3
- package/package.json +2 -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/feedback.d.ts +5 -2
- package/types/people.d.ts +3 -1
- package/types/user.d.ts +1 -0
- package/types/vehicle.d.ts +2 -0
- 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
|
-
<
|
|
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="
|
|
73
|
+
:items="cardItems"
|
|
67
74
|
:loading="cardsLoading"
|
|
68
|
-
item-title="
|
|
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.
|
|
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="
|
|
113
|
+
@click="toggleBarcodeScanning"
|
|
106
114
|
>
|
|
107
|
-
|
|
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="
|
|
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<
|
|
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"
|
|
227
|
+
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards"]);
|
|
153
228
|
|
|
154
|
-
const {
|
|
229
|
+
const { getAvailableContractorCards } = useAccessManagement();
|
|
155
230
|
|
|
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
|
-
);
|
|
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
|
-
|
|
253
|
+
// ─── Card fetching ────────────────────────────────────────────────
|
|
254
|
+
const cardItems = ref<any[]>([]);
|
|
187
255
|
const cardsLoading = ref(false);
|
|
256
|
+
const availableCount = ref<number | null>(null);
|
|
188
257
|
|
|
189
|
-
|
|
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
|
|
195
|
-
|
|
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")
|
|
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>
|