@7365admin1/layer-common 1.10.8 → 1.10.10

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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardAddForm.vue +1 -1
  3. package/components/AccessCardAssignToUnitForm.vue +1 -1
  4. package/components/AccessManagement.vue +1 -1
  5. package/components/BulletinBoardManagement.vue +18 -8
  6. package/components/Carousel.vue +474 -0
  7. package/components/DeliveryCompany.vue +240 -0
  8. package/components/DrawImage.vue +172 -0
  9. package/components/EntryPassInformation.vue +70 -10
  10. package/components/EquipmentItemMain.vue +9 -4
  11. package/components/Feedback/Form.vue +4 -4
  12. package/components/FeedbackMain.vue +734 -146
  13. package/components/FileInput.vue +289 -0
  14. package/components/IncidentReport/Authorities.vue +189 -151
  15. package/components/IncidentReport/IncidentInformation.vue +14 -10
  16. package/components/IncidentReport/IncidentInformationDownload.vue +212 -0
  17. package/components/IncidentReport/affectedEntities.vue +8 -57
  18. package/components/SiteSettings.vue +285 -0
  19. package/components/StockCard.vue +11 -7
  20. package/components/Tooltip/Info.vue +33 -0
  21. package/components/VisitorForm.vue +176 -45
  22. package/components/VisitorManagement.vue +23 -6
  23. package/composables/useAccessManagement.ts +60 -18
  24. package/composables/useBulletin.ts +8 -3
  25. package/composables/useBulletinBoardPermission.ts +48 -0
  26. package/composables/useCleaningPermission.ts +2 -0
  27. package/composables/useCommonPermission.ts +29 -1
  28. package/composables/useEquipmentManagement.ts +63 -0
  29. package/composables/useFeedback.ts +53 -21
  30. package/composables/useFile.ts +6 -0
  31. package/composables/useLocalAuth.ts +29 -1
  32. package/composables/useSiteSettings.ts +1 -1
  33. package/composables/useUploadFiles.ts +94 -0
  34. package/composables/useUtils.ts +152 -53
  35. package/composables/useVisitor.ts +9 -6
  36. package/constants/app.ts +12 -0
  37. package/nuxt.config.ts +2 -0
  38. package/package.json +3 -1
  39. package/plugins/vue-draggable-next.client.ts +5 -0
  40. package/types/feedback.d.ts +5 -2
  41. package/types/site.d.ts +2 -1
  42. package/types/user.d.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 276911d: Update Bulletin Board changes
8
+
9
+ ## 1.10.9
10
+
11
+ ### Patch Changes
12
+
13
+ - added web security feedbacks
14
+
3
15
  ## 1.10.8
4
16
 
5
17
  ### Patch Changes
@@ -333,7 +333,7 @@
333
333
  <v-col cols="12">
334
334
  <InputLabel
335
335
  class="text-capitalize font-weight-bold"
336
- title="Building"
336
+ title="Block"
337
337
  />
338
338
  <v-autocomplete
339
339
  v-model="card.assignBuilding"
@@ -12,7 +12,7 @@
12
12
  <v-col cols="12" class="px-1">
13
13
  <InputLabel
14
14
  class="text-capitalize font-weight-bold"
15
- title="Building"
15
+ title="Block"
16
16
  required
17
17
  />
18
18
  <v-autocomplete
@@ -151,7 +151,7 @@ const props = defineProps({
151
151
  type: Array as PropType<Array<Record<string, any>>>,
152
152
  default: () => [
153
153
  {
154
- title: "Building",
154
+ title: "Block",
155
155
  value: "block.name",
156
156
  },
157
157
  {
@@ -2,7 +2,7 @@
2
2
  <v-row no-gutters>
3
3
  <TableMain :headers="headers" :items="paginatePlaceholderItem" v-model:search="searchInput"
4
4
  :loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
5
- @refresh="getAnnouncementsRefresh" show-header @update:page="handleUpdatePage" @row-click="handleRowClick"
5
+ @refresh="getAnnouncementsRefresh" :show-header="APP_CONSTANTS.RESIDENT === props.recipients" @update:page="handleUpdatePage" @row-click="handleRowClick"
6
6
  @create="handleCreateEvent" :can-create="canCreateBulletinBoard" create-label="Add Announcement">
7
7
  <template #extension>
8
8
  <v-row no-gutters class="w-100 d-flex flex-column">
@@ -60,6 +60,8 @@
60
60
  </v-row>
61
61
  </template>
62
62
  <script setup lang="ts">
63
+ import { APP_CONSTANTS } from '../constants/app';
64
+
63
65
  definePageMeta({
64
66
  memberOnly: true,
65
67
  })
@@ -104,13 +106,21 @@ const { debounce } = useUtils()
104
106
 
105
107
 
106
108
 
107
- const headers = [
108
- { title: "Title", value: "title" },
109
- { title: "Date Created", value: "createdAt", align: "start" },
110
- { title: "Start Date/End Date", value: "duration", align: "start" },
111
- { title: "No Expiration", value: "noExpiration", align: "start" },
112
- { title: "", value: "actions" },
113
- ];
109
+ const headers = computed(() => {
110
+ const arr = [
111
+ { title: "Title", value: "title", align: "start" },
112
+ ]
113
+
114
+ if (props.recipients === APP_CONSTANTS.RESIDENT) {
115
+ arr.push({ title: "Date Created", value: "createdAt", align: "start" });
116
+ arr.push({ title: "Start Date/End Date", value: "duration", align: "start" });
117
+ arr.push({ title: "No Expiration", value: "noExpiration", align: "start" });
118
+ arr.push({ title: "", value: "actions", align: "center" });
119
+ }
120
+
121
+ return arr;
122
+
123
+ });
114
124
  const items = ref<TAnnouncement[]>([]);
115
125
  const page = ref(1);
116
126
  const pages = ref(0);
@@ -0,0 +1,474 @@
1
+ <template>
2
+ <v-carousel
3
+ eager
4
+ :show-arrows="mediaItems && mediaItems.length > 1"
5
+ v-if="!isConverting"
6
+ :height="height || 400"
7
+ >
8
+ <template v-for="(item, idx) in mediaItems" :key="idx">
9
+ <!-- IMAGE -->
10
+ <v-carousel-item v-if="item.type === 'image'">
11
+ <v-img
12
+ :src="item.url"
13
+ class="cursor-pointer"
14
+ cover
15
+ height="100%"
16
+ @click.stop="openMediaDialog(item.url)"
17
+ >
18
+ <template #placeholder>
19
+ <v-skeleton-loader height="100%" width="100%" />
20
+ </template>
21
+ </v-img>
22
+
23
+ <div class="edit-btn mb-5" v-if="imgEditable">
24
+ <v-btn
25
+ color="primary"
26
+ height="42px"
27
+ width="100px"
28
+ @click.stop="onMediaEdit(item.url, idx)"
29
+ >
30
+ Edit
31
+ </v-btn>
32
+ </div>
33
+
34
+ <div class="delete-btn mb-3" v-if="imgDelete">
35
+ <v-btn
36
+ color="red"
37
+ height="42px"
38
+ width="100px"
39
+ @click.stop="onMediaDelete(item.url)"
40
+ >
41
+ Delete
42
+ </v-btn>
43
+ </div>
44
+ </v-carousel-item>
45
+
46
+ <!-- VIDEO -->
47
+ <v-carousel-item v-else-if="item.type === 'video'">
48
+ <div
49
+ class="media-fill cursor-pointer"
50
+ @click.stop="openMediaDialog(item.url)"
51
+ >
52
+ <video class="media-video" muted playsinline preload="metadata">
53
+ <source :src="item.url" />
54
+ </video>
55
+ <div class="play-overlay">
56
+ <v-btn class="play-btn" icon size="55" color="primary">
57
+ <v-icon icon="mdi-play" size="40" />
58
+ </v-btn>
59
+ </div>
60
+
61
+ <div class="delete-btn pb-4" v-if="imgDelete">
62
+ <v-btn
63
+ color="red"
64
+ height="42px"
65
+ width="100px"
66
+ @click.stop="onMediaDelete(item.url)"
67
+ >
68
+ Delete
69
+ </v-btn>
70
+ </div>
71
+ </div>
72
+ </v-carousel-item>
73
+
74
+ <!-- DOCUMENT -->
75
+ <v-carousel-item v-else>
76
+ <div
77
+ class="media-fill d-flex flex-column align-center justify-center cursor-pointer"
78
+ @click.stop="
79
+ downloadFile({ name: item.name, url: item.url, type: item.type })
80
+ "
81
+ >
82
+ <v-icon
83
+ :icon="getFileIcon(item.type)"
84
+ size="150"
85
+ :color="getFileIconColor(getFileIcon(item.type))"
86
+ />
87
+ <div class="text-h6 text-center text-wrap mt-2">{{ item.name }}</div>
88
+
89
+ <div class="delete-btn pb-4" v-if="imgDelete">
90
+ <v-btn
91
+ color="red"
92
+ height="42px"
93
+ width="100px"
94
+ @click.stop="onMediaDelete(item.url)"
95
+ >
96
+ Delete
97
+ </v-btn>
98
+ </div>
99
+ </div>
100
+ </v-carousel-item>
101
+ </template>
102
+ </v-carousel>
103
+
104
+ <v-skeleton-loader height="400px" width="100%" v-else />
105
+
106
+ <!-- Dialog for fullscreen view -->
107
+ <v-dialog v-model="isDialogOpen" max-width="700">
108
+ <v-card>
109
+ <v-btn icon class="close-btn" @click="isDialogOpen = false">
110
+ <v-icon icon="mdi-close" size="40" />
111
+ </v-btn>
112
+
113
+ <!-- Image preview -->
114
+ <v-img
115
+ v-if="selectedMediaType === 'image'"
116
+ :src="selectedMediaUrl"
117
+ contain
118
+ />
119
+
120
+ <!-- Video preview with playback controls -->
121
+ <div
122
+ v-else-if="selectedMediaType === 'video'"
123
+ class="dialog-video-container"
124
+ >
125
+ <video autoplay controls class="full-size-video" @click.stop>
126
+ <source :src="selectedMediaUrl" />
127
+ </video>
128
+ </div>
129
+ </v-card>
130
+ </v-dialog>
131
+
132
+ <!-- Image editor component -->
133
+ <DrawImage
134
+ v-if="isShowImageEdit"
135
+ :is-show-dialog="isShowImageEdit"
136
+ :image-url="imageUrl"
137
+ :image-idx="imageIdx"
138
+ @on-submit="onImageSubmitEdit"
139
+ @on-close-dialog="isShowImageEdit = false"
140
+ />
141
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
142
+ </template>
143
+
144
+ <script setup lang="ts">
145
+ const { fileToBase64 } = useUtils();
146
+
147
+ const emit = defineEmits([
148
+ "on-click-carousel",
149
+ "on-file-edit",
150
+ "on-file-delete",
151
+ ]);
152
+
153
+ const props = defineProps<{
154
+ height?: string | number;
155
+ urls?: string[];
156
+ clickable?: boolean;
157
+ imgEditable?: boolean;
158
+ dataFiles?: {
159
+ name: string;
160
+ data: File;
161
+ progress: number;
162
+ id?: string;
163
+ url?: string;
164
+ type?: string;
165
+ }[];
166
+ imgDelete?: boolean;
167
+ }>();
168
+
169
+ const isConverting = ref(false);
170
+ const isShowImageEdit = ref(false);
171
+ const imageUrl = ref();
172
+ const imageIdx = ref();
173
+ const videoRefs = ref([]);
174
+
175
+ // Media items with thumbnail property
176
+ const mediaItems = ref<
177
+ {
178
+ url: string;
179
+ name: string;
180
+ type: string;
181
+ mimeType?: string;
182
+ thumbnail?: string;
183
+ }[]
184
+ >([]);
185
+
186
+ // Dialog state
187
+ const isDialogOpen = ref(false);
188
+ const selectedMediaUrl = ref("");
189
+ const selectedMediaType = ref<string>("");
190
+ const selectedMediaMimeType = ref("");
191
+
192
+ function getFileType(file: any) {
193
+ return file.type.startsWith("video/")
194
+ ? "video"
195
+ : file.type.startsWith("image/")
196
+ ? "image"
197
+ : file.type;
198
+ }
199
+
200
+ function getMimeType(file: any) {
201
+ return file.type || "application/octet-stream";
202
+ }
203
+
204
+ // Edit functions
205
+ const onMediaEdit = (url: string, idx: number) => {
206
+ isShowImageEdit.value = true;
207
+ imageUrl.value = url;
208
+ imageIdx.value = idx;
209
+ };
210
+
211
+ const onMediaDelete = (url: string) => {
212
+ mediaItems.value = mediaItems.value.filter((file) => file.url !== url);
213
+ URL.revokeObjectURL(url);
214
+ emit("on-file-delete", url);
215
+ };
216
+
217
+ const onImageSubmitEdit = (url: string, idx: number) => {
218
+ if (mediaItems.value && mediaItems.value.length > 0) {
219
+ mediaItems.value[idx].url = url;
220
+ }
221
+ isShowImageEdit.value = false;
222
+ emit("on-file-edit", url, idx);
223
+ };
224
+
225
+ // Open dialog for fullscreen view
226
+ const openMediaDialog = (url: string) => {
227
+ const mediaItem = mediaItems.value.find((item) => item.url === url);
228
+ if (mediaItem) {
229
+ selectedMediaUrl.value = url;
230
+ selectedMediaType.value = mediaItem.type;
231
+ selectedMediaMimeType.value = mediaItem.mimeType || "";
232
+ isDialogOpen.value = true;
233
+ }
234
+ };
235
+
236
+ async function getMimeFromHead(url: string) {
237
+ const res = await fetch(url);
238
+ if (!res.ok) throw new Error(`HEAD failed: ${res.status}`);
239
+ return res.headers.get("content-type");
240
+ }
241
+
242
+ watchEffect(async (onCleanup) => {
243
+ let isCancelled = false;
244
+
245
+ onCleanup(() => {
246
+ isCancelled = true;
247
+ });
248
+
249
+ // Local temporary storage
250
+ const newMediaItems: any = [];
251
+
252
+ if (props.dataFiles && props.dataFiles.length > 0) {
253
+ isConverting.value = true;
254
+
255
+ for (const file of props.dataFiles) {
256
+ try {
257
+ let fileType;
258
+ let mimeType;
259
+ let finalUrl;
260
+
261
+ // 1. Process the file data
262
+ if ("type" in file && "size" in file) {
263
+ mimeType = getMimeType(file);
264
+ fileType = getFileType(file);
265
+
266
+ if (fileType === "video") {
267
+ finalUrl = URL.createObjectURL(file);
268
+ } else {
269
+ // Awaiting async conversion
270
+ finalUrl = await fileToBase64(file);
271
+ }
272
+ } else {
273
+ finalUrl = file.url;
274
+ fileType = getFileType(file);
275
+ mimeType = getMimeType(file);
276
+ }
277
+
278
+ // 2. Check if the watcher was reset during the 'await'
279
+ if (isCancelled) return;
280
+
281
+ // 3. Push to LOCAL array, not the reactive .value
282
+ newMediaItems.push({
283
+ url: finalUrl,
284
+ name: file?.name,
285
+ type: fileType,
286
+ mimeType: mimeType,
287
+ thumbnail: undefined,
288
+ });
289
+ } catch (error) {
290
+ console.error("Error processing file:", error);
291
+ }
292
+ }
293
+ } else if (props.urls && props.urls.length > 0) {
294
+ isConverting.value = true;
295
+
296
+ for (const url of props.urls) {
297
+ try {
298
+ const mimeType = await getMimeFromHead(url);
299
+ if (isCancelled) return;
300
+
301
+ newMediaItems.push({
302
+ url,
303
+ name: "",
304
+ type: mimeType
305
+ ? mimeType.startsWith("image/")
306
+ ? "image"
307
+ : mimeType.startsWith("video/")
308
+ ? "video"
309
+ : mimeType
310
+ : mimeType,
311
+ });
312
+ } catch (error) {
313
+ console.error("Error fetching MIME type:", error);
314
+ }
315
+ }
316
+ }
317
+
318
+ // 4. Finalizing state
319
+ if (!isCancelled) {
320
+ isConverting.value = false;
321
+ // Replace the entire array at once to avoid "doubling" or partial updates
322
+ mediaItems.value = newMediaItems;
323
+ console.log("watchEffect mediaItems updated:", mediaItems.value);
324
+ }
325
+ });
326
+
327
+ const getFileIcon = (type: string) => {
328
+ const iconMap: any = {
329
+ "application/pdf": "mdi-file-pdf-box",
330
+ "application/msword": "mdi-file-word-box",
331
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
332
+ "mdi-file-word-box",
333
+ "application/vnd.ms-excel": "mdi-file-excel-box",
334
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
335
+ "mdi-file-excel-box",
336
+ "application/vnd.ms-powerpoint": "mdi-file-powerpoint-box",
337
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation":
338
+ "mdi-file-powerpoint-box",
339
+ "text/plain": "mdi-file-document-outline",
340
+ "text/csv": "mdi-file-delimited-outline",
341
+ "application/zip": "mdi-zip-box-outline",
342
+ "application/x-rar-compressed": "mdi-zip-box-outline",
343
+ // Add more mappings as needed
344
+ };
345
+
346
+ return iconMap[type] || "mdi-file-outline";
347
+ };
348
+
349
+ function getFileIconColor(icon?: string) {
350
+ if (icon === "mdi-file-pdf-box") return "#F44336"; // Red
351
+ if (icon === "mdi-file-word-box") return "#2196F3"; // Blue
352
+ if (icon === "mdi-file-excel-box" || icon === "mdi-file-delimited-outline")
353
+ return "#4CAF50"; // Green
354
+ if (icon === "mdi-file-powerpoint-box") return "#FF9800"; // Orange
355
+ if (icon === "mdi-zip-box-outline") return "#9C27B0"; // Purple
356
+ return "grey";
357
+ }
358
+
359
+ const message = ref("");
360
+ const messageColor = ref("");
361
+ const messageSnackbar = ref(false);
362
+
363
+ function showMessage(msg: string, color: string) {
364
+ message.value = msg;
365
+ messageColor.value = color;
366
+ messageSnackbar.value = true;
367
+ }
368
+
369
+ async function downloadFile({
370
+ name,
371
+ url,
372
+ type,
373
+ }: {
374
+ name: string;
375
+ url: string;
376
+ type: string;
377
+ }) {
378
+ try {
379
+ const link = document.createElement("a");
380
+ link.href = url;
381
+ if (type == "application/pdf") {
382
+ link.target = "_blank";
383
+ }
384
+ link.rel = "noopener noreferrer";
385
+ link.download = name;
386
+
387
+ document.body.appendChild(link);
388
+ link.click();
389
+ document.body.removeChild(link);
390
+
391
+ URL.revokeObjectURL(url);
392
+ } catch (err) {
393
+ showMessage(err as string, "error");
394
+ }
395
+ }
396
+
397
+ // // Clean up object URLs when component is unmounted
398
+ // onBeforeUnmount(() => {
399
+ // // Revoke any object URLs to prevent memory leaks
400
+ // mediaItems.value.forEach((item) => {
401
+ // if (item.type === "video" && item.url && item.url.startsWith("blob:")) {
402
+ // URL.revokeObjectURL(item.url);
403
+ // }
404
+ // });
405
+ // });
406
+ </script>
407
+
408
+ <style scoped>
409
+ .edit-btn {
410
+ position: absolute;
411
+ bottom: 24%;
412
+ right: 5%;
413
+ z-index: 5;
414
+ }
415
+
416
+ .delete-btn {
417
+ position: absolute;
418
+ bottom: 13%;
419
+ right: 5%;
420
+ }
421
+
422
+ .close-btn {
423
+ position: absolute;
424
+ top: 10px;
425
+ right: 10px;
426
+ background-color: rgba(0, 0, 0, 0.5);
427
+ color: white;
428
+ z-index: 10;
429
+ }
430
+
431
+ .play-overlay {
432
+ position: absolute;
433
+ inset: 0;
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ pointer-events: none;
438
+ }
439
+ .play-overlay .v-btn {
440
+ pointer-events: auto;
441
+ }
442
+
443
+ .play-btn {
444
+ border-radius: 9999px;
445
+ }
446
+
447
+ .dialog-video-container {
448
+ display: flex;
449
+ justify-content: center;
450
+ align-items: center;
451
+ width: 100%;
452
+ padding: 20px;
453
+ }
454
+
455
+ .full-size-video {
456
+ max-width: 100%;
457
+ max-height: 80vh;
458
+ }
459
+
460
+ .media-fill {
461
+ position: relative;
462
+ width: 100%;
463
+ height: 100%;
464
+ }
465
+
466
+ .media-video {
467
+ position: absolute;
468
+ inset: 0;
469
+ width: 100%;
470
+ height: 100%;
471
+ object-fit: cover;
472
+ display: block;
473
+ }
474
+ </style>