@7365admin1/layer-common 1.10.9 → 1.11.0
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 +10 -13
- package/components/AccessCardQrTagging.vue +2 -2
- package/components/BulletinBoardManagement.vue +18 -8
- package/components/Chat/SkeletonLoader.vue +71 -0
- package/components/DashboardMain.vue +176 -0
- package/components/DeliveryCompany.vue +240 -0
- package/components/EntryPassInformation.vue +38 -8
- package/components/FeedbackMain.vue +4 -19
- package/components/FileInputWithList.vue +304 -0
- package/components/IncidentReport/Authorities.vue +189 -151
- package/components/IncidentReport/IncidentInformation.vue +28 -12
- package/components/IncidentReport/IncidentInformationDownload.vue +225 -0
- package/components/IncidentReport/affectedEntities.vue +13 -57
- package/components/Signature.vue +133 -0
- package/components/SiteSettings.vue +285 -0
- package/components/SlideCardGroup.vue +194 -0
- package/components/Tooltip/Info.vue +33 -0
- package/components/VisitorForm.vue +65 -3
- package/components/VisitorManagement.vue +23 -6
- package/composables/useAccessManagement.ts +44 -6
- package/composables/useBulletin.ts +8 -3
- package/composables/useBulletinBoardPermission.ts +48 -0
- package/composables/useCleaningPermission.ts +2 -0
- package/composables/useComment.ts +147 -0
- package/composables/useCommonPermission.ts +29 -1
- package/composables/useFeedback.ts +79 -29
- package/composables/useFile.ts +6 -0
- package/composables/usePDFDownload.ts +1 -1
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useVisitor.ts +6 -5
- package/composables/useWorkOrder.ts +61 -26
- package/constants/app.ts +12 -0
- package/nuxt.config.ts +2 -0
- package/package.json +3 -1
- package/plugins/vue-draggable-next.client.ts +5 -0
- package/public/default-image.svg +4 -0
- package/public/placeholder-image.svg +6 -0
- package/types/comment.d.ts +38 -0
- package/types/dashboard.d.ts +12 -0
- package/types/feedback.d.ts +56 -20
- package/types/site.d.ts +2 -1
- package/types/work-order.d.ts +54 -18
- package/utils/data.ts +31 -0
|
@@ -82,13 +82,6 @@
|
|
|
82
82
|
placeholder="Select cards..."
|
|
83
83
|
return-object
|
|
84
84
|
>
|
|
85
|
-
<template #item="{ props: itemProps, item }">
|
|
86
|
-
<v-list-item v-bind="itemProps">
|
|
87
|
-
<template #subtitle>
|
|
88
|
-
<span class="text-caption text-grey">{{ item.raw.cardNo }}</span>
|
|
89
|
-
</template>
|
|
90
|
-
</v-list-item>
|
|
91
|
-
</template>
|
|
92
85
|
</v-select>
|
|
93
86
|
</div>
|
|
94
87
|
|
|
@@ -189,6 +182,8 @@
|
|
|
189
182
|
</v-card>
|
|
190
183
|
</v-container>
|
|
191
184
|
</v-dialog>
|
|
185
|
+
|
|
186
|
+
<Snackbar v-model="snackbar" :text="snackbarText" :color="snackbarColor" />
|
|
192
187
|
</div>
|
|
193
188
|
</template>
|
|
194
189
|
|
|
@@ -226,7 +221,7 @@ const props = defineProps({
|
|
|
226
221
|
|
|
227
222
|
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards"]);
|
|
228
223
|
|
|
229
|
-
const { getAvailableContractorCards } = useAccessManagement();
|
|
224
|
+
const { getAvailableContractorCards, generateQrVms } = useAccessManagement();
|
|
230
225
|
|
|
231
226
|
const nfcEnabled = computed(() => props.settings?.data?.settings?.nfcPass ?? false);
|
|
232
227
|
const printer = computed(() => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null });
|
|
@@ -250,6 +245,17 @@ const selectedCards = computed({
|
|
|
250
245
|
},
|
|
251
246
|
});
|
|
252
247
|
|
|
248
|
+
// ─── Snackbar ─────────────────────────────────────────────────────
|
|
249
|
+
const snackbar = ref(false);
|
|
250
|
+
const snackbarText = ref("");
|
|
251
|
+
const snackbarColor = ref("error");
|
|
252
|
+
|
|
253
|
+
function showSnackbar(text: string, color = "error") {
|
|
254
|
+
snackbarText.value = text;
|
|
255
|
+
snackbarColor.value = color;
|
|
256
|
+
snackbar.value = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
253
259
|
// ─── Card fetching ────────────────────────────────────────────────
|
|
254
260
|
const cardItems = ref<any[]>([]);
|
|
255
261
|
const cardsLoading = ref(false);
|
|
@@ -266,6 +272,27 @@ async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
|
266
272
|
});
|
|
267
273
|
cardItems.value = res?.data?.[0]?.items ?? [];
|
|
268
274
|
availableCount.value = (res?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
275
|
+
|
|
276
|
+
if (type === "QRCODE" && props.settings?.data?._id && availableCount.value !== null && availableCount.value < 50) {
|
|
277
|
+
await generateQrVms({
|
|
278
|
+
site: props.settings.data.site,
|
|
279
|
+
unitId: props.unitId,
|
|
280
|
+
quantity: 50,
|
|
281
|
+
});
|
|
282
|
+
// Re-fetch to get the updated count after generation
|
|
283
|
+
const refreshed = await getAvailableContractorCards({
|
|
284
|
+
type,
|
|
285
|
+
siteId: props.siteId,
|
|
286
|
+
unitId: props.unitId,
|
|
287
|
+
});
|
|
288
|
+
cardItems.value = refreshed?.data?.[0]?.items ?? [];
|
|
289
|
+
availableCount.value = (refreshed?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
290
|
+
}
|
|
291
|
+
} catch (err: any) {
|
|
292
|
+
const msg = err?.data?.message ?? err?.message ?? String(err);
|
|
293
|
+
showSnackbar(`Entry pass service error: ${msg}`);
|
|
294
|
+
availableCount.value = null;
|
|
295
|
+
cardItems.value = [];
|
|
269
296
|
} finally {
|
|
270
297
|
cardsLoading.value = false;
|
|
271
298
|
}
|
|
@@ -387,6 +414,9 @@ const scanFrame = async () => {
|
|
|
387
414
|
addCard(card);
|
|
388
415
|
closeCameraDialog();
|
|
389
416
|
return;
|
|
417
|
+
} else if (barcode.rawValue) {
|
|
418
|
+
showSnackbar(`Card "${barcode.rawValue}" not found in available cards.`, "warning");
|
|
419
|
+
return;
|
|
390
420
|
}
|
|
391
421
|
}
|
|
392
422
|
} catch {
|
|
@@ -109,29 +109,14 @@
|
|
|
109
109
|
>
|
|
110
110
|
<template #item.createdBy="{ item, index }">
|
|
111
111
|
<v-avatar
|
|
112
|
-
v-if="
|
|
113
|
-
item?.createdBy &&
|
|
114
|
-
Boolean(item?.createdBy) &&
|
|
115
|
-
typeof item?.createdBy === 'object' &&
|
|
116
|
-
Object.keys(item?.createdBy).length > 0
|
|
117
|
-
"
|
|
112
|
+
v-if="item?.createdBy?.name"
|
|
118
113
|
size="small"
|
|
119
114
|
:color="materialColors[index % materialColors.length]"
|
|
120
115
|
class="text-subtitle-2 mr-1 mr-md-4"
|
|
121
116
|
>
|
|
122
|
-
{{
|
|
123
|
-
getInitial(
|
|
124
|
-
`${item?.createdBy?.givenName ?? ""} ${
|
|
125
|
-
item?.createdBy?.surname ?? ""
|
|
126
|
-
}`
|
|
127
|
-
) || ""
|
|
128
|
-
}}
|
|
117
|
+
{{ getInitial(item?.createdBy?.name) ?? "" }}
|
|
129
118
|
</v-avatar>
|
|
130
|
-
{{
|
|
131
|
-
`${item?.createdBy?.givenName ?? ""} ${
|
|
132
|
-
item?.createdBy?.surname ?? ""
|
|
133
|
-
}`
|
|
134
|
-
}}
|
|
119
|
+
{{ item?.createdBy?.name ?? "N/A" }}
|
|
135
120
|
</template>
|
|
136
121
|
<template #item.subject="{ item }">
|
|
137
122
|
{{ item.subject || "N/A" }}
|
|
@@ -685,7 +670,7 @@ const {
|
|
|
685
670
|
} = useUtils();
|
|
686
671
|
|
|
687
672
|
const headers: Array<Record<string, any>> = [
|
|
688
|
-
{ title: "Name", value: "
|
|
673
|
+
{ title: "Name", value: "createdBy", align: "start" },
|
|
689
674
|
{ title: "Subject", value: "subject", align: "start" },
|
|
690
675
|
{ title: "Date", value: "createdAt", align: "start" },
|
|
691
676
|
{ title: "App", value: "app", align: "start" },
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-file-input
|
|
4
|
+
v-model="files"
|
|
5
|
+
:label="label"
|
|
6
|
+
accept="image/*"
|
|
7
|
+
:prepend-icon="prependIcon"
|
|
8
|
+
hide-details
|
|
9
|
+
show-size
|
|
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
|
+
<v-row no-gutters v-if="hasLabel">
|
|
27
|
+
<v-col cols="12" class="mt-1">
|
|
28
|
+
<v-chip-group column>
|
|
29
|
+
<template v-for="(file, index) in files" :key="file.name">
|
|
30
|
+
<v-chip
|
|
31
|
+
closable
|
|
32
|
+
class="text-wrap text-caption custom-chip"
|
|
33
|
+
@click:close="removeFile(index)"
|
|
34
|
+
>
|
|
35
|
+
<span class="chip-text">{{ file.name }}</span>
|
|
36
|
+
</v-chip>
|
|
37
|
+
</template>
|
|
38
|
+
</v-chip-group>
|
|
39
|
+
</v-col>
|
|
40
|
+
</v-row>
|
|
41
|
+
|
|
42
|
+
<!-- Camera Dialog -->
|
|
43
|
+
<v-dialog
|
|
44
|
+
v-model="showCameraDialog"
|
|
45
|
+
transition="dialog-bottom-transition"
|
|
46
|
+
width="800"
|
|
47
|
+
max-width="800"
|
|
48
|
+
persistent
|
|
49
|
+
@after-enter="startCamera"
|
|
50
|
+
>
|
|
51
|
+
<v-container
|
|
52
|
+
class="d-flex justify-center"
|
|
53
|
+
max-height="90vh"
|
|
54
|
+
width="800"
|
|
55
|
+
max-width="800"
|
|
56
|
+
>
|
|
57
|
+
<v-card elevation="2" class="d-flex flex-column align-center pa-2">
|
|
58
|
+
<v-toolbar>
|
|
59
|
+
<v-card-title class="text-h5">Take a Picture</v-card-title>
|
|
60
|
+
<v-spacer></v-spacer>
|
|
61
|
+
<v-btn
|
|
62
|
+
color="grey-darken-1"
|
|
63
|
+
icon="mdi-close"
|
|
64
|
+
@click="closeCameraDialog"
|
|
65
|
+
></v-btn>
|
|
66
|
+
</v-toolbar>
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
id="reader"
|
|
70
|
+
class="d-flex justify-center align-center"
|
|
71
|
+
style="
|
|
72
|
+
position: relative;
|
|
73
|
+
width: 500px;
|
|
74
|
+
min-width: 400px;
|
|
75
|
+
height: 400px;
|
|
76
|
+
"
|
|
77
|
+
>
|
|
78
|
+
<video
|
|
79
|
+
ref="video"
|
|
80
|
+
style="flex: 1; height: 400px; min-width: 300px"
|
|
81
|
+
class="video-shutter"
|
|
82
|
+
autoplay
|
|
83
|
+
></video>
|
|
84
|
+
<canvas
|
|
85
|
+
ref="canvas"
|
|
86
|
+
style="flex: 1; height: 400px; min-width: 300px; display: none"
|
|
87
|
+
></canvas>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<v-row align="center" justify="center">
|
|
91
|
+
<v-col cols="6">
|
|
92
|
+
<v-btn color="primary" icon class="mt-4" @click="switchCamera">
|
|
93
|
+
<v-icon>mdi-camera-switch</v-icon>
|
|
94
|
+
</v-btn>
|
|
95
|
+
</v-col>
|
|
96
|
+
<v-col cols="6">
|
|
97
|
+
<v-btn
|
|
98
|
+
color="secondary"
|
|
99
|
+
icon
|
|
100
|
+
class="mt-4"
|
|
101
|
+
@click="captureImageFromCamera"
|
|
102
|
+
>
|
|
103
|
+
<v-icon large>mdi-camera-outline</v-icon>
|
|
104
|
+
</v-btn>
|
|
105
|
+
</v-col>
|
|
106
|
+
</v-row>
|
|
107
|
+
</v-card>
|
|
108
|
+
</v-container>
|
|
109
|
+
</v-dialog>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<script setup lang="ts">
|
|
114
|
+
interface FileWithPreview {
|
|
115
|
+
name: string;
|
|
116
|
+
data: File;
|
|
117
|
+
progress: number;
|
|
118
|
+
url: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const props = defineProps({
|
|
122
|
+
label: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: "Select File",
|
|
125
|
+
},
|
|
126
|
+
prependIcon: {
|
|
127
|
+
type: String,
|
|
128
|
+
default: "mdi-paperclip",
|
|
129
|
+
},
|
|
130
|
+
required: {
|
|
131
|
+
type: Boolean,
|
|
132
|
+
default: true,
|
|
133
|
+
},
|
|
134
|
+
initFiles: {
|
|
135
|
+
type: Array,
|
|
136
|
+
},
|
|
137
|
+
hasLabel: {
|
|
138
|
+
type: Boolean,
|
|
139
|
+
default: true,
|
|
140
|
+
},
|
|
141
|
+
hasHideInput: {
|
|
142
|
+
type: Boolean,
|
|
143
|
+
default: false,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const emit = defineEmits<{
|
|
148
|
+
(event: "onFileAttach", payload: Array<{ data: File }>): void;
|
|
149
|
+
(event: "update:files", files: FileWithPreview[]): void;
|
|
150
|
+
(event: "onFileRemoved", payload: { index: number; file: File }): void;
|
|
151
|
+
(event: "onClear"): void;
|
|
152
|
+
}>();
|
|
153
|
+
|
|
154
|
+
const { showUploadedFiles } = useUploadFiles();
|
|
155
|
+
|
|
156
|
+
const files = ref<File[]>([]);
|
|
157
|
+
const attachedFiles = ref<FileWithPreview[]>([]);
|
|
158
|
+
const showCameraDialog = ref(false);
|
|
159
|
+
const video = ref<HTMLVideoElement | null>(null);
|
|
160
|
+
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
161
|
+
const cameraFacingMode = ref<"environment" | "user">("environment");
|
|
162
|
+
|
|
163
|
+
const message = ref("");
|
|
164
|
+
const messageColor = ref("");
|
|
165
|
+
const messageSnackbar = ref(false);
|
|
166
|
+
|
|
167
|
+
function showMessage(msg: string, color: string) {
|
|
168
|
+
message.value = msg;
|
|
169
|
+
messageColor.value = color;
|
|
170
|
+
messageSnackbar.value = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
watchEffect(() => {
|
|
174
|
+
if (Array.isArray(props.initFiles) && props.initFiles.length > 0) {
|
|
175
|
+
files.value = props.initFiles.filter((file) => file && file.name); // Ensure valid files
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const handleFileSelect = async () => {
|
|
179
|
+
if (files.value && files.value.length > 0) {
|
|
180
|
+
const newFiles = files.value.map((file: File) => ({
|
|
181
|
+
name: file.name,
|
|
182
|
+
data: file,
|
|
183
|
+
progress: 0,
|
|
184
|
+
url: URL.createObjectURL(file),
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
// attachedFiles.value = [...newFiles];
|
|
188
|
+
showUploadedFiles(newFiles);
|
|
189
|
+
|
|
190
|
+
emit("update:files", newFiles);
|
|
191
|
+
} else {
|
|
192
|
+
files.value = [...(props.initFiles as typeof files.value)];
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleClear = () => {
|
|
197
|
+
files.value = [];
|
|
198
|
+
emit("onClear");
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const openCameraDialog = () => {
|
|
202
|
+
showCameraDialog.value = true;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const closeCameraDialog = () => {
|
|
206
|
+
showCameraDialog.value = false;
|
|
207
|
+
stopCamera();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const startCamera = async () => {
|
|
211
|
+
try {
|
|
212
|
+
const constraints = {
|
|
213
|
+
video: {
|
|
214
|
+
facingMode: cameraFacingMode.value,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
219
|
+
if (video.value) {
|
|
220
|
+
video.value.srcObject = stream;
|
|
221
|
+
video.value.play();
|
|
222
|
+
}
|
|
223
|
+
} catch (error: any) {
|
|
224
|
+
showMessage(`Error accessing camera: ${error.message}`, "error");
|
|
225
|
+
closeCameraDialog();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const stopCamera = () => {
|
|
230
|
+
if (video.value) {
|
|
231
|
+
const stream = video.value.srcObject as MediaStream;
|
|
232
|
+
if (stream) {
|
|
233
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const switchCamera = async () => {
|
|
239
|
+
await stopCamera();
|
|
240
|
+
cameraFacingMode.value =
|
|
241
|
+
cameraFacingMode.value === "environment" ? "user" : "environment";
|
|
242
|
+
showMessage(
|
|
243
|
+
`Switched to ${
|
|
244
|
+
cameraFacingMode.value === "environment" ? "Back Camera" : "Front Camera"
|
|
245
|
+
}`,
|
|
246
|
+
"error"
|
|
247
|
+
);
|
|
248
|
+
startCamera();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const captureImageFromCamera = () => {
|
|
252
|
+
if (!video.value || !canvas.value) return;
|
|
253
|
+
|
|
254
|
+
const context = canvas.value.getContext("2d");
|
|
255
|
+
if (!context) return;
|
|
256
|
+
|
|
257
|
+
// Set canvas dimensions to match video
|
|
258
|
+
canvas.value.width = video.value.videoWidth;
|
|
259
|
+
canvas.value.height = video.value.videoHeight;
|
|
260
|
+
|
|
261
|
+
// Capture the frame
|
|
262
|
+
context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
|
|
263
|
+
|
|
264
|
+
// Convert to file
|
|
265
|
+
canvas.value.toBlob((blob) => {
|
|
266
|
+
if (!blob) return;
|
|
267
|
+
|
|
268
|
+
const file = new File([blob], `camera-capture-${Date.now()}.png`, {
|
|
269
|
+
type: "image/png",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
files.value = [file];
|
|
273
|
+
handleFileSelect();
|
|
274
|
+
closeCameraDialog();
|
|
275
|
+
}, "image/png");
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const removeFile = (index) => {
|
|
279
|
+
const removedFile = files.value[index];
|
|
280
|
+
files.value = files.value.filter((_, i) => i !== index);
|
|
281
|
+
emit("onFileRemoved", { index, file: removedFile }); // Emit when a file is removed
|
|
282
|
+
// emit("onFilesUpdated", files.value); // Emit updated files list
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Cleanup
|
|
286
|
+
onUnmounted(() => {
|
|
287
|
+
stopCamera();
|
|
288
|
+
});
|
|
289
|
+
</script>
|
|
290
|
+
|
|
291
|
+
<style scoped>
|
|
292
|
+
.custom-chip {
|
|
293
|
+
max-width: 100%;
|
|
294
|
+
height: auto !important;
|
|
295
|
+
white-space: normal;
|
|
296
|
+
padding: 3px 20px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.chip-text {
|
|
300
|
+
word-break: break-word;
|
|
301
|
+
white-space: normal;
|
|
302
|
+
line-height: 1.2;
|
|
303
|
+
}
|
|
304
|
+
</style>
|