@7365admin1/layer-common 3.0.27 → 3.0.28

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.0.28
4
+
5
+ ### Patch Changes
6
+
7
+ - 0c04bbf: Update Package Version
8
+
3
9
  ## 3.0.27
4
10
 
5
11
  ### Patch Changes
@@ -41,7 +41,7 @@
41
41
  <div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">
42
42
  Person <span class="text-error">*</span>
43
43
  </div>
44
- <v-autocomplete
44
+ <v-select
45
45
  v-model="assignPerson"
46
46
  :items="filteredPeopleItems"
47
47
  density="compact"
@@ -63,7 +63,7 @@
63
63
  {{ personTypeLabel(item.raw) }}
64
64
  </v-chip>
65
65
  </template>
66
- </v-autocomplete>
66
+ </v-select>
67
67
  <p v-if="assignError" class="text-error text-caption mt-2">{{ assignError }}</p>
68
68
  </v-card-text>
69
69
 
@@ -545,7 +545,10 @@ async function confirmAssign() {
545
545
  cancelAssign();
546
546
  emit("refresh");
547
547
  } catch (e: any) {
548
- const msg = e?.data?.message ?? "Failed to assign. Please try again.";
548
+ const rawMsg = e?.data?.message ?? "";
549
+ const msg = rawMsg.includes("500")
550
+ ? "EntryPass server is unavailable. Please contact your administrator."
551
+ : rawMsg || "Failed to assign. Please try again.";
549
552
  assignError.value = msg;
550
553
  showToast(msg, "error");
551
554
  } finally {
@@ -1,337 +1,417 @@
1
1
  <template>
2
- <v-card width="100%" :loading="processing">
3
- <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6" align="center">
5
- <span class="font-weight-bold text-subtitle-1 text-capitalize">
6
- Preview Announcement
2
+ <v-card width="100%" :loading="processing">
3
+ <v-toolbar>
4
+ <v-row no-gutters class="fill-height px-6" align="center">
5
+ <span class="font-weight-bold text-subtitle-1 text-capitalize">
6
+ Preview Announcement
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0 my-3">
12
+ <v-row no-gutters>
13
+ <template v-for="(label, key) in formattedFields" :key="key">
14
+ <v-col cols="12" class="mb-5">
15
+ <v-row no-gutters class="">
16
+ <v-col class="font-weight-bold">{{ label }}:</v-col>
17
+
18
+ <v-col cols="9" class="px-10">
19
+ <span v-if="key === 'content'" v-html="getFieldValue(key)" />
20
+ <span v-else-if="key === 'noExpiration'">
21
+ <BulletinExpirationChip
22
+ :value="getFieldValue(key) as boolean"
23
+ />
7
24
  </span>
25
+ <span v-else>
26
+ {{ getFieldValue(key) }}
27
+ </span>
28
+ </v-col>
8
29
  </v-row>
9
- </v-toolbar>
10
-
11
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0 my-3">
12
- <v-row no-gutters>
13
- <template v-for="(label, key) in formattedFields" :key="key">
14
- <v-col cols="12" class="mb-5">
15
- <v-row no-gutters class="">
16
- <v-col class="font-weight-bold">{{ label }}:</v-col>
17
-
18
- <v-col cols="9" class="px-10">
19
- <span v-if="key === 'content'" v-html="getFieldValue(key)" />
20
- <span v-else-if="key === 'noExpiration'">
21
- <BulletinExpirationChip :value="getFieldValue(key) as boolean" />
22
- </span>
23
- <span v-else>
24
- {{ getFieldValue(key) }}
25
- </span>
26
- </v-col>
27
-
28
-
29
- </v-row>
30
- </v-col>
31
- </template>
32
-
33
- <v-col cols="12" v-if="prop.activeAnnouncement?.file?.length > 0" class="mb-5">
34
- <v-row no-gutters>
35
- <v-col class="font-weight-bold">Attachments:</v-col>
36
- <v-col cols="9" class="px-10">
37
-
38
- <template v-if="videosArray.length > 0">
39
- <div class="text-caption text-medium-emphasis mb-1">Videos ({{ videosArray.length }})</div>
40
- <v-list density="compact" class="pa-0 rounded-lg border mb-3">
41
- <template v-for="(item, i) in videosArray" :key="item._id">
42
- <v-divider v-if="i > 0" />
43
- <v-list-item
44
- :prepend-icon="getFileIcon(item.file.type)"
45
- :title="item.file.name"
46
- :subtitle="item.file.type"
47
- class="cursor-pointer"
48
- @click="openMediaViewer(item._id, 'media')"
49
- >
50
- <template #prepend>
51
- <v-icon :color="getFileIconColor(item.file.type)" class="mr-3">{{ getFileIcon(item.file.type) }}</v-icon>
52
- </template>
53
- <template #append>
54
- <v-icon size="16" color="grey-lighten-1">mdi-play-circle-outline</v-icon>
55
- </template>
56
- </v-list-item>
57
- </template>
58
- </v-list>
59
- </template>
60
-
61
- <template v-if="photosArray.length > 0">
62
- <div class="text-caption text-medium-emphasis mb-1" :class="{ 'mt-3': videosArray.length > 0 }">Photos ({{ photosArray.length }})</div>
63
- <v-list density="compact" class="pa-0 rounded-lg border mb-3">
64
- <template v-for="(item, i) in photosArray" :key="item._id">
65
- <v-divider v-if="i > 0" />
66
- <v-list-item
67
- class="cursor-pointer"
68
- @click="openMediaViewer(item._id, 'media')"
69
- >
70
- <template #prepend>
71
- <v-avatar size="36" rounded="sm" class="mr-3">
72
- <v-img :src="getFileUrl(item._id)" cover>
73
- <template #error>
74
- <v-icon color="green" size="20">mdi-image</v-icon>
75
- </template>
76
- </v-img>
77
- </v-avatar>
78
- </template>
79
- <v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
80
- <v-list-item-subtitle class="text-caption">{{ item.file.type }}</v-list-item-subtitle>
81
- <template #append>
82
- <v-icon size="16" color="grey-lighten-1">mdi-eye-outline</v-icon>
83
- </template>
84
- </v-list-item>
85
- </template>
86
- </v-list>
87
- </template>
88
-
89
- <template v-if="filesArray.length > 0">
90
- <div class="text-caption text-medium-emphasis mb-1" :class="{ 'mt-3': videosArray.length > 0 || photosArray.length > 0 }">Files ({{ filesArray.length }})</div>
91
- <v-list density="compact" class="pa-0 rounded-lg border">
92
- <template v-for="(item, i) in filesArray" :key="item._id">
93
- <v-divider v-if="i > 0" />
94
- <v-list-item
95
- class="cursor-pointer"
96
- @click="openFileViewer(item._id, item.file.name)"
97
- >
98
- <template #prepend>
99
- <v-icon :color="getFileIconColor(item.file.type)" class="mr-3">{{ getFileIcon(item.file.type) }}</v-icon>
100
- </template>
101
- <v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
102
- <v-list-item-subtitle class="text-caption">{{ item.file.type }}</v-list-item-subtitle>
103
- <template #append>
104
- <v-icon size="16" color="grey-lighten-1">mdi-open-in-new</v-icon>
105
- </template>
106
- </v-list-item>
107
- </template>
108
- </v-list>
30
+ </v-col>
31
+ </template>
32
+
33
+ <v-col
34
+ cols="12"
35
+ v-if="prop.activeAnnouncement?.file?.length > 0"
36
+ class="mb-5"
37
+ >
38
+ <v-row no-gutters>
39
+ <v-col class="font-weight-bold">Attachments:</v-col>
40
+ <v-col cols="9" class="px-10">
41
+ <template v-if="videosArray.length > 0">
42
+ <div class="text-caption text-medium-emphasis mb-1">
43
+ Videos ({{ videosArray.length }})
44
+ </div>
45
+ <v-list density="compact" class="pa-0 rounded-lg border mb-3">
46
+ <template v-for="(item, i) in videosArray" :key="item._id">
47
+ <v-divider v-if="i > 0" />
48
+ <v-list-item
49
+ :prepend-icon="getFileIcon(item.file.type)"
50
+ :title="item.file.name"
51
+ :subtitle="item.file.type"
52
+ class="cursor-pointer"
53
+ @click="openMediaViewer(item._id, 'media')"
54
+ >
55
+ <template #prepend>
56
+ <v-icon
57
+ :color="getFileIconColor(item.file.type)"
58
+ class="mr-3"
59
+ >{{ getFileIcon(item.file.type) }}</v-icon
60
+ >
61
+ </template>
62
+ <template #append>
63
+ <v-icon size="16" color="grey-lighten-1"
64
+ >mdi-play-circle-outline</v-icon
65
+ >
66
+ </template>
67
+ </v-list-item>
68
+ </template>
69
+ </v-list>
70
+ </template>
71
+
72
+ <template v-if="photosArray.length > 0">
73
+ <div
74
+ class="text-caption text-medium-emphasis mb-1"
75
+ :class="{ 'mt-3': videosArray.length > 0 }"
76
+ >
77
+ Photos ({{ photosArray.length }})
78
+ </div>
79
+ <v-list density="compact" class="pa-0 rounded-lg border mb-3">
80
+ <template v-for="(item, i) in photosArray" :key="item._id">
81
+ <v-divider v-if="i > 0" />
82
+ <v-list-item
83
+ class="cursor-pointer"
84
+ @click="openMediaViewer(item._id, 'media')"
85
+ >
86
+ <template #prepend>
87
+ <v-avatar size="36" rounded="sm" class="mr-3">
88
+ <v-img :src="getFileUrl(item._id)" cover>
89
+ <template #error>
90
+ <v-icon color="green" size="20">mdi-image</v-icon>
109
91
  </template>
110
-
111
- </v-col>
112
- </v-row>
113
- </v-col>
114
-
115
- <!-- Media Carousel (images + videos) -->
116
- <ImageCarousel v-model="showCarousel" :active-file-id="activeFileId" :files="carouselFiles" />
117
-
118
- <!-- File Viewer Dialog (PDF, docs, etc.) -->
119
- <v-dialog v-model="showFileDialog" max-width="900" scrollable>
120
- <v-card>
121
- <v-toolbar density="compact">
122
- <v-toolbar-title class="text-subtitle-2 text-truncate">{{ activeFileName }}</v-toolbar-title>
123
- <v-spacer />
124
- <v-btn icon="mdi-open-in-new" variant="text" size="small"
125
- @click="openInNewTab(activeFileId)" />
126
- <v-btn icon="mdi-close" variant="text" size="small"
127
- @click="showFileDialog = false" />
128
- </v-toolbar>
129
- <v-card-text class="pa-0" style="height: 75vh;">
130
- <iframe
131
- :src="getFileUrl(activeFileId)"
132
- width="100%"
133
- height="100%"
134
- style="border: none;"
135
- :title="activeFileName"
136
- />
137
- </v-card-text>
138
- <v-card-actions>
139
- <v-spacer />
140
- <v-btn variant="text" class="text-none" @click="openInNewTab(activeFileId)">
141
- <v-icon start>mdi-download</v-icon>
142
- Open / Download
143
- </v-btn>
144
- </v-card-actions>
145
- </v-card>
146
- </v-dialog>
147
- </v-row>
148
- </v-card-text>
149
-
150
- <v-toolbar class="pa-0" density="compact">
151
- <v-row no-gutters>
152
- <v-col :col="(!canUpdate && !canDelete) ? 12 : 6" class="pa-0">
153
- <v-btn block variant="text" height="48" class="text-none" @click="emit('close')">
154
- Close
155
- </v-btn>
156
- </v-col>
157
-
158
- <v-col v-if="canUpdate || canDelete" cols="6" class="pa-0">
159
- <v-menu contained>
160
- <template #activator="{ props }">
161
- <v-btn block variant="flat" color="black" height="48" class="text-none" tile v-bind="props">
162
- More actions
163
- </v-btn>
164
- </template>
165
-
166
- <v-list class="pa-0">
167
- <v-list-item v-if="canUpdate" @click="handleEdit">
168
- <v-list-item-title class="text-subtitle-2">
169
- Edit Announcement
170
- </v-list-item-title>
171
- </v-list-item>
172
-
173
- <v-list-item v-if="canDelete" class="text-red" @click="handleDelete">
174
- <v-list-item-title class="text-subtitle-2">
175
- Delete Announcement
176
- </v-list-item-title>
177
- </v-list-item>
178
- </v-list>
179
- </v-menu>
180
- </v-col>
181
- </v-row>
182
- </v-toolbar>
183
- </v-card>
92
+ </v-img>
93
+ </v-avatar>
94
+ </template>
95
+ <v-list-item-title class="text-body-2">{{
96
+ item.file.name
97
+ }}</v-list-item-title>
98
+ <v-list-item-subtitle class="text-caption">{{
99
+ item.file.type
100
+ }}</v-list-item-subtitle>
101
+ <template #append>
102
+ <v-icon size="16" color="grey-lighten-1"
103
+ >mdi-eye-outline</v-icon
104
+ >
105
+ </template>
106
+ </v-list-item>
107
+ </template>
108
+ </v-list>
109
+ </template>
110
+
111
+ <template v-if="filesArray.length > 0">
112
+ <div
113
+ class="text-caption text-medium-emphasis mb-1"
114
+ :class="{
115
+ 'mt-3': videosArray.length > 0 || photosArray.length > 0,
116
+ }"
117
+ >
118
+ Files ({{ filesArray.length }})
119
+ </div>
120
+ <v-list density="compact" class="pa-0 rounded-lg border">
121
+ <template v-for="(item, i) in filesArray" :key="item._id">
122
+ <v-divider v-if="i > 0" />
123
+ <v-list-item
124
+ class="cursor-pointer"
125
+ @click="openFileViewer(item._id, item.file.name)"
126
+ >
127
+ <template #prepend>
128
+ <v-icon
129
+ :color="getFileIconColor(item.file.type)"
130
+ class="mr-3"
131
+ >{{ getFileIcon(item.file.type) }}</v-icon
132
+ >
133
+ </template>
134
+ <v-list-item-title class="text-body-2">{{
135
+ item.file.name
136
+ }}</v-list-item-title>
137
+ <v-list-item-subtitle class="text-caption">{{
138
+ item.file.type
139
+ }}</v-list-item-subtitle>
140
+ <template #append>
141
+ <v-icon size="16" color="grey-lighten-1"
142
+ >mdi-open-in-new</v-icon
143
+ >
144
+ </template>
145
+ </v-list-item>
146
+ </template>
147
+ </v-list>
148
+ </template>
149
+ </v-col>
150
+ </v-row>
151
+ </v-col>
152
+
153
+ <!-- Media Carousel (images + videos) -->
154
+ <ImageCarousel
155
+ v-model="showCarousel"
156
+ :active-file-id="activeFileId"
157
+ :files="carouselFiles"
158
+ />
159
+
160
+ <!-- File Viewer Dialog (PDF, docs, etc.) -->
161
+ <v-dialog v-model="showFileDialog" max-width="900" scrollable>
162
+ <v-card>
163
+ <v-toolbar density="compact">
164
+ <v-toolbar-title class="text-subtitle-2 text-truncate">{{
165
+ activeFileName
166
+ }}</v-toolbar-title>
167
+ <v-spacer />
168
+ <v-btn
169
+ icon="mdi-open-in-new"
170
+ variant="text"
171
+ size="small"
172
+ @click="openInNewTab(activeFileId)"
173
+ />
174
+ <v-btn
175
+ icon="mdi-close"
176
+ variant="text"
177
+ size="small"
178
+ @click="showFileDialog = false"
179
+ />
180
+ </v-toolbar>
181
+ <v-card-text class="pa-0" style="height: 75vh">
182
+ <iframe
183
+ :src="getFileUrl(activeFileId)"
184
+ width="100%"
185
+ height="100%"
186
+ style="border: none"
187
+ :title="activeFileName"
188
+ />
189
+ </v-card-text>
190
+ <v-card-actions>
191
+ <v-spacer />
192
+ <v-btn
193
+ variant="text"
194
+ class="text-none"
195
+ @click="openInNewTab(activeFileId)"
196
+ >
197
+ <v-icon start>mdi-download</v-icon>
198
+ Open / Download
199
+ </v-btn>
200
+ </v-card-actions>
201
+ </v-card>
202
+ </v-dialog>
203
+ </v-row>
204
+ </v-card-text>
205
+
206
+ <v-toolbar class="pa-0" density="compact">
207
+ <v-row no-gutters>
208
+ <v-col :col="!canUpdate && !canDelete ? 12 : 6" class="pa-0">
209
+ <v-btn
210
+ block
211
+ variant="text"
212
+ height="48"
213
+ class="text-none"
214
+ @click="emit('close')"
215
+ >
216
+ Close
217
+ </v-btn>
218
+ </v-col>
219
+
220
+ <v-col v-if="canUpdate || canDelete" cols="6" class="pa-0">
221
+ <v-menu contained>
222
+ <template #activator="{ props }">
223
+ <v-btn
224
+ block
225
+ variant="flat"
226
+ color="black"
227
+ height="48"
228
+ class="text-none"
229
+ tile
230
+ v-bind="props"
231
+ >
232
+ More actions
233
+ </v-btn>
234
+ </template>
235
+
236
+ <v-list class="pa-0">
237
+ <v-list-item v-if="canUpdate" @click="handleEdit">
238
+ <v-list-item-title class="text-subtitle-2">
239
+ Edit Announcement
240
+ </v-list-item-title>
241
+ </v-list-item>
242
+
243
+ <v-list-item
244
+ v-if="canDelete"
245
+ class="text-red"
246
+ @click="handleDelete"
247
+ >
248
+ <v-list-item-title class="text-subtitle-2">
249
+ Delete Announcement
250
+ </v-list-item-title>
251
+ </v-list-item>
252
+ </v-list>
253
+ </v-menu>
254
+ </v-col>
255
+ </v-row>
256
+ </v-toolbar>
257
+ </v-card>
184
258
  </template>
185
259
 
186
-
187
260
  <script setup lang="ts">
188
-
189
261
  const prop = defineProps({
190
- activeAnnouncement: {
191
- type: Object as PropType<TAnnouncement>,
192
- required: true
193
- },
194
- canUpdate: Boolean,
195
- canDelete: Boolean,
196
- })
262
+ activeAnnouncement: {
263
+ type: Object as PropType<TAnnouncement>,
264
+ required: true,
265
+ },
266
+ canUpdate: Boolean,
267
+ canDelete: Boolean,
268
+ });
197
269
 
198
- const processing = ref(false)
199
- const { recipientList } = useBulletin()
200
- const { getFileUrl, urlToFile } = useFile()
270
+ const processing = ref(false);
271
+ const { recipientList } = useBulletin();
272
+ const { getFileUrl, urlToFile } = useFile();
201
273
 
202
- const emit = defineEmits(['close', 'edit', 'delete'])
274
+ const emit = defineEmits(["close", "edit", "delete"]);
203
275
 
204
276
  /* Field labels */
205
277
  const formattedFields: Partial<Record<keyof TAnnouncement, string>> = {
206
- title: "Title",
207
- createdAt: "Date Created",
208
- recipients: "Recipients",
209
- content: "Content",
210
- startDate: "Start Date",
211
- endDate: "End Date",
212
- noExpiration: "No Expiration",
213
- }
278
+ title: "Title",
279
+ createdAt: "Date Created",
280
+ recipients: "Recipients",
281
+ content: "Content",
282
+ startDate: "Start Date",
283
+ endDate: "End Date",
284
+ noExpiration: "No Expiration",
285
+ };
214
286
 
215
287
  const formattedRecipients = computed(() => {
216
- const arr = recipientList.filter(x =>
217
- prop.activeAnnouncement?.recipients?.includes(x.value)
218
- )
219
- return arr.map(item => item.title).join(", ")
220
- })
288
+ const arr = recipientList.filter((x) =>
289
+ prop.activeAnnouncement?.recipients?.includes(x.value)
290
+ );
291
+ return arr.map((item) => item.title).join(", ");
292
+ });
221
293
 
222
294
  function getFieldValue(key: keyof TAnnouncement) {
223
- const value = prop.activeAnnouncement?.[key]
224
-
225
- if (value === null || value === "" || value === undefined) return ""
226
-
227
- switch (key) {
228
- case "recipients":
229
- return formattedRecipients.value
230
- case "createdAt":
231
- case "startDate":
232
- case "endDate":
233
- return toLocalDate(value as string)
234
- default:
235
- return value
236
- }
295
+ const value = prop.activeAnnouncement?.[key];
296
+
297
+ if (value === null || value === "" || value === undefined) return "";
298
+
299
+ switch (key) {
300
+ case "recipients":
301
+ return formattedRecipients.value;
302
+ case "createdAt":
303
+ case "startDate":
304
+ case "endDate":
305
+ return toLocalDate(value as string);
306
+ default:
307
+ return value;
308
+ }
237
309
  }
238
310
 
239
- const rawFileArray = ref<{ file: File, _id: string, preview: boolean }[]>([])
311
+ const rawFileArray = ref<{ file: File; _id: string; preview: boolean }[]>([]);
240
312
 
241
- const photosArray = getByType('image/')
242
- const videosArray = getByType('video/')
243
- const filesArray = getByType('other')
313
+ const photosArray = getByType("image/");
314
+ const videosArray = getByType("video/");
315
+ const filesArray = getByType("other");
244
316
 
245
317
  /* Viewer state */
246
- const showCarousel = ref(false)
247
- const showFileDialog = ref(false)
248
- const activeFileId = ref('')
249
- const activeFileName = ref('')
250
- const carouselFiles = ref<string[]>([])
251
-
252
- function openMediaViewer(id: string, _type: 'media') {
253
- activeFileId.value = id
254
- carouselFiles.value = [
255
- ...photosArray.value.map(x => x._id),
256
- ...videosArray.value.map(x => x._id),
257
- ]
258
- showCarousel.value = true
318
+ const showCarousel = ref(false);
319
+ const showFileDialog = ref(false);
320
+ const activeFileId = ref("");
321
+ const activeFileName = ref("");
322
+ const carouselFiles = ref<string[]>([]);
323
+
324
+ function openMediaViewer(id: string, _type: "media") {
325
+ activeFileId.value = id;
326
+ carouselFiles.value = [
327
+ ...photosArray.value.map((x) => x._id),
328
+ ...videosArray.value.map((x) => x._id),
329
+ ];
330
+ showCarousel.value = true;
259
331
  }
260
332
 
261
333
  function openFileViewer(id: string, name: string) {
262
- activeFileId.value = id
263
- activeFileName.value = name
264
- showFileDialog.value = true
334
+ activeFileId.value = id;
335
+ activeFileName.value = name;
336
+ showFileDialog.value = true;
265
337
  }
266
338
 
267
339
  function openInNewTab(id: string) {
268
- window.open(getFileUrl(id), '_blank', 'noopener,noreferrer')
340
+ window.open(getFileUrl(id), "_blank", "noopener,noreferrer");
269
341
  }
270
342
 
271
343
  function getFileIcon(mimeType: string): string {
272
- if (mimeType.startsWith('image/')) return 'mdi-image'
273
- if (mimeType.startsWith('video/')) return 'mdi-video'
274
- if (mimeType === 'application/pdf') return 'mdi-file-pdf-box'
275
- if (mimeType.includes('word') || mimeType.includes('document')) return 'mdi-file-word-box'
276
- if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'mdi-file-excel-box'
277
- if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'mdi-file-powerpoint-box'
278
- if (mimeType === 'text/plain') return 'mdi-file-document-outline'
279
- if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) return 'mdi-folder-zip-outline'
280
- return 'mdi-file-outline'
344
+ if (mimeType.startsWith("image/")) return "mdi-image";
345
+ if (mimeType.startsWith("video/")) return "mdi-video";
346
+ if (mimeType === "application/pdf") return "mdi-file-pdf-box";
347
+ if (mimeType.includes("word") || mimeType.includes("document"))
348
+ return "mdi-file-word-box";
349
+ if (mimeType.includes("excel") || mimeType.includes("spreadsheet"))
350
+ return "mdi-file-excel-box";
351
+ if (mimeType.includes("powerpoint") || mimeType.includes("presentation"))
352
+ return "mdi-file-powerpoint-box";
353
+ if (mimeType === "text/plain") return "mdi-file-document-outline";
354
+ if (
355
+ mimeType.includes("zip") ||
356
+ mimeType.includes("compressed") ||
357
+ mimeType.includes("archive")
358
+ )
359
+ return "mdi-folder-zip-outline";
360
+ return "mdi-file-outline";
281
361
  }
282
362
 
283
363
  function getFileIconColor(mimeType: string): string {
284
- if (mimeType === 'application/pdf') return 'red'
285
- if (mimeType.includes('word') || mimeType.includes('document')) return 'blue'
286
- if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'green'
287
- if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'orange'
288
- if (mimeType.startsWith('video/')) return 'purple'
289
- return 'grey'
364
+ if (mimeType === "application/pdf") return "red";
365
+ if (mimeType.includes("word") || mimeType.includes("document")) return "blue";
366
+ if (mimeType.includes("excel") || mimeType.includes("spreadsheet"))
367
+ return "green";
368
+ if (mimeType.includes("powerpoint") || mimeType.includes("presentation"))
369
+ return "orange";
370
+ if (mimeType.startsWith("video/")) return "purple";
371
+ return "grey";
290
372
  }
291
373
 
292
- function getByType(prefix: 'image/' | 'video/' | 'other') {
293
- return computed(() => {
294
- return rawFileArray.value.filter(x => {
295
- const type = x?.file?.type
296
- if (typeof type !== 'string') return false
297
-
298
- if (prefix === 'other') {
299
- return !type.startsWith('image/') && !type.startsWith('video/')
300
- }
301
- return type.startsWith(prefix)
302
- })
303
- })
374
+ function getByType(prefix: "image/" | "video/" | "other") {
375
+ return computed(() => {
376
+ return rawFileArray.value.filter((x) => {
377
+ const type = x?.file?.type;
378
+ if (typeof type !== "string") return false;
379
+
380
+ if (prefix === "other") {
381
+ return !type.startsWith("image/") && !type.startsWith("video/");
382
+ }
383
+ return type.startsWith(prefix);
384
+ });
385
+ });
304
386
  }
305
387
 
306
388
  function toLocalDate(utcString: string) {
307
- if (typeof utcString !== 'string') return;
308
- return new Date(utcString).toLocaleString("en-US", {
309
- year: "numeric",
310
- month: "2-digit",
311
- day: "2-digit",
312
- })
389
+ if (typeof utcString !== "string") return;
390
+ return new Date(utcString).toLocaleDateString("en-GB", {
391
+ year: "numeric",
392
+ month: "2-digit",
393
+ day: "2-digit",
394
+ });
313
395
  }
314
396
 
315
397
  function handleEdit() {
316
- emit("edit")
398
+ emit("edit");
317
399
  }
318
400
 
319
401
  function handleDelete() {
320
- emit("delete")
402
+ emit("delete");
321
403
  }
322
404
 
323
405
  watchEffect(async () => {
324
- const data = prop.activeAnnouncement
325
- if (!data) return
326
-
327
- rawFileArray.value = await Promise.all(
328
- data.file.map(async (item) => {
329
- const fileUrl = getFileUrl(item._id)
330
- const file = await urlToFile(fileUrl, item.name)
331
- return { file, _id: item._id, preview: item.preview ?? false }
332
- })
333
- )
334
- })
335
-
336
-
406
+ const data = prop.activeAnnouncement;
407
+ if (!data) return;
408
+
409
+ rawFileArray.value = await Promise.all(
410
+ data.file.map(async (item) => {
411
+ const fileUrl = getFileUrl(item._id);
412
+ const file = await urlToFile(fileUrl, item.name);
413
+ return { file, _id: item._id, preview: item.preview ?? false };
414
+ })
415
+ );
416
+ });
337
417
  </script>
@@ -3,7 +3,7 @@
3
3
  <v-toolbar>
4
4
  <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
5
5
  <span class="font-weight-bold text-h5 text-capitalize">
6
- {{ prop.mode }} {{ formatVisitorType(type) }}
6
+ {{ formMode }} {{ formatVisitorType(type) }}
7
7
  </span>
8
8
  <ButtonClose @click="emit('close:all')" />
9
9
  </v-row>
@@ -217,7 +217,7 @@
217
217
  :selected-nfc-cards="passType === 'NFC' ? passCards : []" />
218
218
  </v-col>
219
219
 
220
- <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
220
+ <v-col v-if="formMode === 'add'" cols="12" class="mt-2">
221
221
  <v-checkbox v-model="createMore" density="comfortable" hide-details>
222
222
  <template #label>
223
223
  <span class="text-subtitle-2 font-weight-bold">
@@ -239,11 +239,11 @@
239
239
  <v-row no-gutters>
240
240
  <v-col cols="6">
241
241
  <v-btn v-if="
242
- prop.mode === 'add' &&
242
+ formMode === 'add' &&
243
243
  (withStep2 || withStep3) &&
244
244
  contractorStep > 1
245
245
  " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
246
- <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
246
+ <v-btn v-else-if="formMode === 'add' || formMode === 'register'" tile block variant="text" class="text-none"
247
247
  size="48" @click="backToSelection" text="Back to Selection" />
248
248
  <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
249
249
  </v-col>
@@ -252,7 +252,7 @@
252
252
  :disabled="processing" @click="handleNextPage" :text="'Next'" />
253
253
  <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
254
254
  :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
255
- :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
255
+ :text="formMode == 'add' ? 'Submit' : 'Update'" />
256
256
  </v-col>
257
257
  </v-row>
258
258
  </v-toolbar>
@@ -297,6 +297,82 @@
297
297
  </v-card-text>
298
298
  </v-card>
299
299
  </v-dialog>
300
+
301
+ <v-dialog
302
+ v-if="enableUnregistered"
303
+ v-model="dialog.showUnregisteredPlateDialog"
304
+ max-width="900"
305
+ persistent
306
+ >
307
+ <v-card color="background" class="pa-4">
308
+ <v-card-actions class="justify-end pa-0">
309
+ <v-btn
310
+ icon="mdi-close"
311
+ variant="text"
312
+ size="large"
313
+ @click="closeUnregisteredPlateDialog"
314
+ />
315
+ </v-card-actions>
316
+
317
+ <v-card-text class="text-center pt-2">
318
+ <v-icon color="orange" size="96">mdi-alert-circle-outline</v-icon>
319
+
320
+ <div :class="`text-body-${$vuetify.display.xs ? 'mobile' : 'tablet'} mt-8`">
321
+ Vehicle Number {{ unregisteredPlateSearchText }} is similar to below
322
+ <br />
323
+ Unregistered visitors last 24 hours. Please select to
324
+ <br />
325
+ Update
326
+ </div>
327
+
328
+ <v-table class="mt-8 border rounded">
329
+ <thead>
330
+ <tr>
331
+ <th class="text-left">Plate Number</th>
332
+ <th class="text-left">Check In</th>
333
+ <th class="text-center">Action</th>
334
+ </tr>
335
+ </thead>
336
+ <tbody>
337
+ <tr
338
+ v-for="match in unregisteredPlateMatches"
339
+ :key="match._id || match.plateNumber"
340
+ >
341
+ <td class="text-left font-weight-bold text-uppercase">
342
+ {{ match.plateNumber || "N/A" }}
343
+ </td>
344
+ <td class="text-left">
345
+ {{ formatUnregisteredCheckIn(match.checkIn) }}
346
+ </td>
347
+ <td class="text-center">
348
+ <v-btn
349
+ color="red"
350
+ class="text-capitalize"
351
+ density="compact"
352
+ size="large"
353
+ @click="selectUnregisteredPlateMatch(match)"
354
+ >
355
+ Update
356
+ </v-btn>
357
+ </td>
358
+ </tr>
359
+ </tbody>
360
+ </v-table>
361
+ </v-card-text>
362
+
363
+ <v-card-actions>
364
+ <v-btn
365
+ block
366
+ color="red"
367
+ variant="flat"
368
+ size="x-large"
369
+ @click="closeUnregisteredPlateDialog"
370
+ >
371
+ Cancel & Add New
372
+ </v-btn>
373
+ </v-card-actions>
374
+ </v-card>
375
+ </v-dialog>
300
376
  </v-card>
301
377
  </template>
302
378
 
@@ -353,6 +429,7 @@ const {
353
429
  typeFieldMap,
354
430
  contractorTypes,
355
431
  getVisitors,
432
+ checkSearchUncheckedOutUnregisteredVisitor,
356
433
  updateVisitor,
357
434
  } = useVisitor();
358
435
  const { getBySiteId: getEntryPassSettingsBySiteId } =
@@ -408,10 +485,12 @@ const registeredUnitCompanyName = ref("N/A");
408
485
  const dialog = reactive({
409
486
  vehicleNumberUsersList: false,
410
487
  showNonCheckedOutDialog: false,
488
+ showUnregisteredPlateDialog: false,
411
489
  });
412
490
 
413
491
  const loading = reactive({
414
492
  checkingOut: false,
493
+ checkingUnregisteredPlate: false,
415
494
  });
416
495
 
417
496
  const validForm = ref(false);
@@ -437,9 +516,18 @@ const deliveryCompanyInput = ref("");
437
516
  const deliveryCompanyList = ref<string[]>([]);
438
517
 
439
518
  const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([]);
519
+ const unregisteredPlateMatches = ref<TVisitor[]>([]);
520
+ const unregisteredPlateSearchText = ref("");
521
+ const selectedUnregisteredVisitor = ref<TVisitor | null>(null);
522
+ const skipUnregisteredPlateCheck = ref(false);
523
+ const pendingUnregisteredAction = ref<"next" | "submit" | null>(null);
440
524
 
441
525
  const vehicleNumberUserItems = ref<TPeople[]>([]);
442
526
 
527
+ const formMode = computed<"add" | "edit" | "register">(() => {
528
+ return selectedUnregisteredVisitor.value ? "register" : prop.mode;
529
+ });
530
+
443
531
  const shouldShowField = (
444
532
  fieldKey: keyof TVisitorPayload | "delivery-company"
445
533
  ) => {
@@ -467,6 +555,10 @@ const showNextButton = computed(() => {
467
555
  } else return false;
468
556
  });
469
557
 
558
+ const enableUnregistered = computed(() => {
559
+ return prop.mode === "add" && shouldShowField("plateNumber");
560
+ });
561
+
470
562
  const showShowKeysField = computed(() => {
471
563
  return ["guest", "contractor", "walk-in"].includes(prop.type);
472
564
  });
@@ -696,12 +788,16 @@ const debounceSearchVisitorsByPlateNumbers = debounce(() => {
696
788
  fetchVisitorListByVehicleNumberRefresh();
697
789
  }, 300);
698
790
 
699
- watch(
700
- () => visitor.plateNumber,
701
- (newVal) => {
702
- debounceSearchVisitorsByPlateNumbers();
703
- }
704
- );
791
+ // watch(
792
+ // () => visitor.plateNumber,
793
+ // (newVal) => {
794
+ // skipUnregisteredPlateCheck.value = false;
795
+ // selectedUnregisteredVisitor.value = null;
796
+ // unregisteredPlateMatches.value = [];
797
+ // dialog.showUnregisteredPlateDialog = false;
798
+ // debounceSearchVisitorsByPlateNumbers();
799
+ // }
800
+ // );
705
801
 
706
802
  async function handleCheckout(visitorId: string) {
707
803
  if (!visitorId) {
@@ -988,6 +1084,95 @@ function formatVisitorType(type: TVisitorType): string {
988
1084
  }
989
1085
  }
990
1086
 
1087
+ async function checkUnregisteredPlateBeforeContinue(action: "next" | "submit") {
1088
+ if (
1089
+ !enableUnregistered.value ||
1090
+ skipUnregisteredPlateCheck.value ||
1091
+ selectedUnregisteredVisitor.value ||
1092
+ !visitor.plateNumber
1093
+ ) {
1094
+ return true;
1095
+ }
1096
+
1097
+ try {
1098
+ loading.checkingUnregisteredPlate = true;
1099
+ unregisteredPlateSearchText.value = visitor.plateNumber;
1100
+ const res = await checkSearchUncheckedOutUnregisteredVisitor({
1101
+ site: prop.site,
1102
+ plateNumber: visitor.plateNumber,
1103
+ hourAgo: 24,
1104
+ page: 1,
1105
+ limit: 100,
1106
+ });
1107
+ const items = Array.isArray(res?.items)
1108
+ ? res.items
1109
+ : Array.isArray(res?.data?.items)
1110
+ ? res.data.items
1111
+ : Array.isArray(res?.data)
1112
+ ? res.data
1113
+ : Array.isArray(res)
1114
+ ? res
1115
+ : [];
1116
+
1117
+ unregisteredPlateMatches.value = items;
1118
+ if (items.length === 0) return true;
1119
+
1120
+ pendingUnregisteredAction.value = action;
1121
+ dialog.showUnregisteredPlateDialog = true;
1122
+ return false;
1123
+ } catch (error: any) {
1124
+ errorMessage.value =
1125
+ error?.data?.message ||
1126
+ error?.response?._data?.message ||
1127
+ "Unable to check unregistered visitors. Please try again.";
1128
+ return false;
1129
+ } finally {
1130
+ loading.checkingUnregisteredPlate = false;
1131
+ }
1132
+ }
1133
+
1134
+ function formatUnregisteredCheckIn(checkIn?: string) {
1135
+ return checkIn ? UTCToLocalTIme(checkIn) : "N/A";
1136
+ }
1137
+
1138
+ async function continuePendingUnregisteredAction() {
1139
+ const action = pendingUnregisteredAction.value;
1140
+ pendingUnregisteredAction.value = null;
1141
+ if (action === "next") {
1142
+ await handleNextPage();
1143
+ } else if (action === "submit") {
1144
+ await submit();
1145
+ }
1146
+ }
1147
+
1148
+ async function closeUnregisteredPlateDialog() {
1149
+ dialog.showUnregisteredPlateDialog = false;
1150
+ unregisteredPlateMatches.value = [];
1151
+ skipUnregisteredPlateCheck.value = true;
1152
+ }
1153
+
1154
+ async function selectUnregisteredPlateMatch(match: TVisitor) {
1155
+ selectedUnregisteredVisitor.value = match;
1156
+ skipUnregisteredPlateCheck.value = true;
1157
+ dialog.showUnregisteredPlateDialog = false;
1158
+ unregisteredPlateMatches.value = [];
1159
+
1160
+ visitor.plateNumber = match.plateNumber;
1161
+ visitor.attachments = match.attachments || visitor.attachments;
1162
+ visitor.block = match.block || visitor.block;
1163
+ visitor.level = match.level || visitor.level;
1164
+ visitor.unit = match.unit || visitor.unit;
1165
+ visitor.company = match.company || visitor.company;
1166
+ visitor.contractorType = match.contractorType || visitor.contractorType;
1167
+ visitor.name = match.name || visitor.name;
1168
+ visitor.contact = match.contact || visitor.contact;
1169
+ visitor.nric = match.nric || visitor.nric;
1170
+ visitor.company = match.company || visitor.company;
1171
+ visitor.remarks = match.remarks || visitor.remarks;
1172
+
1173
+ pendingUnregisteredAction.value = null;
1174
+ }
1175
+
991
1176
  async function handleNextPage() {
992
1177
  errorMessage.value = "";
993
1178
  formRef.value!.validate();
@@ -1010,6 +1195,7 @@ async function handleNextPage() {
1010
1195
  errorMessage.value = "Please select at least one NFC card before proceeding.";
1011
1196
  return;
1012
1197
  }
1198
+ if (!(await checkUnregisteredPlateBeforeContinue("next"))) return;
1013
1199
  if (step < 3) contractorStep.value++;
1014
1200
  else await submit();
1015
1201
  }
@@ -1023,6 +1209,7 @@ async function submit() {
1023
1209
  }
1024
1210
 
1025
1211
  if (passType.value === "HID_QR" && !validateHidQrPrinterConfig()) return;
1212
+ if (!(await checkUnregisteredPlateBeforeContinue("submit"))) return;
1026
1213
 
1027
1214
  processing.value = true;
1028
1215
 
@@ -1033,7 +1220,7 @@ async function submit() {
1033
1220
  createdBy: prop.currentUser,
1034
1221
  };
1035
1222
 
1036
- if (prop.mode === "add" || prop.mode === "register") {
1223
+ if (formMode.value === "add" || formMode.value === "register") {
1037
1224
  const allowedFields = typeFieldMap[prop.type];
1038
1225
  for (const key of allowedFields as (keyof TVisitorPayload)[]) {
1039
1226
  if (visitor[key] !== undefined) {
@@ -1071,7 +1258,7 @@ async function submit() {
1071
1258
  }
1072
1259
 
1073
1260
  // add register status when registering unregistered visitor
1074
- if (prop.mode === "register") {
1261
+ if (formMode.value === "register") {
1075
1262
  payload.status = "registered";
1076
1263
  const currentUserId = currentUser.value?._id;
1077
1264
  if(currentUserId) {
@@ -1081,7 +1268,18 @@ async function submit() {
1081
1268
 
1082
1269
  try {
1083
1270
  let res;
1084
- if (prop.mode === "add") {
1271
+ if (prop.mode === "add" && selectedUnregisteredVisitor.value?._id) {
1272
+ payload.status = "registered";
1273
+ const currentUserId = currentUser.value?._id;
1274
+ if (currentUserId) {
1275
+ payload.updatedBy = currentUserId;
1276
+ }
1277
+ const { unitName, ...rest } = payload;
1278
+ res = await updateVisitor(
1279
+ selectedUnregisteredVisitor.value._id,
1280
+ rest as TVisitorPayload
1281
+ );
1282
+ } else if (formMode.value === "add") {
1085
1283
  res = await createVisitor(payload as TVisitorPayload);
1086
1284
  } else if (
1087
1285
  (prop.mode === "edit" || prop.mode === "register") &&
@@ -1224,7 +1422,7 @@ async function submit() {
1224
1422
  const err = error?.data?.message;
1225
1423
  errorMessage.value =
1226
1424
  err ||
1227
- `Failed to ${prop.mode === "add" ? "add" : "update"
1425
+ `Failed to ${formMode.value === "add" ? "add" : "update"
1228
1426
  } visitor. Please try again.`;
1229
1427
  } finally {
1230
1428
  processing.value = false;
@@ -1368,7 +1566,22 @@ async function printHidQrCodePass(visitorId = "", manageProcessing = true) {
1368
1566
  watch(
1369
1567
  () => visitor.plateNumber,
1370
1568
  (newVal) => {
1371
- visitor.plateNumber = newVal?.toUpperCase();
1569
+ const normalizedPlateNumber = newVal?.toUpperCase();
1570
+ if (newVal !== normalizedPlateNumber) {
1571
+ visitor.plateNumber = normalizedPlateNumber;
1572
+ return;
1573
+ }
1574
+
1575
+ if (
1576
+ selectedUnregisteredVisitor.value &&
1577
+ normalizedPlateNumber !== selectedUnregisteredVisitor.value.plateNumber?.toUpperCase()
1578
+ ) {
1579
+ selectedUnregisteredVisitor.value = null;
1580
+ }
1581
+
1582
+ skipUnregisteredPlateCheck.value = false;
1583
+ unregisteredPlateMatches.value = [];
1584
+ dialog.showUnregisteredPlateDialog = false;
1372
1585
  }
1373
1586
  );
1374
1587
 
@@ -94,6 +94,14 @@ export default function () {
94
94
  tab?: string;
95
95
  };
96
96
 
97
+ type SearchUncheckedOutUnregisteredVisitorParams = {
98
+ site: string;
99
+ plateNumber: string;
100
+ hourAgo?: number;
101
+ page?: number;
102
+ limit?: number;
103
+ };
104
+
97
105
  const tab = useState("visitorsTab", () => "visitor");
98
106
 
99
107
  async function getVisitors({
@@ -159,6 +167,26 @@ export default function () {
159
167
  );
160
168
  }
161
169
 
170
+ async function checkSearchUncheckedOutUnregisteredVisitor({
171
+ site,
172
+ plateNumber,
173
+ hourAgo = 24,
174
+ page = 1,
175
+ limit = 100,
176
+ }: SearchUncheckedOutUnregisteredVisitorParams) {
177
+ return await useNuxtApp().$api<Record<string, any>>("/api/visitors", {
178
+ method: "GET",
179
+ query: {
180
+ site,
181
+ type: "uncheckout-unregistered",
182
+ plateNumber,
183
+ hourAgo,
184
+ page,
185
+ limit,
186
+ },
187
+ });
188
+ }
189
+
162
190
  async function deleteVisitor(_id: string) {
163
191
  return await useNuxtApp().$api<Record<string, any>>(
164
192
  `/api/visitor-transactions/id/${_id}`,
@@ -198,6 +226,7 @@ export default function () {
198
226
  contractorTypes,
199
227
  createVisitor,
200
228
  getVisitors,
229
+ checkSearchUncheckedOutUnregisteredVisitor,
201
230
  updateVisitor,
202
231
  deleteVisitor,
203
232
  validateVisitorQrCode,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.27",
5
+ "version": "3.0.28",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {