@7365admin1/layer-common 1.10.8 → 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 +6 -0
- package/components/AccessCardAddForm.vue +1 -1
- package/components/AccessCardAssignToUnitForm.vue +1 -1
- package/components/AccessManagement.vue +1 -1
- package/components/Carousel.vue +474 -0
- package/components/DrawImage.vue +172 -0
- package/components/EntryPassInformation.vue +36 -10
- package/components/EquipmentItemMain.vue +9 -4
- package/components/Feedback/Form.vue +4 -4
- package/components/FeedbackMain.vue +748 -145
- package/components/FileInput.vue +289 -0
- package/components/StockCard.vue +11 -7
- package/components/VisitorForm.vue +129 -44
- package/composables/useAccessManagement.ts +41 -18
- package/composables/useEquipmentManagement.ts +63 -0
- 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/useVisitor.ts +6 -4
- package/package.json +2 -1
- package/types/feedback.d.ts +5 -2
- package/types/user.d.ts +1 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-file-input
|
|
4
|
+
v-model="files"
|
|
5
|
+
:label="label"
|
|
6
|
+
accept="image/*"
|
|
7
|
+
:prepend-icon="prependIcon"
|
|
8
|
+
:loading="isFileUploading"
|
|
9
|
+
hide-details
|
|
10
|
+
chips
|
|
11
|
+
multiple
|
|
12
|
+
clearable
|
|
13
|
+
@update:modelValue="handleFileSelect"
|
|
14
|
+
@click:clear="handleClear"
|
|
15
|
+
:hide-input="hasHideInput"
|
|
16
|
+
>
|
|
17
|
+
<template v-slot:append v-if="hasLabel">
|
|
18
|
+
<slot name="append">
|
|
19
|
+
<v-btn color="primary" height="50px" @click="openCameraDialog">
|
|
20
|
+
<v-icon>mdi-camera</v-icon>
|
|
21
|
+
</v-btn>
|
|
22
|
+
</slot>
|
|
23
|
+
</template>
|
|
24
|
+
</v-file-input>
|
|
25
|
+
|
|
26
|
+
<!-- Camera Dialog -->
|
|
27
|
+
<v-dialog
|
|
28
|
+
v-model="showCameraDialog"
|
|
29
|
+
transition="dialog-bottom-transition"
|
|
30
|
+
width="800"
|
|
31
|
+
max-width="800"
|
|
32
|
+
persistent
|
|
33
|
+
@after-enter="startCamera"
|
|
34
|
+
>
|
|
35
|
+
<v-container
|
|
36
|
+
class="d-flex justify-center"
|
|
37
|
+
max-height="90vh"
|
|
38
|
+
width="800"
|
|
39
|
+
max-width="800"
|
|
40
|
+
>
|
|
41
|
+
<v-card elevation="2" class="d-flex flex-column align-center pa-2">
|
|
42
|
+
<v-toolbar>
|
|
43
|
+
<v-card-title class="text-h5">Take a Picture</v-card-title>
|
|
44
|
+
<v-spacer></v-spacer>
|
|
45
|
+
<v-btn
|
|
46
|
+
color="grey-darken-1"
|
|
47
|
+
icon="mdi-close"
|
|
48
|
+
@click="closeCameraDialog"
|
|
49
|
+
></v-btn>
|
|
50
|
+
</v-toolbar>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
id="reader"
|
|
54
|
+
class="d-flex justify-center align-center"
|
|
55
|
+
style="
|
|
56
|
+
position: relative;
|
|
57
|
+
width: 500px;
|
|
58
|
+
min-width: 400px;
|
|
59
|
+
height: 400px;
|
|
60
|
+
"
|
|
61
|
+
>
|
|
62
|
+
<video
|
|
63
|
+
ref="video"
|
|
64
|
+
style="flex: 1; height: 400px; min-width: 300px"
|
|
65
|
+
class="video-shutter"
|
|
66
|
+
autoplay
|
|
67
|
+
></video>
|
|
68
|
+
<canvas
|
|
69
|
+
ref="canvas"
|
|
70
|
+
style="flex: 1; height: 400px; min-width: 300px; display: none"
|
|
71
|
+
></canvas>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<v-row align="center" justify="center">
|
|
75
|
+
<v-col cols="6">
|
|
76
|
+
<v-btn color="primary" icon class="mt-4" @click="switchCamera">
|
|
77
|
+
<v-icon>mdi-camera-switch</v-icon>
|
|
78
|
+
</v-btn>
|
|
79
|
+
</v-col>
|
|
80
|
+
<v-col cols="6">
|
|
81
|
+
<v-btn
|
|
82
|
+
color="secondary"
|
|
83
|
+
icon
|
|
84
|
+
class="mt-4"
|
|
85
|
+
@click="captureImageFromCamera"
|
|
86
|
+
>
|
|
87
|
+
<v-icon large>mdi-camera-outline</v-icon>
|
|
88
|
+
</v-btn>
|
|
89
|
+
</v-col>
|
|
90
|
+
</v-row>
|
|
91
|
+
</v-card>
|
|
92
|
+
</v-container>
|
|
93
|
+
</v-dialog>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<script setup lang="ts">
|
|
100
|
+
interface FileWithPreview {
|
|
101
|
+
name: string;
|
|
102
|
+
data: File;
|
|
103
|
+
progress: number;
|
|
104
|
+
url: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const props = defineProps({
|
|
108
|
+
label: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: "Select File",
|
|
111
|
+
},
|
|
112
|
+
prependIcon: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: "mdi-paperclip",
|
|
115
|
+
},
|
|
116
|
+
required: {
|
|
117
|
+
type: Boolean,
|
|
118
|
+
default: true,
|
|
119
|
+
},
|
|
120
|
+
initFiles: {
|
|
121
|
+
type: Array,
|
|
122
|
+
},
|
|
123
|
+
hasLabel: {
|
|
124
|
+
type: Boolean,
|
|
125
|
+
default: true,
|
|
126
|
+
},
|
|
127
|
+
hasHideInput: {
|
|
128
|
+
type: Boolean,
|
|
129
|
+
default: false,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const emit = defineEmits<{
|
|
134
|
+
(event: "onFileAttach", payload: Array<{ data: File }>): void;
|
|
135
|
+
(event: "update:files", files: FileWithPreview[]): void;
|
|
136
|
+
(event: "onFileRemoved", payload: { index: number; file: File }): void;
|
|
137
|
+
(event: "onClear"): void;
|
|
138
|
+
}>();
|
|
139
|
+
|
|
140
|
+
const { showUploadedFiles } = useUploadFiles();
|
|
141
|
+
const isFileUploading = ref<boolean>(false);
|
|
142
|
+
|
|
143
|
+
const files = ref<File[]>([]);
|
|
144
|
+
const showCameraDialog = ref(false);
|
|
145
|
+
const video = ref<HTMLVideoElement | null>(null);
|
|
146
|
+
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
147
|
+
const cameraFacingMode = ref<"environment" | "user">("environment");
|
|
148
|
+
|
|
149
|
+
const message = ref("");
|
|
150
|
+
const messageColor = ref("");
|
|
151
|
+
const messageSnackbar = ref(false);
|
|
152
|
+
|
|
153
|
+
function showMessage(msg: string, color: string) {
|
|
154
|
+
message.value = msg;
|
|
155
|
+
messageColor.value = color;
|
|
156
|
+
messageSnackbar.value = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
watchEffect(() => {
|
|
160
|
+
files.value = [...(props.initFiles as typeof files.value)];
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const handleFileSelect = async () => {
|
|
164
|
+
if (files.value && files.value.length > 0) {
|
|
165
|
+
const newFiles = files.value.map((file: File) => ({
|
|
166
|
+
name: file.name,
|
|
167
|
+
data: file,
|
|
168
|
+
progress: 0,
|
|
169
|
+
url: URL.createObjectURL(file),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
// attachedFiles.value = [...newFiles];
|
|
173
|
+
showUploadedFiles(newFiles);
|
|
174
|
+
|
|
175
|
+
emit("update:files", newFiles);
|
|
176
|
+
} else {
|
|
177
|
+
files.value = [...(props.initFiles as typeof files.value)];
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleClear = () => {
|
|
182
|
+
files.value = [];
|
|
183
|
+
emit("onClear");
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const openCameraDialog = () => {
|
|
187
|
+
showCameraDialog.value = true;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const closeCameraDialog = () => {
|
|
191
|
+
showCameraDialog.value = false;
|
|
192
|
+
stopCamera();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const startCamera = async () => {
|
|
196
|
+
try {
|
|
197
|
+
const constraints = {
|
|
198
|
+
video: {
|
|
199
|
+
facingMode: cameraFacingMode.value,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
204
|
+
if (video.value) {
|
|
205
|
+
video.value.srcObject = stream;
|
|
206
|
+
video.value.play();
|
|
207
|
+
}
|
|
208
|
+
} catch (error: any) {
|
|
209
|
+
showMessage(error, "error");
|
|
210
|
+
closeCameraDialog();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const stopCamera = () => {
|
|
215
|
+
if (video.value) {
|
|
216
|
+
const stream = video.value.srcObject as MediaStream;
|
|
217
|
+
if (stream) {
|
|
218
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const switchCamera = async () => {
|
|
224
|
+
await stopCamera();
|
|
225
|
+
cameraFacingMode.value =
|
|
226
|
+
cameraFacingMode.value === "environment" ? "user" : "environment";
|
|
227
|
+
showMessage(
|
|
228
|
+
`Switched to ${
|
|
229
|
+
cameraFacingMode.value === "environment" ? "Back Camera" : "Front Camera"
|
|
230
|
+
}`,
|
|
231
|
+
"error"
|
|
232
|
+
);
|
|
233
|
+
startCamera();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const captureImageFromCamera = () => {
|
|
237
|
+
if (!video.value || !canvas.value) return;
|
|
238
|
+
|
|
239
|
+
const context = canvas.value.getContext("2d");
|
|
240
|
+
if (!context) return;
|
|
241
|
+
|
|
242
|
+
// Set canvas dimensions to match video
|
|
243
|
+
canvas.value.width = video.value.videoWidth;
|
|
244
|
+
canvas.value.height = video.value.videoHeight;
|
|
245
|
+
|
|
246
|
+
// Capture the frame
|
|
247
|
+
context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
|
|
248
|
+
|
|
249
|
+
// Convert to file
|
|
250
|
+
canvas.value.toBlob((blob) => {
|
|
251
|
+
if (!blob) return;
|
|
252
|
+
|
|
253
|
+
const file = new File([blob], `camera-capture-${Date.now()}.png`, {
|
|
254
|
+
type: "image/png",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
files.value = [file];
|
|
258
|
+
handleFileSelect();
|
|
259
|
+
closeCameraDialog();
|
|
260
|
+
}, "image/png");
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const removeFile = (index) => {
|
|
264
|
+
const removedFile = files.value[index];
|
|
265
|
+
files.value = files.value.filter((_, i) => i !== index);
|
|
266
|
+
emit("onFileRemoved", { index, file: removedFile }); // Emit when a file is removed
|
|
267
|
+
// emit("onFilesUpdated", files.value); // Emit updated files list
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Cleanup
|
|
271
|
+
onUnmounted(() => {
|
|
272
|
+
stopCamera();
|
|
273
|
+
});
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<style scoped>
|
|
277
|
+
.custom-chip {
|
|
278
|
+
max-width: 100%;
|
|
279
|
+
height: auto !important;
|
|
280
|
+
white-space: normal;
|
|
281
|
+
padding: 3px 20px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.chip-text {
|
|
285
|
+
word-break: break-word;
|
|
286
|
+
white-space: normal;
|
|
287
|
+
line-height: 1.2;
|
|
288
|
+
}
|
|
289
|
+
</style>
|
package/components/StockCard.vue
CHANGED
|
@@ -110,6 +110,10 @@
|
|
|
110
110
|
</template>
|
|
111
111
|
|
|
112
112
|
<script setup lang="ts">
|
|
113
|
+
import useEquipment from "../composables/useEquipment";
|
|
114
|
+
import useStock from "../composables/useStock";
|
|
115
|
+
import useUtils from "../composables/useUtils";
|
|
116
|
+
|
|
113
117
|
const props = defineProps({
|
|
114
118
|
orgId: { type: String, default: "" },
|
|
115
119
|
site: { type: String, default: "" },
|
|
@@ -126,18 +130,18 @@ const supply = ref({
|
|
|
126
130
|
remarks: "",
|
|
127
131
|
});
|
|
128
132
|
|
|
129
|
-
const {
|
|
133
|
+
const { getEquipmentById } = useEquipment();
|
|
130
134
|
|
|
131
135
|
const supplyId = useRoute().params.id as string;
|
|
132
136
|
|
|
133
|
-
const { data:
|
|
134
|
-
`get-
|
|
135
|
-
() =>
|
|
137
|
+
const { data: getEquipmentByIdReq } = await useLazyAsyncData(
|
|
138
|
+
`get-equipment-by-id-${supplyId}`,
|
|
139
|
+
() => getEquipmentById(supplyId)
|
|
136
140
|
);
|
|
137
141
|
|
|
138
142
|
watchEffect(() => {
|
|
139
|
-
if (
|
|
140
|
-
supply.value =
|
|
143
|
+
if (getEquipmentByIdReq?.value) {
|
|
144
|
+
supply.value = getEquipmentByIdReq.value as any;
|
|
141
145
|
}
|
|
142
146
|
});
|
|
143
147
|
|
|
@@ -163,7 +167,7 @@ const {
|
|
|
163
167
|
() => getStockBySupply(props.site, supplyId),
|
|
164
168
|
{
|
|
165
169
|
watch: [page, () => props.site, () => supplyId],
|
|
166
|
-
}
|
|
170
|
+
}
|
|
167
171
|
);
|
|
168
172
|
|
|
169
173
|
watchEffect(() => {
|
|
@@ -144,7 +144,8 @@
|
|
|
144
144
|
|
|
145
145
|
<v-col v-if="shouldShowField('unit')" cols="12">
|
|
146
146
|
<InputLabel class="text-capitalize" title="Registered Unit Company Name" required />
|
|
147
|
-
<v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable"
|
|
147
|
+
<v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable"
|
|
148
|
+
:loading="buildingUnitDataPending" readonly class="no-pointer" />
|
|
148
149
|
</v-col>
|
|
149
150
|
|
|
150
151
|
<v-col v-if="shouldShowField('remarks')" cols="12">
|
|
@@ -154,10 +155,16 @@
|
|
|
154
155
|
|
|
155
156
|
<v-col v-if="prop.type === 'contractor' && contractorStep === 2" cols="12">
|
|
156
157
|
<PassInformation />
|
|
157
|
-
<EntryPassInformation
|
|
158
|
-
v-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
<EntryPassInformation
|
|
159
|
+
v-if="entryPassSettings?.data?.settings?.nfcPass"
|
|
160
|
+
v-model="passType"
|
|
161
|
+
v-model:quantity="passQuantity"
|
|
162
|
+
v-model:cards="passCards"
|
|
163
|
+
:settings="entryPassSettings"
|
|
164
|
+
:loading="entryPassSettingsPending"
|
|
165
|
+
:site-id="prop.site"
|
|
166
|
+
:unit-id="visitor.unit || null"
|
|
167
|
+
/>
|
|
161
168
|
</v-col>
|
|
162
169
|
|
|
163
170
|
<v-col v-if="prop.type === 'contractor' && contractorStep === 3" cols="12">
|
|
@@ -190,8 +197,8 @@
|
|
|
190
197
|
prop.type === 'contractor' &&
|
|
191
198
|
contractorStep > 1
|
|
192
199
|
" tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
|
|
193
|
-
<v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
|
|
194
|
-
@click="backToSelection" text="Back to Selection" />
|
|
200
|
+
<v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
|
|
201
|
+
size="48" @click="backToSelection" text="Back to Selection" />
|
|
195
202
|
<v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
|
|
196
203
|
</v-col>
|
|
197
204
|
<v-col cols="6">
|
|
@@ -209,6 +216,44 @@
|
|
|
209
216
|
:vehicle-number-users-list="vehicleNumberUserItems" @close="dialog.vehicleNumberUsersList = false"
|
|
210
217
|
@update:people="handleAutofillDataViaVehicleNumber" />
|
|
211
218
|
</v-dialog>
|
|
219
|
+
|
|
220
|
+
<v-dialog v-model="dialog.showNonCheckedOutDialog" max-width="700" persistent>
|
|
221
|
+
<v-card :loading="loading.checkingOut">
|
|
222
|
+
<v-toolbar>
|
|
223
|
+
<v-toolbar-title>
|
|
224
|
+
<v-row no-gutters class="d-flex align-center justify-space-between">
|
|
225
|
+
<span class="font-weight-bold">
|
|
226
|
+
You have an unchecked-out vehicle for: {{ visitor.plateNumber }}
|
|
227
|
+
</span>
|
|
228
|
+
</v-row>
|
|
229
|
+
</v-toolbar-title>
|
|
230
|
+
</v-toolbar>
|
|
231
|
+
|
|
232
|
+
<v-card-text>
|
|
233
|
+
|
|
234
|
+
<v-list lines="three">
|
|
235
|
+
<v-list-item v-if="matchingPlateNumberNonCheckedOutArr.length > 0"
|
|
236
|
+
v-for="v in matchingPlateNumberNonCheckedOutArr" :key="v._id" class="cursor-pointer">
|
|
237
|
+
<v-list-item-title>
|
|
238
|
+
{{ v.plateNumber }} - {{ v.name }}
|
|
239
|
+
</v-list-item-title>
|
|
240
|
+
|
|
241
|
+
<v-list-item-subtitle>
|
|
242
|
+
Checked In at : {{ UTCToLocalTIme(v.checkIn) }}
|
|
243
|
+
</v-list-item-subtitle>
|
|
244
|
+
|
|
245
|
+
<template #append>
|
|
246
|
+
<v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
|
|
247
|
+
:loading="loading.checkingOut && v?._id === prop.visitorData?._id"
|
|
248
|
+
@click.stop="handleCheckout(v._id)" />
|
|
249
|
+
</template>
|
|
250
|
+
|
|
251
|
+
</v-list-item>
|
|
252
|
+
</v-list>
|
|
253
|
+
|
|
254
|
+
</v-card-text>
|
|
255
|
+
</v-card>
|
|
256
|
+
</v-dialog>
|
|
212
257
|
</v-card>
|
|
213
258
|
</template>
|
|
214
259
|
|
|
@@ -243,12 +288,12 @@ type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
|
|
|
243
288
|
const currentAutofillSource = ref<AutofillSource>(null);
|
|
244
289
|
|
|
245
290
|
|
|
246
|
-
const { requiredRule, debounce } = useUtils();
|
|
291
|
+
const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
|
|
247
292
|
const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
|
|
248
|
-
const { createVisitor, typeFieldMap, contractorTypes } = useVisitor();
|
|
293
|
+
const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
|
|
249
294
|
const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
|
|
250
295
|
const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
|
|
251
|
-
const { getById: getUnitDataById} = useBuildingUnit()
|
|
296
|
+
const { getById: getUnitDataById } = useBuildingUnit()
|
|
252
297
|
|
|
253
298
|
const emit = defineEmits([
|
|
254
299
|
"back",
|
|
@@ -278,15 +323,20 @@ const visitor = reactive<Partial<TVisitorPayload>>({
|
|
|
278
323
|
});
|
|
279
324
|
|
|
280
325
|
const passType = ref("");
|
|
281
|
-
const passQuantity = ref<number | null>(
|
|
326
|
+
const passQuantity = ref<number | null>(1);
|
|
282
327
|
const passCards = ref<string[]>([]);
|
|
283
328
|
|
|
284
329
|
const registeredUnitCompanyName = ref('N/A')
|
|
285
330
|
|
|
286
331
|
const dialog = reactive({
|
|
287
332
|
vehicleNumberUsersList: false,
|
|
333
|
+
showNonCheckedOutDialog: false,
|
|
288
334
|
});
|
|
289
335
|
|
|
336
|
+
const loading = reactive({
|
|
337
|
+
checkingOut: false,
|
|
338
|
+
})
|
|
339
|
+
|
|
290
340
|
const validForm = ref(false);
|
|
291
341
|
const formRef = ref<HTMLFormElement | null>(null);
|
|
292
342
|
const processing = ref(false);
|
|
@@ -306,6 +356,8 @@ const blocksArray = ref<TDefaultOptionObj[]>([]);
|
|
|
306
356
|
const levelsArray = ref<TDefaultOptionObj[]>([]);
|
|
307
357
|
const unitsArray = ref<TDefaultOptionObj[]>([]);
|
|
308
358
|
|
|
359
|
+
const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([])
|
|
360
|
+
|
|
309
361
|
|
|
310
362
|
const vehicleNumberUserItems = ref<TPeople[]>([])
|
|
311
363
|
|
|
@@ -466,25 +518,70 @@ watch(fetchCompanyListReq, (arr) => {
|
|
|
466
518
|
}
|
|
467
519
|
})
|
|
468
520
|
const {
|
|
469
|
-
data:
|
|
470
|
-
refresh:
|
|
471
|
-
pending:
|
|
521
|
+
data: fetchVisitorListByVehicleNumberReq,
|
|
522
|
+
refresh: fetchVisitorListByVehicleNumberRefresh,
|
|
523
|
+
pending: fetchVisitorListByVehicleNumberPending,
|
|
472
524
|
|
|
473
|
-
} = useLazyAsyncData(`fetch-vehicle-number
|
|
525
|
+
} = useLazyAsyncData(`fetch-visitor-list-by-vehicle-number`, () => {
|
|
474
526
|
if (!visitor.plateNumber) return Promise.resolve(null)
|
|
475
|
-
return
|
|
527
|
+
return getVisitors({ page: 1, limit: 20, site: prop.site, plateNumber: visitor.plateNumber, checkedOut: false, status: "registered" })
|
|
476
528
|
})
|
|
477
529
|
|
|
478
|
-
watch(
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
530
|
+
watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
|
|
531
|
+
const itemsArray = arr?.items || []
|
|
532
|
+
const isValidArray = Array.isArray(itemsArray)
|
|
533
|
+
matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : []
|
|
534
|
+
if(matchingPlateNumberNonCheckedOutArr.value.length > 0) {
|
|
535
|
+
dialog.showNonCheckedOutDialog = true;
|
|
536
|
+
} else {
|
|
537
|
+
dialog.showNonCheckedOutDialog = false;
|
|
485
538
|
}
|
|
486
539
|
})
|
|
487
540
|
|
|
541
|
+
|
|
542
|
+
const debounceSearchVisitorsByPlateNumbers = debounce(() => {
|
|
543
|
+
if (!visitor.plateNumber) {
|
|
544
|
+
matchingPlateNumberNonCheckedOutArr.value = []
|
|
545
|
+
dialog.showNonCheckedOutDialog = false;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
fetchVisitorListByVehicleNumberRefresh();
|
|
549
|
+
}, 300);
|
|
550
|
+
|
|
551
|
+
watch(() => visitor.plateNumber, (newVal) => {
|
|
552
|
+
debounceSearchVisitorsByPlateNumbers();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
async function handleCheckout(visitorId: string) {
|
|
558
|
+
if (!visitorId) {
|
|
559
|
+
errorMessage.value = "Invalid visitor ID. Cannot proceed with checkout.";
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
loading.checkingOut = true;
|
|
565
|
+
const res = await updateVisitor(visitorId as string, {
|
|
566
|
+
checkOut: new Date().toISOString(),
|
|
567
|
+
});
|
|
568
|
+
if (res) {
|
|
569
|
+
await fetchVisitorListByVehicleNumberRefresh();
|
|
570
|
+
|
|
571
|
+
}
|
|
572
|
+
} catch (error: any) {
|
|
573
|
+
const errorMessage = error?.response?._data?.message;
|
|
574
|
+
console.log("[ERROR]", error);
|
|
575
|
+
errorMessage.value =
|
|
576
|
+
errorMessage || "An error occurred while checking out the vehicle.";
|
|
577
|
+
} finally {
|
|
578
|
+
loading.checkingOut = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
488
585
|
const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
|
|
489
586
|
|
|
490
587
|
watch(companyNameInput, async (val) => {
|
|
@@ -496,19 +593,7 @@ watch(companyNameInput, async (val) => {
|
|
|
496
593
|
})
|
|
497
594
|
|
|
498
595
|
|
|
499
|
-
const debounceSearchVehicleUsers = debounce(() => {
|
|
500
|
-
if (!visitor.plateNumber) {
|
|
501
|
-
vehicleNumberUserItems.value = [];
|
|
502
|
-
dialog.vehicleNumberUsersList = false;
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
fetchVehicleNumberUserRefresh();
|
|
506
|
-
}, 300);
|
|
507
596
|
|
|
508
|
-
watch(() => visitor.plateNumber, (newVal) => {
|
|
509
|
-
if (currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber") return;
|
|
510
|
-
debounceSearchVehicleUsers();
|
|
511
|
-
});
|
|
512
597
|
|
|
513
598
|
|
|
514
599
|
function handleAutofillDataViaVehicleNumber(item: TPeople) {
|
|
@@ -766,12 +851,12 @@ async function submit() {
|
|
|
766
851
|
|
|
767
852
|
|
|
768
853
|
if (prop.type === "contractor") {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
854
|
+
// contractor type logic payload
|
|
855
|
+
payload = {
|
|
856
|
+
...payload,
|
|
857
|
+
members: visitor.members,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
775
860
|
try {
|
|
776
861
|
const res = await createVisitor(payload);
|
|
777
862
|
if (res) {
|
|
@@ -801,9 +886,9 @@ onMounted(() => {
|
|
|
801
886
|
contractorStep.value = 1;
|
|
802
887
|
currentAutofillSource.value = null;
|
|
803
888
|
|
|
804
|
-
if(prop.mode === 'register' && prop.visitorData) {
|
|
805
|
-
console.log('Register mode, prefill visitor data', prop.visitorData?.plateNumber
|
|
806
|
-
|
|
889
|
+
if (prop.mode === 'register' && prop.visitorData) {
|
|
890
|
+
console.log('Register mode, prefill visitor data', prop.visitorData?.plateNumber)
|
|
891
|
+
visitor.plateNumber = prop.visitorData?.plateNumber || ""
|
|
807
892
|
}
|
|
808
893
|
});
|
|
809
894
|
</script>
|
|
@@ -55,10 +55,7 @@ export default function useAccessManagement() {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function getAllAccessCardsCounts(params: {
|
|
59
|
-
site: string;
|
|
60
|
-
userType: string;
|
|
61
|
-
}) {
|
|
58
|
+
function getAllAccessCardsCounts(params: { site: string; userType: string }) {
|
|
62
59
|
return useNuxtApp().$api<Record<string, any>>(
|
|
63
60
|
`/api/access-management/all-access-cards-counts`,
|
|
64
61
|
{
|
|
@@ -71,14 +68,16 @@ export default function useAccessManagement() {
|
|
|
71
68
|
);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
function getUserTypeAccessCards(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
function getUserTypeAccessCards(
|
|
72
|
+
params: {
|
|
73
|
+
page?: number;
|
|
74
|
+
limit?: number;
|
|
75
|
+
search?: string;
|
|
76
|
+
organization?: string;
|
|
77
|
+
userType?: string;
|
|
78
|
+
site?: string;
|
|
79
|
+
} = {}
|
|
80
|
+
) {
|
|
82
81
|
return useNuxtApp().$api<Record<string, any>>(
|
|
83
82
|
`/api/access-management/user-type-access-cards`,
|
|
84
83
|
{
|
|
@@ -200,12 +199,14 @@ export default function useAccessManagement() {
|
|
|
200
199
|
);
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
function getVisitorAccessCards(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
function getVisitorAccessCards(
|
|
203
|
+
params: {
|
|
204
|
+
page?: number;
|
|
205
|
+
limit?: number;
|
|
206
|
+
site?: string;
|
|
207
|
+
search?: string;
|
|
208
|
+
} = {}
|
|
209
|
+
) {
|
|
209
210
|
return useNuxtApp().$api<Record<string, any>>(
|
|
210
211
|
`/api/access-management/visitor-access-cards`,
|
|
211
212
|
{
|
|
@@ -221,6 +222,27 @@ export default function useAccessManagement() {
|
|
|
221
222
|
);
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
function getAvailableContractorCards(params: {
|
|
226
|
+
type: "NFC" | "QRCODE";
|
|
227
|
+
siteId: string;
|
|
228
|
+
unitId: string;
|
|
229
|
+
page?: number;
|
|
230
|
+
limit?: number;
|
|
231
|
+
}) {
|
|
232
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
233
|
+
`/api/access-management/available-card-contractor/${params.type}`,
|
|
234
|
+
{
|
|
235
|
+
method: "GET",
|
|
236
|
+
query: {
|
|
237
|
+
siteId: params.siteId,
|
|
238
|
+
unitId: params.unitId,
|
|
239
|
+
page: params.page ?? 1,
|
|
240
|
+
limit: params.limit ?? 100,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
224
246
|
return {
|
|
225
247
|
getDoorAccessLevels,
|
|
226
248
|
getLiftAccessLevels,
|
|
@@ -238,5 +260,6 @@ export default function useAccessManagement() {
|
|
|
238
260
|
getVisitorAccessCards,
|
|
239
261
|
saveVisitorAccessCardQrTag,
|
|
240
262
|
getAllVisitorAccessCardsQrTags,
|
|
263
|
+
getAvailableContractorCards,
|
|
241
264
|
};
|
|
242
265
|
}
|