@7365admin1/layer-common 1.9.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AcceptDialog.vue +44 -0
  3. package/components/AccessCardAddForm.vue +101 -13
  4. package/components/AccessManagement.vue +130 -47
  5. package/components/AddSupplyForm.vue +165 -0
  6. package/components/AreaChecklistHistoryLogs.vue +235 -0
  7. package/components/AreaChecklistHistoryMain.vue +176 -0
  8. package/components/AreaFormDialog.vue +266 -0
  9. package/components/AreaMain.vue +841 -0
  10. package/components/AttendanceCheckInOutDialog.vue +416 -0
  11. package/components/AttendanceDetailsDialog.vue +184 -0
  12. package/components/AttendanceMain.vue +155 -0
  13. package/components/AttendanceMapSearchDialog.vue +393 -0
  14. package/components/AttendanceSettingsDialog.vue +398 -0
  15. package/components/BuildingManagement/buildings.vue +5 -5
  16. package/components/BuildingManagement/units.vue +5 -5
  17. package/components/ChecklistItemRow.vue +54 -0
  18. package/components/CheckoutItemMain.vue +705 -0
  19. package/components/CleaningScheduleMain.vue +271 -0
  20. package/components/DocumentManagement.vue +8 -9
  21. package/components/EntryPass/QrTemplatePreview.vue +104 -0
  22. package/components/EntryPassMain.vue +252 -200
  23. package/components/HygieneUpdateMoreAction.vue +238 -0
  24. package/components/IncidentReport/Authorities.vue +226 -0
  25. package/components/IncidentReport/IncidentInformation.vue +258 -0
  26. package/components/IncidentReport/affectedEntities.vue +167 -0
  27. package/components/InvitationMain.vue +19 -17
  28. package/components/ManageChecklistMain.vue +384 -0
  29. package/components/MemberMain.vue +48 -20
  30. package/components/MyAttendanceMain.vue +224 -0
  31. package/components/OnlineFormsConfiguration.vue +9 -2
  32. package/components/PasswordConfirmation.vue +95 -0
  33. package/components/PhotoUpload.vue +410 -0
  34. package/components/RolePermissionMain.vue +17 -15
  35. package/components/ScheduleAreaMain.vue +313 -0
  36. package/components/ScheduleTaskAreaFormDialog.vue +144 -0
  37. package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
  38. package/components/ScheduleTaskForm.vue +471 -0
  39. package/components/ScheduleTaskMain.vue +345 -0
  40. package/components/ScheduleTastTicketMain.vue +182 -0
  41. package/components/ServiceProviderMain.vue +27 -7
  42. package/components/StockCard.vue +191 -0
  43. package/components/SupplyManagementMain.vue +557 -0
  44. package/components/TableHygiene.vue +617 -0
  45. package/components/UnitMain.vue +451 -0
  46. package/components/VisitorManagement.vue +28 -15
  47. package/composables/useAccessManagement.ts +90 -0
  48. package/composables/useAreaPermission.ts +51 -0
  49. package/composables/useAreas.ts +99 -0
  50. package/composables/useAttendance.ts +89 -0
  51. package/composables/useAttendancePermission.ts +68 -0
  52. package/composables/useBuilding.ts +2 -2
  53. package/composables/useBuildingUnit.ts +2 -2
  54. package/composables/useCard.ts +2 -0
  55. package/composables/useCheckout.ts +61 -0
  56. package/composables/useCheckoutPermission.ts +80 -0
  57. package/composables/useCleaningPermission.ts +229 -0
  58. package/composables/useCleaningSchedulePermission.ts +58 -0
  59. package/composables/useCleaningSchedules.ts +233 -0
  60. package/composables/useCountry.ts +8 -0
  61. package/composables/useDOBEntries.ts +13 -0
  62. package/composables/useDashboardData.ts +2 -2
  63. package/composables/useDocument.ts +3 -2
  64. package/composables/useFeedback.ts +1 -1
  65. package/composables/useFile.ts +4 -6
  66. package/composables/useLocation.ts +78 -0
  67. package/composables/useOnlineForm.ts +16 -9
  68. package/composables/usePeople.ts +87 -72
  69. package/composables/useQR.ts +29 -0
  70. package/composables/useRole.ts +3 -2
  71. package/composables/useScheduleTask.ts +89 -0
  72. package/composables/useScheduleTaskArea.ts +85 -0
  73. package/composables/useScheduleTaskPermission.ts +68 -0
  74. package/composables/useSiteEntryPassSettings.ts +4 -15
  75. package/composables/useStock.ts +45 -0
  76. package/composables/useSupply.ts +63 -0
  77. package/composables/useSupplyPermission.ts +92 -0
  78. package/composables/useUnitPermission.ts +51 -0
  79. package/composables/useUnits.ts +82 -0
  80. package/composables/useWebUsb.ts +389 -0
  81. package/composables/useWorkOrder.ts +1 -1
  82. package/nuxt.config.ts +3 -0
  83. package/package.json +4 -1
  84. package/types/area.d.ts +22 -0
  85. package/types/attendance.d.ts +38 -0
  86. package/types/checkout-item.d.ts +27 -0
  87. package/types/cleaner-schedule.d.ts +54 -0
  88. package/types/location.d.ts +42 -0
  89. package/types/schedule-task.d.ts +18 -0
  90. package/types/stock.d.ts +16 -0
  91. package/types/supply.d.ts +11 -0
  92. package/types/verification.d.ts +1 -1
  93. 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>