@7365admin1/layer-common 1.10.0 → 1.10.1
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/AcceptDialog.vue +44 -0
- package/components/AccessCardAddForm.vue +101 -13
- package/components/AccessManagement.vue +130 -47
- package/components/AddSupplyForm.vue +165 -0
- package/components/AreaChecklistHistoryLogs.vue +235 -0
- package/components/AreaChecklistHistoryMain.vue +176 -0
- package/components/AreaFormDialog.vue +266 -0
- package/components/AreaMain.vue +841 -0
- package/components/AttendanceCheckInOutDialog.vue +416 -0
- package/components/AttendanceDetailsDialog.vue +184 -0
- package/components/AttendanceMain.vue +155 -0
- package/components/AttendanceMapSearchDialog.vue +393 -0
- package/components/AttendanceSettingsDialog.vue +398 -0
- package/components/BuildingManagement/buildings.vue +5 -5
- package/components/BuildingManagement/units.vue +5 -5
- package/components/ChecklistItemRow.vue +54 -0
- package/components/CheckoutItemMain.vue +705 -0
- package/components/CleaningScheduleMain.vue +271 -0
- package/components/DocumentManagement.vue +4 -0
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- package/components/ManageChecklistMain.vue +384 -0
- package/components/MemberMain.vue +48 -20
- package/components/MyAttendanceMain.vue +224 -0
- package/components/OnlineFormsConfiguration.vue +9 -2
- package/components/PhotoUpload.vue +410 -0
- package/components/ScheduleAreaMain.vue +313 -0
- package/components/ScheduleTaskAreaFormDialog.vue +144 -0
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
- package/components/ScheduleTaskForm.vue +471 -0
- package/components/ScheduleTaskMain.vue +345 -0
- package/components/ScheduleTastTicketMain.vue +182 -0
- package/components/StockCard.vue +191 -0
- package/components/SupplyManagementMain.vue +557 -0
- package/components/TableHygiene.vue +617 -0
- package/components/UnitMain.vue +451 -0
- package/components/VisitorManagement.vue +28 -15
- package/composables/useAccessManagement.ts +90 -0
- package/composables/useAreaPermission.ts +51 -0
- package/composables/useAreas.ts +99 -0
- package/composables/useAttendance.ts +89 -0
- package/composables/useAttendancePermission.ts +68 -0
- package/composables/useBuilding.ts +2 -2
- package/composables/useBuildingUnit.ts +2 -2
- package/composables/useCard.ts +2 -0
- package/composables/useCheckout.ts +61 -0
- package/composables/useCheckoutPermission.ts +80 -0
- package/composables/useCleaningPermission.ts +229 -0
- package/composables/useCleaningSchedulePermission.ts +58 -0
- package/composables/useCleaningSchedules.ts +233 -0
- package/composables/useCountry.ts +8 -0
- package/composables/useDashboardData.ts +2 -2
- package/composables/useFeedback.ts +1 -1
- package/composables/useLocation.ts +78 -0
- package/composables/useOnlineForm.ts +16 -9
- package/composables/usePeople.ts +87 -72
- package/composables/useQR.ts +29 -0
- package/composables/useScheduleTask.ts +89 -0
- package/composables/useScheduleTaskArea.ts +85 -0
- package/composables/useScheduleTaskPermission.ts +68 -0
- package/composables/useSiteEntryPassSettings.ts +4 -15
- package/composables/useStock.ts +45 -0
- package/composables/useSupply.ts +63 -0
- package/composables/useSupplyPermission.ts +92 -0
- package/composables/useUnitPermission.ts +51 -0
- package/composables/useUnits.ts +82 -0
- package/composables/useWebUsb.ts +389 -0
- package/composables/useWorkOrder.ts +1 -1
- package/nuxt.config.ts +3 -0
- package/package.json +4 -1
- package/types/area.d.ts +22 -0
- package/types/attendance.d.ts +38 -0
- package/types/checkout-item.d.ts +27 -0
- package/types/cleaner-schedule.d.ts +54 -0
- package/types/location.d.ts +42 -0
- package/types/schedule-task.d.ts +18 -0
- package/types/stock.d.ts +16 -0
- package/types/supply.d.ts +11 -0
- package/utils/acm-crypto.ts +30 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog v-model="showDialog" max-width="450" persistent>
|
|
3
|
+
<v-card>
|
|
4
|
+
<v-toolbar>
|
|
5
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
6
|
+
<span class="font-weight-bold text-h6">{{
|
|
7
|
+
action === "checkIn" ? "Check In" : "Check Out"
|
|
8
|
+
}}</span>
|
|
9
|
+
</v-row>
|
|
10
|
+
</v-toolbar>
|
|
11
|
+
|
|
12
|
+
<v-card-text class="pa-4">
|
|
13
|
+
<v-form ref="formRef" v-model="valid">
|
|
14
|
+
<v-row no-gutters class="pa-0">
|
|
15
|
+
<v-col cols="12" class="pa-0">
|
|
16
|
+
<v-card
|
|
17
|
+
variant="outlined"
|
|
18
|
+
class="mb-2"
|
|
19
|
+
style="height: 400px; position: relative; overflow: hidden"
|
|
20
|
+
>
|
|
21
|
+
<div
|
|
22
|
+
v-if="loadingCamera"
|
|
23
|
+
class="d-flex align-center justify-center"
|
|
24
|
+
style="height: 100%"
|
|
25
|
+
>
|
|
26
|
+
<v-progress-circular
|
|
27
|
+
indeterminate
|
|
28
|
+
color="primary"
|
|
29
|
+
size="64"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<video
|
|
34
|
+
v-if="showCamera && !capturedImage"
|
|
35
|
+
ref="videoElement"
|
|
36
|
+
autoplay
|
|
37
|
+
playsinline
|
|
38
|
+
muted
|
|
39
|
+
style="
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
object-fit: cover;
|
|
43
|
+
display: block;
|
|
44
|
+
background: #000;
|
|
45
|
+
"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<img
|
|
49
|
+
v-else-if="capturedImage"
|
|
50
|
+
:src="capturedImage"
|
|
51
|
+
style="
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
object-fit: cover;
|
|
55
|
+
display: block;
|
|
56
|
+
"
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<div
|
|
60
|
+
v-else-if="!loadingCamera"
|
|
61
|
+
class="d-flex align-center justify-center flex-column"
|
|
62
|
+
style="height: 100%; background: #f5f5f5"
|
|
63
|
+
>
|
|
64
|
+
<v-icon size="64" color="grey" class="mb-2"
|
|
65
|
+
>mdi-camera-off</v-icon
|
|
66
|
+
>
|
|
67
|
+
<span class="text-caption text-grey"
|
|
68
|
+
>Initializing camera...</span
|
|
69
|
+
>
|
|
70
|
+
</div>
|
|
71
|
+
</v-card>
|
|
72
|
+
</v-col>
|
|
73
|
+
|
|
74
|
+
<v-col v-if="error" cols="12" class="pa-0 mt-2">
|
|
75
|
+
<v-alert
|
|
76
|
+
type="warning"
|
|
77
|
+
variant="tonal"
|
|
78
|
+
density="compact"
|
|
79
|
+
closable
|
|
80
|
+
@click:close="error = ''"
|
|
81
|
+
>
|
|
82
|
+
{{ error }}
|
|
83
|
+
</v-alert>
|
|
84
|
+
</v-col>
|
|
85
|
+
</v-row>
|
|
86
|
+
</v-form>
|
|
87
|
+
</v-card-text>
|
|
88
|
+
|
|
89
|
+
<v-toolbar class="pa-0" density="compact">
|
|
90
|
+
<v-row no-gutters>
|
|
91
|
+
<v-col v-if="!capturedImage" cols="6" class="pa-0">
|
|
92
|
+
<v-btn
|
|
93
|
+
block
|
|
94
|
+
variant="text"
|
|
95
|
+
class="text-none"
|
|
96
|
+
size="large"
|
|
97
|
+
@click="close"
|
|
98
|
+
height="56"
|
|
99
|
+
>
|
|
100
|
+
Cancel
|
|
101
|
+
</v-btn>
|
|
102
|
+
</v-col>
|
|
103
|
+
|
|
104
|
+
<v-col v-if="!capturedImage" cols="6" class="pa-0">
|
|
105
|
+
<v-btn
|
|
106
|
+
block
|
|
107
|
+
variant="flat"
|
|
108
|
+
color="black"
|
|
109
|
+
class="text-none font-weight-bold rounded-0"
|
|
110
|
+
height="56"
|
|
111
|
+
size="large"
|
|
112
|
+
@click="capturePhoto"
|
|
113
|
+
:disabled="!showCamera"
|
|
114
|
+
>
|
|
115
|
+
<v-icon class="mr-2">mdi-camera</v-icon>
|
|
116
|
+
Capture Photo
|
|
117
|
+
</v-btn>
|
|
118
|
+
</v-col>
|
|
119
|
+
|
|
120
|
+
<template v-else>
|
|
121
|
+
<v-col cols="6" class="pa-0">
|
|
122
|
+
<v-btn
|
|
123
|
+
block
|
|
124
|
+
variant="text"
|
|
125
|
+
class="text-none"
|
|
126
|
+
size="large"
|
|
127
|
+
@click="retakePhoto"
|
|
128
|
+
height="56"
|
|
129
|
+
:disabled="submitting"
|
|
130
|
+
>
|
|
131
|
+
<v-icon class="mr-2">mdi-camera-retake</v-icon>
|
|
132
|
+
Retake
|
|
133
|
+
</v-btn>
|
|
134
|
+
</v-col>
|
|
135
|
+
|
|
136
|
+
<v-col cols="6" class="pa-0">
|
|
137
|
+
<v-btn
|
|
138
|
+
block
|
|
139
|
+
variant="flat"
|
|
140
|
+
color="black"
|
|
141
|
+
class="text-none font-weight-bold rounded-0"
|
|
142
|
+
height="56"
|
|
143
|
+
size="large"
|
|
144
|
+
:loading="submitting"
|
|
145
|
+
@click="submit"
|
|
146
|
+
>
|
|
147
|
+
<v-icon class="mr-2">mdi-check</v-icon>
|
|
148
|
+
Submit
|
|
149
|
+
</v-btn>
|
|
150
|
+
</v-col>
|
|
151
|
+
</template>
|
|
152
|
+
</v-row>
|
|
153
|
+
</v-toolbar>
|
|
154
|
+
</v-card>
|
|
155
|
+
</v-dialog>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<script setup lang="ts">
|
|
159
|
+
const showDialog = defineModel({ type: Boolean, default: false });
|
|
160
|
+
|
|
161
|
+
const emit = defineEmits(["saved", "close"]);
|
|
162
|
+
|
|
163
|
+
const props = defineProps({
|
|
164
|
+
action: { type: String, default: "checkIn" }, // "checkIn" or "checkOut"
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const formRef = ref<any>(null);
|
|
168
|
+
const valid = ref(false);
|
|
169
|
+
const submitting = ref(false);
|
|
170
|
+
const error = ref("");
|
|
171
|
+
const loadingCamera = ref(false);
|
|
172
|
+
|
|
173
|
+
const imageFile = ref<File | null>(null);
|
|
174
|
+
|
|
175
|
+
const videoElement = ref<HTMLVideoElement | null>(null);
|
|
176
|
+
const showCamera = ref(false);
|
|
177
|
+
const capturedImage = ref<string | null>(null);
|
|
178
|
+
const mediaStream = ref<MediaStream | null>(null);
|
|
179
|
+
|
|
180
|
+
const { addFile } = useFile();
|
|
181
|
+
|
|
182
|
+
const openCamera = async () => {
|
|
183
|
+
loadingCamera.value = true;
|
|
184
|
+
error.value = "";
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (mediaStream.value) {
|
|
188
|
+
mediaStream.value.getTracks().forEach((track) => track.stop());
|
|
189
|
+
mediaStream.value = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
193
|
+
throw new Error("Camera not supported in this browser");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
197
|
+
video: {
|
|
198
|
+
facingMode: "user",
|
|
199
|
+
width: { ideal: 1280 },
|
|
200
|
+
height: { ideal: 720 },
|
|
201
|
+
},
|
|
202
|
+
audio: false,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
mediaStream.value = stream;
|
|
206
|
+
capturedImage.value = null;
|
|
207
|
+
imageFile.value = null;
|
|
208
|
+
showCamera.value = true;
|
|
209
|
+
|
|
210
|
+
await nextTick();
|
|
211
|
+
await nextTick();
|
|
212
|
+
|
|
213
|
+
if (videoElement.value) {
|
|
214
|
+
videoElement.value.srcObject = stream;
|
|
215
|
+
|
|
216
|
+
videoElement.value.setAttribute("autoplay", "");
|
|
217
|
+
videoElement.value.setAttribute("playsinline", "");
|
|
218
|
+
videoElement.value.setAttribute("muted", "");
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await videoElement.value.play();
|
|
222
|
+
console.log("Camera preview started successfully");
|
|
223
|
+
} catch (playErr) {
|
|
224
|
+
console.error("Error playing video:", playErr);
|
|
225
|
+
throw playErr;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (err: any) {
|
|
229
|
+
console.error("Camera error:", err);
|
|
230
|
+
showCamera.value = false;
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
err.name === "NotAllowedError" ||
|
|
234
|
+
err.name === "PermissionDeniedError"
|
|
235
|
+
) {
|
|
236
|
+
error.value =
|
|
237
|
+
"Camera access denied. Please allow camera access in your browser settings.";
|
|
238
|
+
} else if (
|
|
239
|
+
err.name === "NotFoundError" ||
|
|
240
|
+
err.name === "DevicesNotFoundError"
|
|
241
|
+
) {
|
|
242
|
+
error.value = "No camera found on this device.";
|
|
243
|
+
} else if (
|
|
244
|
+
err.name === "NotReadableError" ||
|
|
245
|
+
err.name === "TrackStartError"
|
|
246
|
+
) {
|
|
247
|
+
error.value = "Camera is already in use by another application.";
|
|
248
|
+
} else if (err.message === "Camera not supported in this browser") {
|
|
249
|
+
error.value = err.message;
|
|
250
|
+
} else {
|
|
251
|
+
error.value =
|
|
252
|
+
"Unable to access camera. Please check your browser permissions.";
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
loadingCamera.value = false;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const capturePhoto = async () => {
|
|
260
|
+
if (!videoElement.value) return;
|
|
261
|
+
|
|
262
|
+
const canvas = document.createElement("canvas");
|
|
263
|
+
canvas.width = videoElement.value.videoWidth;
|
|
264
|
+
canvas.height = videoElement.value.videoHeight;
|
|
265
|
+
const ctx = canvas.getContext("2d");
|
|
266
|
+
|
|
267
|
+
if (!ctx) return;
|
|
268
|
+
|
|
269
|
+
ctx.drawImage(videoElement.value, 0, 0);
|
|
270
|
+
capturedImage.value = canvas.toDataURL("image/jpeg");
|
|
271
|
+
|
|
272
|
+
const blob = await new Promise<Blob | null>((resolve) => {
|
|
273
|
+
canvas.toBlob((blob) => resolve(blob), "image/jpeg");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (!blob) return;
|
|
277
|
+
|
|
278
|
+
const file = new File([blob], "captured-photo.jpg", {
|
|
279
|
+
type: "image/jpeg",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
stopCamera();
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
submitting.value = true;
|
|
286
|
+
const uploadRes = await addFile(file);
|
|
287
|
+
imageFile.value = uploadRes?.id;
|
|
288
|
+
error.value = "";
|
|
289
|
+
} catch (err: any) {
|
|
290
|
+
console.error("Error uploading file:", err);
|
|
291
|
+
error.value =
|
|
292
|
+
err.statusCode === 401
|
|
293
|
+
? "Authentication failed. Please refresh the page and try again."
|
|
294
|
+
: "Failed to upload image. Please try again.";
|
|
295
|
+
} finally {
|
|
296
|
+
submitting.value = false;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const retakePhoto = () => {
|
|
301
|
+
capturedImage.value = null;
|
|
302
|
+
imageFile.value = null;
|
|
303
|
+
error.value = "";
|
|
304
|
+
openCamera();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const stopCamera = () => {
|
|
308
|
+
if (mediaStream.value) {
|
|
309
|
+
mediaStream.value.getTracks().forEach((track) => track.stop());
|
|
310
|
+
mediaStream.value = null;
|
|
311
|
+
}
|
|
312
|
+
showCamera.value = false;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const close = () => {
|
|
316
|
+
stopCamera();
|
|
317
|
+
resetForm();
|
|
318
|
+
showDialog.value = false;
|
|
319
|
+
emit("close");
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const resetForm = () => {
|
|
323
|
+
imageFile.value = null;
|
|
324
|
+
capturedImage.value = null;
|
|
325
|
+
showCamera.value = false;
|
|
326
|
+
error.value = "";
|
|
327
|
+
formRef.value?.resetValidation();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const getCurrentLocation = (): Promise<{
|
|
331
|
+
latitude: number;
|
|
332
|
+
longitude: number;
|
|
333
|
+
}> => {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
if (!navigator.geolocation) {
|
|
336
|
+
reject(new Error("Geolocation is not supported by your browser"));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
navigator.geolocation.getCurrentPosition(
|
|
341
|
+
(position) => {
|
|
342
|
+
resolve({
|
|
343
|
+
latitude: position.coords.latitude,
|
|
344
|
+
longitude: position.coords.longitude,
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
(err) => {
|
|
348
|
+
reject(err);
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
enableHighAccuracy: true,
|
|
352
|
+
timeout: 10000,
|
|
353
|
+
maximumAge: 0,
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const submit = async () => {
|
|
360
|
+
if (!capturedImage.value) {
|
|
361
|
+
error.value = "Please capture a photo before submitting";
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
submitting.value = true;
|
|
366
|
+
error.value = "";
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const location = await getCurrentLocation();
|
|
370
|
+
const imageId =
|
|
371
|
+
typeof imageFile.value === "string" ? imageFile.value : undefined;
|
|
372
|
+
|
|
373
|
+
const isoTimestamp = new Date().toISOString();
|
|
374
|
+
|
|
375
|
+
const payload: any = {
|
|
376
|
+
[props.action]: {
|
|
377
|
+
timestamp: isoTimestamp,
|
|
378
|
+
location: {
|
|
379
|
+
latitude: location.latitude,
|
|
380
|
+
longitude: location.longitude,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (imageId) {
|
|
386
|
+
payload[props.action].img = imageId;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
emit("saved", payload);
|
|
390
|
+
|
|
391
|
+
close();
|
|
392
|
+
} catch (err: any) {
|
|
393
|
+
console.error("Error submitting attendance:", err);
|
|
394
|
+
|
|
395
|
+
if (err.message?.includes("Geolocation") || err.code) {
|
|
396
|
+
error.value =
|
|
397
|
+
"Unable to get your location. Please enable location services.";
|
|
398
|
+
} else {
|
|
399
|
+
error.value = "Failed to submit attendance. Please try again.";
|
|
400
|
+
}
|
|
401
|
+
} finally {
|
|
402
|
+
submitting.value = false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
watch(showDialog, async (newVal) => {
|
|
407
|
+
if (newVal) {
|
|
408
|
+
await nextTick();
|
|
409
|
+
setTimeout(() => {
|
|
410
|
+
openCamera();
|
|
411
|
+
}, 100);
|
|
412
|
+
} else {
|
|
413
|
+
stopCamera();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
</script>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog
|
|
3
|
+
v-model="showDialog"
|
|
4
|
+
:max-width="attendance?.checkOut?.timestamp ? 900 : 500"
|
|
5
|
+
persistent
|
|
6
|
+
>
|
|
7
|
+
<v-card>
|
|
8
|
+
<v-toolbar>
|
|
9
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
10
|
+
<span class="font-weight-bold text-h6 text-capitalize"
|
|
11
|
+
>Attendance Details</span
|
|
12
|
+
>
|
|
13
|
+
</v-row>
|
|
14
|
+
</v-toolbar>
|
|
15
|
+
|
|
16
|
+
<v-card-text class="pa-6">
|
|
17
|
+
<v-row v-if="attendance" dense>
|
|
18
|
+
<v-col :cols="attendance.checkOut?.timestamp ? 6 : 12">
|
|
19
|
+
<v-row dense>
|
|
20
|
+
<v-col cols="12" class="mb-1">
|
|
21
|
+
<InputLabel title="Check In" />
|
|
22
|
+
</v-col>
|
|
23
|
+
<v-col cols="12" class="mb-1">
|
|
24
|
+
<span class="text-body-1">{{
|
|
25
|
+
attendance.checkIn?.timestamp
|
|
26
|
+
? formatDate(attendance.checkIn.timestamp)
|
|
27
|
+
: "N/A"
|
|
28
|
+
}}</span>
|
|
29
|
+
</v-col>
|
|
30
|
+
<v-col cols="12" class="mb-2">
|
|
31
|
+
<span class="text-caption text-medium-emphasis">
|
|
32
|
+
{{
|
|
33
|
+
attendance.checkIn?.location?.latitude &&
|
|
34
|
+
attendance.checkIn?.location?.longitude
|
|
35
|
+
? `Lat: ${attendance.checkIn.location.latitude}, Lng: ${attendance.checkIn.location.longitude}`
|
|
36
|
+
: "Lat: N/A, Lng: N/A"
|
|
37
|
+
}}
|
|
38
|
+
</span>
|
|
39
|
+
</v-col>
|
|
40
|
+
<v-col cols="12">
|
|
41
|
+
<v-card
|
|
42
|
+
v-if="attendance.checkIn?.img"
|
|
43
|
+
variant="outlined"
|
|
44
|
+
class="pa-0"
|
|
45
|
+
style="border: 1px solid rgba(0, 0, 0, 0.12)"
|
|
46
|
+
>
|
|
47
|
+
<v-img
|
|
48
|
+
:src="getFileUrl(attendance.checkIn.img)"
|
|
49
|
+
height="280"
|
|
50
|
+
width="100%"
|
|
51
|
+
cover
|
|
52
|
+
></v-img>
|
|
53
|
+
</v-card>
|
|
54
|
+
<v-card
|
|
55
|
+
v-else
|
|
56
|
+
variant="outlined"
|
|
57
|
+
class="pa-8 text-center"
|
|
58
|
+
style="border: 1px solid rgba(0, 0, 0, 0.12); height: 280px"
|
|
59
|
+
>
|
|
60
|
+
<span class="text-caption text-medium-emphasis"
|
|
61
|
+
>No image</span
|
|
62
|
+
>
|
|
63
|
+
</v-card>
|
|
64
|
+
</v-col>
|
|
65
|
+
</v-row>
|
|
66
|
+
</v-col>
|
|
67
|
+
|
|
68
|
+
<v-col v-if="attendance.checkOut?.timestamp" cols="6">
|
|
69
|
+
<v-row dense>
|
|
70
|
+
<v-col cols="12" class="mb-1">
|
|
71
|
+
<InputLabel title="Check Out" />
|
|
72
|
+
</v-col>
|
|
73
|
+
<v-col cols="12" class="mb-1">
|
|
74
|
+
<span class="text-body-1">{{
|
|
75
|
+
formatDate(attendance.checkOut.timestamp)
|
|
76
|
+
}}</span>
|
|
77
|
+
</v-col>
|
|
78
|
+
<v-col cols="12" class="mb-2">
|
|
79
|
+
<span class="text-caption text-medium-emphasis">
|
|
80
|
+
{{
|
|
81
|
+
attendance.checkOut?.location?.latitude &&
|
|
82
|
+
attendance.checkOut?.location?.longitude
|
|
83
|
+
? `Lat: ${attendance.checkOut.location.latitude}, Lng: ${attendance.checkOut.location.longitude}`
|
|
84
|
+
: "Lat: N/A, Lng: N/A"
|
|
85
|
+
}}
|
|
86
|
+
</span>
|
|
87
|
+
</v-col>
|
|
88
|
+
<v-col cols="12">
|
|
89
|
+
<v-card
|
|
90
|
+
v-if="attendance.checkOut?.img"
|
|
91
|
+
variant="outlined"
|
|
92
|
+
class="pa-0"
|
|
93
|
+
style="border: 1px solid rgba(0, 0, 0, 0.12)"
|
|
94
|
+
>
|
|
95
|
+
<v-img
|
|
96
|
+
:src="getFileUrl(attendance.checkOut.img)"
|
|
97
|
+
height="280"
|
|
98
|
+
width="100%"
|
|
99
|
+
cover
|
|
100
|
+
></v-img>
|
|
101
|
+
</v-card>
|
|
102
|
+
<v-card
|
|
103
|
+
v-else
|
|
104
|
+
variant="outlined"
|
|
105
|
+
class="pa-8 text-center"
|
|
106
|
+
style="border: 1px solid rgba(0, 0, 0, 0.12); height: 280px"
|
|
107
|
+
>
|
|
108
|
+
<span class="text-caption text-medium-emphasis"
|
|
109
|
+
>No image</span
|
|
110
|
+
>
|
|
111
|
+
</v-card>
|
|
112
|
+
</v-col>
|
|
113
|
+
</v-row>
|
|
114
|
+
</v-col>
|
|
115
|
+
</v-row>
|
|
116
|
+
|
|
117
|
+
<v-row v-else dense>
|
|
118
|
+
<v-col cols="12" class="text-center py-8">
|
|
119
|
+
<v-progress-circular indeterminate color="primary" />
|
|
120
|
+
</v-col>
|
|
121
|
+
</v-row>
|
|
122
|
+
</v-card-text>
|
|
123
|
+
|
|
124
|
+
<v-toolbar class="pa-0" density="compact">
|
|
125
|
+
<v-row no-gutters>
|
|
126
|
+
<v-col cols="12" class="pa-0">
|
|
127
|
+
<v-btn
|
|
128
|
+
block
|
|
129
|
+
variant="text"
|
|
130
|
+
class="text-none"
|
|
131
|
+
size="large"
|
|
132
|
+
@click="close"
|
|
133
|
+
height="48"
|
|
134
|
+
>
|
|
135
|
+
Close
|
|
136
|
+
</v-btn>
|
|
137
|
+
</v-col>
|
|
138
|
+
</v-row>
|
|
139
|
+
</v-toolbar>
|
|
140
|
+
</v-card>
|
|
141
|
+
</v-dialog>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<script setup lang="ts">
|
|
145
|
+
const showDialog = defineModel({ type: Boolean, default: false });
|
|
146
|
+
|
|
147
|
+
const props = defineProps({
|
|
148
|
+
attendanceId: { type: String, default: "" },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const emit = defineEmits(["close"]);
|
|
152
|
+
|
|
153
|
+
const attendance = ref<Record<string, any> | null>(null);
|
|
154
|
+
const { getMyAttendanceById } = useAttendance();
|
|
155
|
+
const { formatDate } = useUtils();
|
|
156
|
+
const { getFileUrl } = useFile();
|
|
157
|
+
|
|
158
|
+
const close = () => {
|
|
159
|
+
showDialog.value = false;
|
|
160
|
+
emit("close");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
watch(
|
|
164
|
+
() => props.attendanceId,
|
|
165
|
+
async (newId) => {
|
|
166
|
+
if (newId && showDialog.value) {
|
|
167
|
+
attendance.value = null;
|
|
168
|
+
try {
|
|
169
|
+
const res = await getMyAttendanceById(newId);
|
|
170
|
+
attendance.value = res?.data || res;
|
|
171
|
+
} catch (err: any) {
|
|
172
|
+
console.error("Failed to load attendance by id", err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{ immediate: true }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
watch(showDialog, (newVal) => {
|
|
180
|
+
if (!newVal) {
|
|
181
|
+
attendance.value = null;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
</script>
|