@7365admin1/layer-common 1.10.2 → 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
CHANGED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" :loading="loading.processing" :disabled="loading.processing || getAnnouncementByIdPending"
|
|
3
|
+
:key="`form-key-${formKey}`">
|
|
4
|
+
<v-toolbar>
|
|
5
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
6
|
+
<span class="font-weight-bold text-h5 text-capitalize">
|
|
7
|
+
{{ prop.mode }} Announcement
|
|
8
|
+
</span>
|
|
9
|
+
</v-row>
|
|
10
|
+
</v-toolbar>
|
|
11
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-6">
|
|
12
|
+
<v-form ref="formRef" v-model="validForm" :disabled="loading.processing" @click="errorMessage = ''">
|
|
13
|
+
<v-row no-gutters>
|
|
14
|
+
<v-col cols="12" class="font-weight-bold text-h6 mb-2">General Information</v-col>
|
|
15
|
+
<v-col cols="12">
|
|
16
|
+
<InputLabel title="Recipient(s)" />
|
|
17
|
+
<v-select v-model="bulletinForm.recipients" :items="recipientList" multiple item-value="value"
|
|
18
|
+
placeholder="Select recipients">
|
|
19
|
+
<template v-slot:prepend-item>
|
|
20
|
+
<v-list-item title="Select All" @click="handleSelectAll" class="">
|
|
21
|
+
<template v-slot:prepend>
|
|
22
|
+
<v-checkbox-btn :color="selectedSomeRecipients ? 'indigo-darken-4' : undefined"
|
|
23
|
+
:indeterminate="selectedSomeRecipients && !selectedAllRecipients"
|
|
24
|
+
:model-value="selectedAllRecipients"></v-checkbox-btn>
|
|
25
|
+
</template>
|
|
26
|
+
</v-list-item>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<template v-slot:selection="{ item, index }">
|
|
30
|
+
<v-chip :text="item.title" class="py-2">
|
|
31
|
+
<template #append>
|
|
32
|
+
<v-btn icon="mdi-close" size="small" density="compact" color="transparent" flat
|
|
33
|
+
class="ml-3 text-black" @mousedown.stop
|
|
34
|
+
@click.stop="handleRemoveRecipient(item)" />
|
|
35
|
+
</template>
|
|
36
|
+
</v-chip>
|
|
37
|
+
</template>
|
|
38
|
+
</v-select>
|
|
39
|
+
</v-col>
|
|
40
|
+
<v-col cols="12">
|
|
41
|
+
<InputLabel class="text-capitalize" title="Title" required />
|
|
42
|
+
<v-text-field v-model.trim="bulletinForm.title" density="compact" :rules="[requiredRule]" />
|
|
43
|
+
</v-col>
|
|
44
|
+
<v-col cols="12" class="my-4">
|
|
45
|
+
<InputLabel class="text-capitalize" title="Content" required />
|
|
46
|
+
<Editor v-model="bulletinForm.content" />
|
|
47
|
+
</v-col>
|
|
48
|
+
|
|
49
|
+
<v-col cols="12">
|
|
50
|
+
<v-checkbox v-model="bulletinForm.noExpiration" label="No Expiration" />
|
|
51
|
+
</v-col>
|
|
52
|
+
|
|
53
|
+
<v-col cols="12">
|
|
54
|
+
<InputLabel class="text-capitalize" title="Start Date" required />
|
|
55
|
+
<InputDateTimePicker v-model:utc="bulletinForm.startDate" :rules="[validStartDateRule]" />
|
|
56
|
+
</v-col>
|
|
57
|
+
|
|
58
|
+
<v-col cols="12" v-if="!bulletinForm.noExpiration">
|
|
59
|
+
<InputLabel class="text-capitalize" title="End Date" required />
|
|
60
|
+
<InputDateTimePicker v-model:utc="bulletinForm.endDate" :rules="[validExpiryDateRule]" />
|
|
61
|
+
</v-col>
|
|
62
|
+
|
|
63
|
+
<v-col cols="12">
|
|
64
|
+
<v-col cols="12" class="font-weight-bold text-h6">Attachments</v-col>
|
|
65
|
+
<v-col cols="12">
|
|
66
|
+
<v-btn text="Upload" class="text-capitalize" color="primary-button" variant="outlined"
|
|
67
|
+
min-width="150" @click="handleUpload" :loading="loading.uploading" />
|
|
68
|
+
</v-col>
|
|
69
|
+
<v-row no-gutters class="px-5">
|
|
70
|
+
<v-col cols="12">Videos ({{ videosArray.length }})</v-col>
|
|
71
|
+
<AttachmentList :list="videosArray" show-checkbox @set-preview="handleSetPreview"
|
|
72
|
+
@remove="handleRemoveFile" />
|
|
73
|
+
|
|
74
|
+
<v-col cols="12" class="mt-5">Photos ({{ photosArray.length }})</v-col>
|
|
75
|
+
<AttachmentList :list="photosArray" show-checkbox @set-preview="handleSetPreview"
|
|
76
|
+
@remove="handleRemoveFile" />
|
|
77
|
+
|
|
78
|
+
<v-col cols="12" class="mt-5">Files ({{ filesArray.length }})</v-col>
|
|
79
|
+
<AttachmentList :list="filesArray" show-checkbox @set-preview="handleSetPreview"
|
|
80
|
+
@remove="handleRemoveFile" />
|
|
81
|
+
</v-row>
|
|
82
|
+
</v-col>
|
|
83
|
+
</v-row>
|
|
84
|
+
</v-form>
|
|
85
|
+
</v-card-text>
|
|
86
|
+
|
|
87
|
+
<v-row no-gutters class="w-100" v-if="errorMessage">
|
|
88
|
+
<p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
|
|
89
|
+
</v-row>
|
|
90
|
+
<v-toolbar density="compact">
|
|
91
|
+
<v-row no-gutters>
|
|
92
|
+
<v-col cols="6">
|
|
93
|
+
<v-btn tile block variant="text" class="text-none" size="48" @click="close" text="Close" />
|
|
94
|
+
</v-col>
|
|
95
|
+
<v-col cols="6">
|
|
96
|
+
<v-btn tile block variant="flat" color="primary-button" class="text-none" size="48"
|
|
97
|
+
:text="prop.mode === 'add' ? 'Add Bulletin' : 'Save'"
|
|
98
|
+
:disabled="loading.processing || !validForm" @click="onSubmit" />
|
|
99
|
+
</v-col>
|
|
100
|
+
</v-row>
|
|
101
|
+
</v-toolbar>
|
|
102
|
+
</v-card>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<script setup lang="ts">
|
|
106
|
+
const prop = defineProps({
|
|
107
|
+
siteId: {
|
|
108
|
+
type: String,
|
|
109
|
+
required: true
|
|
110
|
+
},
|
|
111
|
+
mode: {
|
|
112
|
+
type: String as PropType<'edit' | 'add'>,
|
|
113
|
+
default: "add"
|
|
114
|
+
},
|
|
115
|
+
activeId: {
|
|
116
|
+
type: String,
|
|
117
|
+
default: ""
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const { requiredRule } = useUtils()
|
|
122
|
+
const { addFile, urlToFile, getFileUrl } = useFile()
|
|
123
|
+
const { add, update, recipientList, getBulletinById } = useBulletin()
|
|
124
|
+
|
|
125
|
+
const bulletinForm = reactive<TCreateAnnouncementPayload>({
|
|
126
|
+
recipients: [],
|
|
127
|
+
title: "",
|
|
128
|
+
content: "",
|
|
129
|
+
site: prop.siteId,
|
|
130
|
+
noExpiration: false,
|
|
131
|
+
startDate: "",
|
|
132
|
+
endDate: "",
|
|
133
|
+
file: []
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const emit = defineEmits(['close', 'done'])
|
|
137
|
+
|
|
138
|
+
const validForm = ref(false);
|
|
139
|
+
const formRef = ref<HTMLFormElement>()
|
|
140
|
+
const formKey = ref(0)
|
|
141
|
+
const errorMessage = ref('')
|
|
142
|
+
|
|
143
|
+
const rawFileArray = ref<{ file: File, _id: string, preview: boolean }[]>([])
|
|
144
|
+
|
|
145
|
+
const loading = reactive({
|
|
146
|
+
uploading: false,
|
|
147
|
+
processing: false
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
const photosArray = getByType('image/')
|
|
154
|
+
const videosArray = getByType('video/')
|
|
155
|
+
const filesArray = getByType('other')
|
|
156
|
+
|
|
157
|
+
function getByType(prefix: 'image/' | 'video/' | 'other') {
|
|
158
|
+
return computed(() => {
|
|
159
|
+
return rawFileArray.value.filter(x => {
|
|
160
|
+
const type = x?.file?.type
|
|
161
|
+
if (typeof type !== 'string') return false
|
|
162
|
+
|
|
163
|
+
if (prefix === 'other') {
|
|
164
|
+
return !type.startsWith('image/') && !type.startsWith('video/')
|
|
165
|
+
}
|
|
166
|
+
return type.startsWith(prefix)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
data: getAnnouncementByIdReq,
|
|
174
|
+
refresh: getAnnouncementByIdRefresh,
|
|
175
|
+
pending: getAnnouncementByIdPending,
|
|
176
|
+
error: getAnnouncementByIdError
|
|
177
|
+
} = await useLazyAsyncData(
|
|
178
|
+
`get-announcements-${prop.activeId}`,
|
|
179
|
+
() => {
|
|
180
|
+
if(prop.mode === 'add') return Promise.resolve(null) // ignore when add mode
|
|
181
|
+
const id = prop?.activeId
|
|
182
|
+
if (!id) {
|
|
183
|
+
throw new Error("Invalid activeId or mode. Cannot fetch announcement.")
|
|
184
|
+
}
|
|
185
|
+
return getBulletinById(id)
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
watch: [() => prop.activeId],
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
watch([getAnnouncementByIdReq, getAnnouncementByIdError], async ([newData, newError]) => {
|
|
193
|
+
if (newData) {
|
|
194
|
+
bulletinForm.title = newData?.title
|
|
195
|
+
bulletinForm.content = newData?.content
|
|
196
|
+
bulletinForm.noExpiration = newData?.noExpiration
|
|
197
|
+
bulletinForm.startDate = newData?.startDate
|
|
198
|
+
bulletinForm.endDate = newData?.endDate
|
|
199
|
+
bulletinForm.file = newData?.file
|
|
200
|
+
bulletinForm.recipients = newData?.recipients
|
|
201
|
+
|
|
202
|
+
// mapped rawFileArray
|
|
203
|
+
|
|
204
|
+
rawFileArray.value = await Promise.all(newData?.file?.map(async (item: TAnnouncementFile) => {
|
|
205
|
+
const fileUrl = getFileUrl(item?._id)
|
|
206
|
+
const file = await urlToFile(fileUrl, item?.name)
|
|
207
|
+
return {
|
|
208
|
+
file,
|
|
209
|
+
_id: item?._id,
|
|
210
|
+
preview: item.preview ?? false
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
}))
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (newError) {
|
|
218
|
+
const errMessage = newError?.message
|
|
219
|
+
console.error("Failed to load announcement:", errMessage)
|
|
220
|
+
errorMessage.value = errMessage || ("Cannot fetch announcement" + newError)
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const allRecipientValues: TAnnouncementRecipients[] = recipientList.map(r => r.value)
|
|
225
|
+
|
|
226
|
+
const selectedAllRecipients = computed(() => {
|
|
227
|
+
return bulletinForm.recipients.length === allRecipientValues.length &&
|
|
228
|
+
bulletinForm.recipients.every((x) => allRecipientValues.includes(x))
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
const selectedSomeRecipients = computed(() => {
|
|
234
|
+
return bulletinForm.recipients?.length > 0
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
function handleRemoveRecipient(item: any) {
|
|
240
|
+
const recipientVal = item?.value
|
|
241
|
+
const filtered = bulletinForm.recipients.filter(x => x !== recipientVal)
|
|
242
|
+
bulletinForm.recipients = filtered
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleSelectAll() {
|
|
246
|
+
if (selectedAllRecipients.value) {
|
|
247
|
+
bulletinForm.recipients = []
|
|
248
|
+
} else {
|
|
249
|
+
bulletinForm.recipients = allRecipientValues
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
function validStartDateRule(value: string) {
|
|
255
|
+
if (!value) {
|
|
256
|
+
return 'Start Date is required';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validExpiryDateRule(value: string) {
|
|
263
|
+
const noExpiration = bulletinForm.noExpiration
|
|
264
|
+
if (noExpiration) return true;
|
|
265
|
+
const startDateISO = bulletinForm.startDate;
|
|
266
|
+
if (!value) {
|
|
267
|
+
return 'End Date is required';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (value && startDateISO) {
|
|
271
|
+
const expiry = new Date(value);
|
|
272
|
+
const start = new Date(startDateISO as string);
|
|
273
|
+
return expiry >= start || 'End date must be equal or later than start date';
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
watch(() => bulletinForm.noExpiration, (newVal) => {
|
|
279
|
+
if (newVal === true) {
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
bulletinForm.endDate = ""
|
|
282
|
+
}, 100)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async function onSubmit() {
|
|
288
|
+
errorMessage.value = ""
|
|
289
|
+
await formRef.value?.validate()
|
|
290
|
+
if (!validForm.value) {
|
|
291
|
+
errorMessage.value = "Please complete required fields"
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let payload: Partial<TCreateAnnouncementPayload> = {}
|
|
296
|
+
if (prop.mode === 'add') {
|
|
297
|
+
payload = { ...bulletinForm }
|
|
298
|
+
} else if (prop.mode === 'edit') {
|
|
299
|
+
const { site, ...rest } = bulletinForm
|
|
300
|
+
payload = rest
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
loading.processing = true
|
|
305
|
+
if (prop.mode === 'add') {
|
|
306
|
+
await add(payload)
|
|
307
|
+
} else if (prop.mode === 'edit') {
|
|
308
|
+
const id = prop.activeId
|
|
309
|
+
if (!id) throw new Error('Missing activeId prop')
|
|
310
|
+
await update(prop.activeId, payload)
|
|
311
|
+
} else {
|
|
312
|
+
throw new Error('Missing mode prop!')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
emit('done')
|
|
316
|
+
|
|
317
|
+
} catch (error: any) {
|
|
318
|
+
const err = error?.data?.message
|
|
319
|
+
errorMessage.value = err || `Failed to ${prop.mode === 'add' ? 'add' : 'update'}. Please try again.`;
|
|
320
|
+
|
|
321
|
+
} finally {
|
|
322
|
+
loading.processing = false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function close() {
|
|
327
|
+
emit('close')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function handleUpload() {
|
|
331
|
+
|
|
332
|
+
const input = document.createElement("input")
|
|
333
|
+
input.type = "file"
|
|
334
|
+
input.multiple = true
|
|
335
|
+
|
|
336
|
+
input.onchange = async (event: Event) => {
|
|
337
|
+
const target = event.target as HTMLInputElement
|
|
338
|
+
const filesArray = target.files || []
|
|
339
|
+
const MAX_SIZE = 25 * 1024 * 1024 // 25MB
|
|
340
|
+
for (const file of filesArray) {
|
|
341
|
+
loading.uploading = true
|
|
342
|
+
|
|
343
|
+
if (file.size > MAX_SIZE) {
|
|
344
|
+
errorMessage.value = `Skipping File "${file.name}", exceeds 25MB limit.`
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const res = await addFile(file)
|
|
350
|
+
const fileId = res?.id
|
|
351
|
+
const fileName = file?.name || `file-${fileId}`
|
|
352
|
+
if (fileId) {
|
|
353
|
+
rawFileArray.value.push({ file, _id: fileId, preview: false })
|
|
354
|
+
// bulletinForm.file.push({ name: fileName, preview: false, _id: fileId })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (err: any) {
|
|
358
|
+
console.error("Upload failed:", err)
|
|
359
|
+
const message = err?.data?.message
|
|
360
|
+
errorMessage.value = "Failed to upload image. Please try again."
|
|
361
|
+
} finally {
|
|
362
|
+
loading.uploading = false
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
input.click()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function handleSetPreview(fileId: string) {
|
|
371
|
+
rawFileArray.value.forEach(item => {
|
|
372
|
+
if (item._id !== fileId) {
|
|
373
|
+
item.preview = false
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function handleRemoveFile(fileId: string) {
|
|
379
|
+
rawFileArray.value = rawFileArray.value.filter(item => item?._id !== fileId)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
watch(rawFileArray, (newArr) => {
|
|
383
|
+
bulletinForm.file = newArr.map(item => {
|
|
384
|
+
return {
|
|
385
|
+
_id: item?._id,
|
|
386
|
+
name: item?.file?.name,
|
|
387
|
+
preview: item?.preview,
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
}, { deep: true })
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
</script>
|
|
395
|
+
|
|
396
|
+
<style scoped></style>
|
|
@@ -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>
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare interface TAnnouncement {
|
|
2
|
+
_id: string;
|
|
3
|
+
recipients: TAnnouncementRecipients[];
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
file: AnnouncementFile[];
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
site: string;
|
|
10
|
+
createdBy: string;
|
|
11
|
+
noExpiration: boolean;
|
|
12
|
+
startDate: string;
|
|
13
|
+
endDate: string;
|
|
14
|
+
status: TAnnouncementStatus
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare interface TAnnouncementFile {
|
|
18
|
+
_id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
preview: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare type TAnnouncementRecipients = "admin" | "organization" | "site" | "service-provider" | "service-provider-member" | "resident";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
declare type TCreateAnnouncementPayload = Pick<TAnnouncement, "recipients" | "file" | "site" | "noExpiration" | "startDate" | "endDate" | "content" | "title">
|
|
28
|
+
|
|
29
|
+
declare type TAnnouncementStatus = "active" | "expired"
|