@7365admin1/layer-common 1.10.1 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/AccessCard/AvailableStats.vue +55 -0
- package/components/AccessCardAddForm.vue +185 -8
- package/components/AccessCardAssignToUnitForm.vue +440 -0
- package/components/AccessManagement.vue +106 -56
- package/components/AreaMain.vue +26 -4
- package/components/BulletinBoardForm.vue +396 -0
- package/components/BulletinBoardManagement.vue +322 -0
- package/components/BulletinBoardView.vue +195 -0
- package/components/BulletinExpirationChip.vue +13 -0
- package/components/SignaturePad.vue +17 -5
- package/components/SupplyManagementMain.vue +1 -1
- package/composables/useAccessManagement.ts +73 -0
- package/composables/useBulletin.ts +82 -0
- package/package.json +1 -1
- package/types/bulletin-board.d.ts +29 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<TableMain :headers="headers" :items="paginatePlaceholderItem" v-model:search="searchInput"
|
|
4
|
+
:loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
|
|
5
|
+
@refresh="getAnnouncementsRefresh" show-header @update:page="handleUpdatePage" @row-click="handleRowClick"
|
|
6
|
+
@create="handleCreateEvent" :can-create="canCreateBulletinBoard" create-label="Add Announcement">
|
|
7
|
+
<template #extension>
|
|
8
|
+
<v-row no-gutters class="w-100 d-flex flex-column">
|
|
9
|
+
<v-tabs v-model="status" color="primary" :height="40" @update:model-value="toRoute" class="w-100">
|
|
10
|
+
<v-tab v-for="tab in tabOptions" :value="tab.status" :key="tab.status" class="text-capitalize">
|
|
11
|
+
{{ tab.name }}
|
|
12
|
+
</v-tab>
|
|
13
|
+
</v-tabs>
|
|
14
|
+
</v-row>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<template v-slot:item.createdAt="{ item }">
|
|
18
|
+
<span class="d-flex align-center ga-2">
|
|
19
|
+
<v-icon icon="mdi-calendar-start" color="green" size="20" />
|
|
20
|
+
<span class="text-capitalize">{{ toLocalDate(item.createdAt) || "-" }}</span>
|
|
21
|
+
</span>
|
|
22
|
+
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<template v-slot:item.noExpiration="{ item }">
|
|
26
|
+
<BulletinExpirationChip :value="item?.noExpiration" />
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<template v-slot:item.duration="{ item }">
|
|
30
|
+
<span class="d-flex align-center ga-2">
|
|
31
|
+
<v-icon icon="mdi-calendar-start" color="green" size="20" />
|
|
32
|
+
<span class="text-capitalize">{{ toLocalDate(item.startDate) || "-" }}</span>
|
|
33
|
+
</span>
|
|
34
|
+
<span class="d-flex align-center ga-2">
|
|
35
|
+
<v-icon icon="mdi-calendar-end" color="red" size="20" />
|
|
36
|
+
<span class="text-capitalize">{{ toLocalDate(item.endDate) || "_" }}</span>
|
|
37
|
+
</span>
|
|
38
|
+
</template>
|
|
39
|
+
</TableMain>
|
|
40
|
+
|
|
41
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" style="z-index: 3000;" />
|
|
42
|
+
|
|
43
|
+
<v-dialog v-model="dialog.showForm" width="600" persistent>
|
|
44
|
+
<BulletinBoardForm :mode="mode" :site-id="siteId" :active-id="selectedAnnouncementId"
|
|
45
|
+
@close="dialog.showForm = false" @done="handleDone" />
|
|
46
|
+
</v-dialog>
|
|
47
|
+
|
|
48
|
+
<v-dialog v-model="dialog.moreActions" v-if="activeAnnouncementObj && canViewBulletinBoardDetails" width="600"
|
|
49
|
+
persistent>
|
|
50
|
+
<BulletinBoardView :active-announcement="activeAnnouncementObj" :can-delete="canDeleteBulletinBoard"
|
|
51
|
+
:can-update="canUpdateBulletinBoard" @edit="handleEditAnnouncement" @close="dialog.moreActions = false"
|
|
52
|
+
@delete="dialog.deletePrompt = true" />
|
|
53
|
+
</v-dialog>
|
|
54
|
+
|
|
55
|
+
<v-dialog v-model="dialog.deletePrompt" width="450" persistent>
|
|
56
|
+
<CardDeleteConfirmation prompt-title="Are you sure want to delete this announcement?"
|
|
57
|
+
:loading="loading.deletingAnnouncement" @close="dialog.deletePrompt = false"
|
|
58
|
+
@delete="handleProceedDeleteAnnouncement" />
|
|
59
|
+
</v-dialog>
|
|
60
|
+
</v-row>
|
|
61
|
+
</template>
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
definePageMeta({
|
|
64
|
+
memberOnly: true,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const props = defineProps({
|
|
68
|
+
siteId: {
|
|
69
|
+
type: String,
|
|
70
|
+
required: true
|
|
71
|
+
},
|
|
72
|
+
canCreateBulletinBoard: {
|
|
73
|
+
type: Boolean,
|
|
74
|
+
default: true
|
|
75
|
+
},
|
|
76
|
+
canUpdateBulletinBoard: {
|
|
77
|
+
type: Boolean,
|
|
78
|
+
default: true
|
|
79
|
+
},
|
|
80
|
+
canViewBulletinBoardDetails: {
|
|
81
|
+
type: Boolean,
|
|
82
|
+
default: true
|
|
83
|
+
}, canDeleteBulletinBoard: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: true
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
const { authenticate } = useLocalAuth();
|
|
91
|
+
authenticate();
|
|
92
|
+
|
|
93
|
+
const route = useRoute();
|
|
94
|
+
const orgId = route.params.org as string;
|
|
95
|
+
const siteId = props.siteId;
|
|
96
|
+
const routeName = (useRoute().name as string) ?? "";
|
|
97
|
+
const { getAll, deleteBulletinById } = useBulletin()
|
|
98
|
+
const { debounce } = useUtils()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
const headers = [
|
|
103
|
+
{ title: "Title", value: "title" },
|
|
104
|
+
{ title: "Date Created", value: "createdAt", align: "start" },
|
|
105
|
+
{ title: "Start Date/End Date", value: "duration", align: "start" },
|
|
106
|
+
{ title: "No Expiration", value: "noExpiration", align: "start" },
|
|
107
|
+
{ title: "", value: "actions" },
|
|
108
|
+
];
|
|
109
|
+
const items = ref<TAnnouncement[]>([]);
|
|
110
|
+
const page = ref(1);
|
|
111
|
+
const pages = ref(0);
|
|
112
|
+
const pageRange = ref("-- - -- of --");
|
|
113
|
+
const selectedAnnouncementId = ref<string>("")
|
|
114
|
+
const mode = ref<'add' | 'edit'>('add')
|
|
115
|
+
const status = ref<TAnnouncementStatus>()
|
|
116
|
+
const searchInput = ref('')
|
|
117
|
+
|
|
118
|
+
const dialog = reactive({
|
|
119
|
+
showForm: false,
|
|
120
|
+
moreActions: false,
|
|
121
|
+
viewDetails: false,
|
|
122
|
+
deletePrompt: false,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const loading = reactive({
|
|
126
|
+
deletingAnnouncement: false
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const messageSnackbar = ref(false)
|
|
130
|
+
const message = ref('')
|
|
131
|
+
const messageColor = ref<'success' | 'error' | 'info'>()
|
|
132
|
+
|
|
133
|
+
const activeAnnouncementObj = computed(() => {
|
|
134
|
+
return items.value.find(x => x._id === selectedAnnouncementId.value)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const paginatePlaceholderItem = computed(() => {
|
|
138
|
+
const pageSize = 10
|
|
139
|
+
const total = items.value.length
|
|
140
|
+
const currentPage = page.value
|
|
141
|
+
|
|
142
|
+
const start = (currentPage - 1) * pageSize
|
|
143
|
+
const end = start + pageSize
|
|
144
|
+
|
|
145
|
+
const from = total === 0 ? 0 : start + 1
|
|
146
|
+
const to = Math.min(end, total)
|
|
147
|
+
pageRange.value = `${from}-${to} of ${total}`
|
|
148
|
+
|
|
149
|
+
pages.value = Math.ceil(total / pageSize)
|
|
150
|
+
|
|
151
|
+
return items.value.slice(start, end)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
const {
|
|
156
|
+
data: getAnnouncementReq,
|
|
157
|
+
refresh: getAnnouncementsRefresh,
|
|
158
|
+
pending: getAnnouncementPending,
|
|
159
|
+
} = await useLazyAsyncData(
|
|
160
|
+
`get-all-announcements-${page.value}`,
|
|
161
|
+
() => getAll({ page: page.value, site: siteId, status: status.value }),
|
|
162
|
+
{
|
|
163
|
+
watch: [page, () => route.query],
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
watch(getAnnouncementReq, (newVal) => {
|
|
168
|
+
items.value = newVal?.items || [];
|
|
169
|
+
pages.value = newVal?.pages || 0;
|
|
170
|
+
pageRange.value = newVal?.pageRange || "-- - -- of --";
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
function eventStatusFormat(status: any) {
|
|
175
|
+
switch (status) {
|
|
176
|
+
case 'In Progress':
|
|
177
|
+
return {
|
|
178
|
+
color: 'orange',
|
|
179
|
+
text: 'In Progress'
|
|
180
|
+
};
|
|
181
|
+
case 'Active':
|
|
182
|
+
return {
|
|
183
|
+
color: 'green',
|
|
184
|
+
text: status
|
|
185
|
+
}
|
|
186
|
+
case 'Scheduled':
|
|
187
|
+
return {
|
|
188
|
+
color: 'blue',
|
|
189
|
+
text: status
|
|
190
|
+
}
|
|
191
|
+
default:
|
|
192
|
+
return {
|
|
193
|
+
color: 'grey',
|
|
194
|
+
text: status
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tabOptions = [
|
|
200
|
+
{ name: "Active", status: "active" },
|
|
201
|
+
{ name: "Expired", status: "expired" },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
function toRoute(status: any) {
|
|
205
|
+
const obj = tabOptions.find((x) => x.status === status);
|
|
206
|
+
if (!obj) return;
|
|
207
|
+
page.value = 1
|
|
208
|
+
navigateTo({
|
|
209
|
+
name: routeName,
|
|
210
|
+
params: {
|
|
211
|
+
org: orgId,
|
|
212
|
+
},
|
|
213
|
+
query: {
|
|
214
|
+
status: obj.status,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function toLocalDate(utcString: string) {
|
|
220
|
+
if (!utcString) return ""
|
|
221
|
+
return new Date(utcString).toLocaleString("en-US", {
|
|
222
|
+
year: "numeric",
|
|
223
|
+
month: "2-digit",
|
|
224
|
+
day: "2-digit",
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleRowClick(data: any) {
|
|
229
|
+
selectedAnnouncementId.value = data?.item?._id;
|
|
230
|
+
dialog.moreActions = true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleUpdatePage(newPageNum: number) {
|
|
234
|
+
page.value = newPageNum;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleClose() {
|
|
238
|
+
dialog.showForm = false;
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleFormDone() {
|
|
243
|
+
getAnnouncementsRefresh();
|
|
244
|
+
dialog.showForm = false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function handleFormCreateMore() {
|
|
248
|
+
getAnnouncementsRefresh();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleCreateEvent() {
|
|
252
|
+
selectedAnnouncementId.value = ""
|
|
253
|
+
mode.value = 'add'
|
|
254
|
+
dialog.showForm = true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function handleDeleteAnnouncement() {
|
|
258
|
+
dialog.moreActions = false;
|
|
259
|
+
dialog.showForm = false;
|
|
260
|
+
dialog.deletePrompt = true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleEditAnnouncement() {
|
|
264
|
+
dialog.moreActions = false
|
|
265
|
+
mode.value = "edit"
|
|
266
|
+
dialog.showForm = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function showMessage(text: string, type: "error" | 'success' | 'info') {
|
|
270
|
+
messageSnackbar.value = true;
|
|
271
|
+
message.value = text;
|
|
272
|
+
messageColor.value = type
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async function handleProceedDeleteAnnouncement() {
|
|
280
|
+
try {
|
|
281
|
+
loading.deletingAnnouncement = true;
|
|
282
|
+
const bulletinId = selectedAnnouncementId.value;
|
|
283
|
+
const res = await deleteBulletinById(bulletinId as string);
|
|
284
|
+
if (res) {
|
|
285
|
+
showMessage("Announcement successfully deleted!", "info");
|
|
286
|
+
await getAnnouncementsRefresh();
|
|
287
|
+
dialog.deletePrompt = false;
|
|
288
|
+
}
|
|
289
|
+
} catch (error: any) {
|
|
290
|
+
const errorMessage = error?.response?._data?.message;
|
|
291
|
+
console.log("[ERROR]", error);
|
|
292
|
+
showMessage(
|
|
293
|
+
errorMessage || "Something went wrong. Please try again later.",
|
|
294
|
+
"error"
|
|
295
|
+
);
|
|
296
|
+
} finally {
|
|
297
|
+
loading.deletingAnnouncement = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async function handleDone() {
|
|
305
|
+
await getAnnouncementsRefresh()
|
|
306
|
+
dialog.showForm = false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
const debounceSearch = debounce(getAnnouncementsRefresh, 500)
|
|
311
|
+
|
|
312
|
+
watch([searchInput], ([search]) => {
|
|
313
|
+
debounceSearch()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
onMounted(() => {
|
|
318
|
+
const statusQuery = (useRoute()?.query?.status)
|
|
319
|
+
status.value = (statusQuery === "active" || statusQuery === "expired") ? statusQuery : "active"
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
</script>
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
|
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 :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 class="font-weight-bold text-subtitle-1">Attachments:</v-row>
|
|
35
|
+
<v-row no-gutters class="">
|
|
36
|
+
<template v-if="videosArray.length > 0">
|
|
37
|
+
<v-col cols="12">Videos ({{ videosArray.length }})</v-col>
|
|
38
|
+
<AttachmentList :list="videosArray" show-checkbox read-only />
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<template v-if="photosArray.length > 0">
|
|
42
|
+
<v-col cols="12" class="mt-1">Photos ({{ photosArray.length }})</v-col>
|
|
43
|
+
<AttachmentList :list="photosArray" show-checkbox read-only />
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template v-if="filesArray.length > 0">
|
|
47
|
+
<v-col cols="12" class="mt-1">Files ({{ filesArray.length }})</v-col>
|
|
48
|
+
<AttachmentList :list="filesArray" show-checkbox read-only />
|
|
49
|
+
</template>
|
|
50
|
+
</v-row>
|
|
51
|
+
</v-col>
|
|
52
|
+
</v-row>
|
|
53
|
+
</v-card-text>
|
|
54
|
+
|
|
55
|
+
<v-toolbar class="pa-0" density="compact">
|
|
56
|
+
<v-row no-gutters>
|
|
57
|
+
<v-col cols="6" class="pa-0">
|
|
58
|
+
<v-btn block variant="text" height="48" class="text-none" @click="emit('close')">
|
|
59
|
+
Close
|
|
60
|
+
</v-btn>
|
|
61
|
+
</v-col>
|
|
62
|
+
|
|
63
|
+
<v-col cols="6" class="pa-0">
|
|
64
|
+
<v-menu contained>
|
|
65
|
+
<template #activator="{ props }">
|
|
66
|
+
<v-btn block variant="flat" color="black" height="48" class="text-none" tile v-bind="props">
|
|
67
|
+
More actions
|
|
68
|
+
</v-btn>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<v-list class="pa-0">
|
|
72
|
+
<v-list-item v-if="canUpdate" @click="handleEdit">
|
|
73
|
+
<v-list-item-title class="text-subtitle-2">
|
|
74
|
+
Edit Announcement
|
|
75
|
+
</v-list-item-title>
|
|
76
|
+
</v-list-item>
|
|
77
|
+
|
|
78
|
+
<v-list-item v-if="canDelete" class="text-red" @click="handleDelete">
|
|
79
|
+
<v-list-item-title class="text-subtitle-2">
|
|
80
|
+
Delete Announcement
|
|
81
|
+
</v-list-item-title>
|
|
82
|
+
</v-list-item>
|
|
83
|
+
</v-list>
|
|
84
|
+
</v-menu>
|
|
85
|
+
</v-col>
|
|
86
|
+
</v-row>
|
|
87
|
+
</v-toolbar>
|
|
88
|
+
</v-card>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
<script setup lang="ts">
|
|
93
|
+
|
|
94
|
+
const prop = defineProps({
|
|
95
|
+
activeAnnouncement: {
|
|
96
|
+
type: Object as PropType<TAnnouncement>,
|
|
97
|
+
required: true
|
|
98
|
+
},
|
|
99
|
+
canUpdate: Boolean,
|
|
100
|
+
canDelete: Boolean,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const processing = ref(false)
|
|
104
|
+
const { recipientList } = useBulletin()
|
|
105
|
+
const { getFileUrl, urlToFile } = useFile()
|
|
106
|
+
|
|
107
|
+
const emit = defineEmits(['close', 'edit', 'delete'])
|
|
108
|
+
|
|
109
|
+
/* Field labels */
|
|
110
|
+
const formattedFields: Partial<Record<keyof TAnnouncement, string>> = {
|
|
111
|
+
title: "Title",
|
|
112
|
+
createdAt: "Date Created",
|
|
113
|
+
recipients: "Recipients",
|
|
114
|
+
content: "Content",
|
|
115
|
+
startDate: "Start Date",
|
|
116
|
+
endDate: "End Date",
|
|
117
|
+
noExpiration: "No Expiration",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const formattedRecipients = computed(() => {
|
|
121
|
+
const arr = recipientList.filter(x =>
|
|
122
|
+
prop.activeAnnouncement?.recipients?.includes(x.value)
|
|
123
|
+
)
|
|
124
|
+
return arr.map(item => item.title).join(", ")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
function getFieldValue(key: keyof TAnnouncement) {
|
|
128
|
+
const value = prop.activeAnnouncement?.[key]
|
|
129
|
+
|
|
130
|
+
if (value === null || value === "" || value === undefined) return ""
|
|
131
|
+
|
|
132
|
+
switch (key) {
|
|
133
|
+
case "recipients":
|
|
134
|
+
return formattedRecipients.value
|
|
135
|
+
case "createdAt":
|
|
136
|
+
case "startDate":
|
|
137
|
+
case "endDate":
|
|
138
|
+
return toLocalDate(value as string)
|
|
139
|
+
default:
|
|
140
|
+
return value
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rawFileArray = ref<{ file: File, _id: string, preview: boolean }[]>([])
|
|
145
|
+
|
|
146
|
+
const photosArray = getByType('image/')
|
|
147
|
+
const videosArray = getByType('video/')
|
|
148
|
+
const filesArray = getByType('other')
|
|
149
|
+
|
|
150
|
+
function getByType(prefix: 'image/' | 'video/' | 'other') {
|
|
151
|
+
return computed(() => {
|
|
152
|
+
return rawFileArray.value.filter(x => {
|
|
153
|
+
const type = x?.file?.type
|
|
154
|
+
if (typeof type !== 'string') return false
|
|
155
|
+
|
|
156
|
+
if (prefix === 'other') {
|
|
157
|
+
return !type.startsWith('image/') && !type.startsWith('video/')
|
|
158
|
+
}
|
|
159
|
+
return type.startsWith(prefix)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toLocalDate(utcString: string) {
|
|
165
|
+
if (typeof utcString !== 'string') return;
|
|
166
|
+
return new Date(utcString).toLocaleString("en-US", {
|
|
167
|
+
year: "numeric",
|
|
168
|
+
month: "2-digit",
|
|
169
|
+
day: "2-digit",
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleEdit() {
|
|
174
|
+
emit("edit")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleDelete() {
|
|
178
|
+
emit("delete")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
watchEffect(async () => {
|
|
182
|
+
const data = prop.activeAnnouncement
|
|
183
|
+
if (!data) return
|
|
184
|
+
|
|
185
|
+
rawFileArray.value = await Promise.all(
|
|
186
|
+
data.file.map(async (item) => {
|
|
187
|
+
const fileUrl = getFileUrl(item._id)
|
|
188
|
+
const file = await urlToFile(fileUrl, item.name)
|
|
189
|
+
return { file, _id: item._id, preview: item.preview ?? false }
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
</script>
|
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
import { ref, defineProps, defineEmits } from "vue";
|
|
28
28
|
import Vue3Signature from "vue3-signature";
|
|
29
29
|
|
|
30
|
-
defineProps({
|
|
30
|
+
const props = defineProps({
|
|
31
31
|
modelValue: String,
|
|
32
|
+
hideToast: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false,
|
|
35
|
+
}
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
const emit = defineEmits(["update:modelValue"]);
|
|
@@ -53,21 +57,29 @@ function showMessage(msg: string, color: string) {
|
|
|
53
57
|
|
|
54
58
|
function onSave(data: string) {
|
|
55
59
|
emit("update:modelValue", data);
|
|
56
|
-
|
|
60
|
+
if(!props.hideToast) {
|
|
61
|
+
showMessage("Signature saved successfully.", "success");
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
function save() {
|
|
60
66
|
const signatureData = signatureRef.value?.save("image/jpeg");
|
|
61
67
|
if (signatureData) {
|
|
62
68
|
emit("update:modelValue", signatureData);
|
|
63
|
-
|
|
69
|
+
if(!props.hideToast) {
|
|
70
|
+
showMessage("Signature saved successfully.", "success");
|
|
71
|
+
}
|
|
64
72
|
} else {
|
|
65
|
-
|
|
73
|
+
if(!props.hideToast) {
|
|
74
|
+
showMessage("No signature to save.", "error");
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
function clear() {
|
|
70
80
|
signatureRef.value?.clear();
|
|
71
|
-
|
|
81
|
+
if(!props.hideToast) {
|
|
82
|
+
showMessage("Signature cleared.", "success");
|
|
83
|
+
}
|
|
72
84
|
}
|
|
73
85
|
</script>
|
|
@@ -55,6 +55,22 @@ export default function useAccessManagement() {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function getAllAccessCardsCounts(params: {
|
|
59
|
+
site: string;
|
|
60
|
+
userType: string;
|
|
61
|
+
}) {
|
|
62
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
63
|
+
`/api/access-management/all-access-cards-counts`,
|
|
64
|
+
{
|
|
65
|
+
method: "GET",
|
|
66
|
+
query: {
|
|
67
|
+
site: params.site,
|
|
68
|
+
userType: params.userType,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
function getUserTypeAccessCards(params: {
|
|
59
75
|
page?: number;
|
|
60
76
|
limit?: number;
|
|
@@ -79,6 +95,59 @@ export default function useAccessManagement() {
|
|
|
79
95
|
);
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
function bulkPhysicalAccessCard(params: { site: string; file: File }) {
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.append("file", params.file);
|
|
101
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
102
|
+
`/api/access-management/bulk-upload`,
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
query: { site: params.site },
|
|
106
|
+
body: formData,
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getAvailableAccessCards(params: {
|
|
112
|
+
site: string;
|
|
113
|
+
userType: string;
|
|
114
|
+
type: string;
|
|
115
|
+
accessLevel: string;
|
|
116
|
+
liftAccessLevel: string;
|
|
117
|
+
}) {
|
|
118
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
119
|
+
`/api/access-management/access-and-lift-cards`,
|
|
120
|
+
{
|
|
121
|
+
method: "GET",
|
|
122
|
+
query: {
|
|
123
|
+
site: params.site,
|
|
124
|
+
userType: params.userType,
|
|
125
|
+
type: params.type,
|
|
126
|
+
accessLevel: params.accessLevel,
|
|
127
|
+
liftAccessLevel: params.liftAccessLevel,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function assignAccessCard(payload: {
|
|
134
|
+
units: string[];
|
|
135
|
+
quantity: number;
|
|
136
|
+
type: string;
|
|
137
|
+
site: string;
|
|
138
|
+
userType: string;
|
|
139
|
+
accessLevel: string;
|
|
140
|
+
liftAccessLevel: string;
|
|
141
|
+
}) {
|
|
142
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
143
|
+
`/api/access-management/assign-access-card`,
|
|
144
|
+
{
|
|
145
|
+
method: "POST",
|
|
146
|
+
body: payload,
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
82
151
|
return {
|
|
83
152
|
getDoorAccessLevels,
|
|
84
153
|
getLiftAccessLevels,
|
|
@@ -86,5 +155,9 @@ export default function useAccessManagement() {
|
|
|
86
155
|
addPhysicalCard,
|
|
87
156
|
addNonPhysicalCard,
|
|
88
157
|
getUserTypeAccessCards,
|
|
158
|
+
getAllAccessCardsCounts,
|
|
159
|
+
bulkPhysicalAccessCard,
|
|
160
|
+
assignAccessCard,
|
|
161
|
+
getAvailableAccessCards,
|
|
89
162
|
};
|
|
90
163
|
}
|