| 1 | <template> |
| 2 | <div class="alerts-list @container"> |
| 3 | <div ref="header" class="flex items-center justify-between gap-2"> |
| 4 | <div class="flex items-center gap-2"> |
| 5 | <div class="flex grow gap-2 @6xl:hidden!"> |
| 6 | <n-popover overlap placement="left"> |
| 7 | <template #trigger> |
| 8 | <div class="bg-default rounded-lg"> |
| 9 | <n-button size="small" class="cursor-help!"> |
| 10 | <template #icon> |
| 11 | <Icon :name="InfoIcon" /> |
| 12 | </template> |
| 13 | </n-button> |
| 14 | </div> |
| 15 | </template> |
| 16 | <div class="flex flex-col gap-2"> |
| 17 | <div class="box"> |
| 18 | Total : |
| 19 | <code> |
| 20 | <span v-if="totalFiltered === total">{{ total }}</span> |
| 21 | <span v-else> |
| 22 | <span>{{ totalFiltered }}</span> |
| 23 | <span class="opacity-50">/</span> |
| 24 | <span class="opacity-50">{{ total }}</span> |
| 25 | </span> |
| 26 | </code> |
| 27 | </div> |
| 28 | <div class="box text-error"> |
| 29 | Open : |
| 30 | <code>{{ statusOpenTotal }}</code> |
| 31 | </div> |
| 32 | <div class="box text-warning"> |
| 33 | In Progress : |
| 34 | <code>{{ statusInProgressTotal }}</code> |
| 35 | </div> |
| 36 | <div class="box text-success"> |
| 37 | Close : |
| 38 | <code>{{ statusCloseTotal }}</code> |
| 39 | </div> |
| 40 | </div> |
| 41 | </n-popover> |
| 42 | </div> |
| 43 | <div class="hidden grow items-center gap-1 text-sm @6xl:flex"> |
| 44 | <n-button quaternary size="small" @click="filtersCTX?.setFilter([{ type: 'status', value: null }])"> |
| 45 | <div class="flex items-center gap-2"> |
| 46 | <span>Total</span> |
| 47 | <code class="py-1"> |
| 48 | <span v-if="totalFiltered === total">{{ total }}</span> |
| 49 | <span v-else class="flex gap-1"> |
| 50 | <span>{{ totalFiltered }}</span> |
| 51 | <span class="opacity-50">/</span> |
| 52 | <span class="opacity-50">{{ total }}</span> |
| 53 | </span> |
| 54 | </code> |
| 55 | </div> |
| 56 | </n-button> |
| 57 | <span>/</span> |
| 58 | <n-button |
| 59 | quaternary |
| 60 | size="small" |
| 61 | @click="filtersCTX?.setFilter([{ type: 'status', value: 'OPEN' }])" |
| 62 | > |
| 63 | <div class="flex items-center gap-2"> |
| 64 | <span>Open</span> |
| 65 | <code class="text-error py-1">{{ statusOpenTotal }}</code> |
| 66 | </div> |
| 67 | </n-button> |
| 68 | <span>/</span> |
| 69 | <n-button |
| 70 | quaternary |
| 71 | size="small" |
| 72 | @click="filtersCTX?.setFilter([{ type: 'status', value: 'IN_PROGRESS' }])" |
| 73 | > |
| 74 | <div class="flex items-center gap-2"> |
| 75 | <span>In Progress</span> |
| 76 | <code class="text-warning py-1">{{ statusInProgressTotal }}</code> |
| 77 | </div> |
| 78 | </n-button> |
| 79 | <span>/</span> |
| 80 | <n-button |
| 81 | quaternary |
| 82 | size="small" |
| 83 | @click="filtersCTX?.setFilter([{ type: 'status', value: 'CLOSED' }])" |
| 84 | > |
| 85 | <div class="flex items-center gap-2"> |
| 86 | <span>Close</span> |
| 87 | <code class="text-success py-1">{{ statusCloseTotal }}</code> |
| 88 | </div> |
| 89 | </n-button> |
| 90 | </div> |
| 91 | <n-button size="small" text type="error" @click="showDeleteByTitleModal = true"> |
| 92 | <div class="flex items-center gap-2"> |
| 93 | <Icon :name="TrashIcon" /> |
| 94 | <span class="hidden @3xl:inline">Bulk Delete</span> |
| 95 | </div> |
| 96 | </n-button> |
| 97 | </div> |
| 98 | |
| 99 | <div class="flex items-center justify-end gap-2 whitespace-nowrap"> |
| 100 | <n-pagination |
| 101 | v-model:page="currentPage" |
| 102 | v-model:page-size="pageSize" |
| 103 | :page-slot |
| 104 | :show-size-picker |
| 105 | :page-sizes |
| 106 | :item-count="totalFiltered" |
| 107 | :simple="simpleMode" |
| 108 | /> |
| 109 | <n-select |
| 110 | v-model:value="sort" |
| 111 | size="small" |
| 112 | :options="sortOptions" |
| 113 | :show-checkmark="false" |
| 114 | class="max-w-20" |
| 115 | :disabled="loading" |
| 116 | /> |
| 117 | |
| 118 | <n-badge v-if="showFilters" :show="filtered" dot type="success" :offset="[-4, 0]"> |
| 119 | <n-button size="small" secondary @click="showFiltersView = !showFiltersView"> |
| 120 | <template #icon> |
| 121 | <Icon :name="FilterIcon" /> |
| 122 | </template> |
| 123 | </n-button> |
| 124 | </n-badge> |
| 125 | </div> |
| 126 | </div> |
| 127 | |
| 128 | <CollapseKeepAlive v-if="showFilters" :show="showFiltersView" embedded arrow="top-right"> |
| 129 | <AlertsFilters |
| 130 | :use-query-string="!preset?.length" |
| 131 | :preset |
| 132 | class="p-3" |
| 133 | @submit="applyFilters" |
| 134 | @mounted="filtersCTX = $event" |
| 135 | /> |
| 136 | </CollapseKeepAlive> |
| 137 | |
| 138 | <n-collapse-transition |
| 139 | :show="!!checkedAlerts.length" |
| 140 | style="position: sticky; top: calc(var(--toolbar-height) - 24px); z-index: 1" |
| 141 | > |
| 142 | <n-card content-class="flex flex-wrap items-center gap-3" size="small" class="mt-3 shadow-xl" embedded> |
| 143 | <n-popover content-class="p-0!"> |
| 144 | <template #trigger> |
| 145 | <n-button secondary size="small"> |
| 146 | <div class="flex min-w-8 items-center gap-2"> |
| 147 | <span>Selected</span> |
| 148 | <n-badge |
| 149 | :value="checkedAlerts.length" |
| 150 | color="rgba(var(--border-color-rgb) / 0.3)" |
| 151 | class="font-mono!" |
| 152 | /> |
| 153 | </div> |
| 154 | </n-button> |
| 155 | </template> |
| 156 | <template #default> |
| 157 | <div class="py-4"> |
| 158 | <n-scrollbar trigger="none" class="max-h-150 px-3 [&_.n-scrollbar-container]:rounded-lg"> |
| 159 | <div class="flex max-w-lg flex-col gap-2"> |
| 160 | <AlertItem |
| 161 | v-for="alert of checkedAlerts" |
| 162 | :key="alert.id" |
| 163 | embedded |
| 164 | compact |
| 165 | selectable |
| 166 | checked |
| 167 | :alert-data="alert" |
| 168 | @check="toggleCheck(alert)" |
| 169 | @deleted="deleted(alert)" |
| 170 | @updated="updateAlert($event)" |
| 171 | /> |
| 172 | </div> |
| 173 | </n-scrollbar> |
| 174 | </div> |
| 175 | </template> |
| 176 | <template #footer> |
| 177 | <div class="flex justify-between"> |
| 178 | <n-button size="small" secondary @click="setALlChecked()">Select current page</n-button> |
| 179 | <n-button size="small" secondary @click="resetChecked()">Uncheck all</n-button> |
| 180 | </div> |
| 181 | </template> |
| 182 | </n-popover> |
| 183 | |
| 184 | <AlertMergeCaseButton |
| 185 | v-if="checkedNoLinkedAlerts.length" |
| 186 | :alerts="checkedNoLinkedAlerts" |
| 187 | size="small" |
| 188 | @updated="updateAlert" |
| 189 | @merged="resetChecked()" |
| 190 | /> |
| 191 | |
| 192 | <n-popconfirm to="body" @positive-click="deleteAlerts()"> |
| 193 | <template #trigger> |
| 194 | <n-button size="small" type="error" secondary :loading="deleting"> |
| 195 | <template #icon> |
| 196 | <Icon :name="TrashIcon" /> |
| 197 | </template> |
| 198 | Delete |
| 199 | </n-button> |
| 200 | </template> |
| 201 | Are you sure you want to delete selected Alerts? |
| 202 | </n-popconfirm> |
| 203 | </n-card> |
| 204 | </n-collapse-transition> |
| 205 | |
| 206 | <n-modal |
| 207 | v-model:show="showDeleteByTitleModal" |
| 208 | preset="dialog" |
| 209 | title="Delete Alerts by Title" |
| 210 | :positive-text="deletingByTitle ? 'Deleting...' : 'Delete'" |
| 211 | negative-text="Cancel" |
| 212 | :positive-button-props="{ disabled: !titleFilterInput || deletingByTitle }" |
| 213 | @positive-click="deleteAlertsByTitle" |
| 214 | > |
| 215 | <div class="flex flex-col gap-4"> |
| 216 | <p class="text-sm opacity-70"> |
| 217 | Delete all alerts matching a title filter. This will perform a partial, case-insensitive match. |
| 218 | </p> |
| 219 | <n-input |
| 220 | v-model:value="titleFilterInput" |
| 221 | placeholder="e.g., 'File' to match 'File added to the system.'" |
| 222 | :disabled="deletingByTitle" |
| 223 | @keyup.enter="titleFilterInput && deleteAlertsByTitle()" |
| 224 | > |
| 225 | <template #prefix> |
| 226 | <Icon :name="FilterIcon" /> |
| 227 | </template> |
| 228 | </n-input> |
| 229 | <n-alert v-if="titleFilterInput" type="warning" :bordered="false"> |
| 230 | This will delete all alerts with titles containing "{{ titleFilterInput }}". |
| 231 | <br /> |
| 232 | <strong>This action cannot be undone.</strong> |
| 233 | </n-alert> |
| 234 | </div> |
| 235 | </n-modal> |
| 236 | |
| 237 | <n-spin :show="loading"> |
| 238 | <div class="my-3 flex min-h-52 flex-col gap-2"> |
| 239 | <template v-if="alertsList.length"> |
| 240 | <AlertItem |
| 241 | v-for="alert of alertsList" |
| 242 | :key="alert.id" |
| 243 | selectable |
| 244 | :checked="isChecked(alert)" |
| 245 | :alert-data="alert" |
| 246 | :highlight="highlight === alert.id.toString()" |
| 247 | :details-on-mounted="highlight === alert.id.toString() && !highlightedItemOpened" |
| 248 | class="item-appear item-appear-bottom item-appear-005" |
| 249 | @check="toggleCheck(alert)" |
| 250 | @opened="highlightedItemOpened = true" |
| 251 | @deleted="deleted(alert)" |
| 252 | @updated="updateAlert($event)" |
| 253 | /> |
| 254 | </template> |
| 255 | <template v-else> |
| 256 | <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" /> |
| 257 | </template> |
| 258 | </div> |
| 259 | </n-spin> |
| 260 | <div class="footer flex justify-end"> |
| 261 | <n-pagination |
| 262 | v-if="alertsList.length > 3" |
| 263 | v-model:page="currentPage" |
| 264 | :page-size |
| 265 | :item-count="totalFiltered" |
| 266 | :page-slot="6" |
| 267 | /> |
| 268 | </div> |
| 269 | </div> |
| 270 | </template> |
| 271 | |
| 272 | <script setup lang="ts"> |
| 273 | // TODO-FE: refactor |
| 274 | import type { AlertsListFilter } from "./types.d" |
| 275 | import type { AlertsQuery } from "@/api/endpoints/incidentManagement/alerts" |
| 276 | import type { Alert } from "@/types/incidentManagement/alerts.d" |
| 277 | import type { Case } from "@/types/incidentManagement/cases.d" |
| 278 | import { useResizeObserver, useStorage } from "@vueuse/core" |
| 279 | import axios from "axios" |
| 280 | import _orderBy from "lodash/orderBy" |
| 281 | import { |
| 282 | NAlert, |
| 283 | NBadge, |
| 284 | NButton, |
| 285 | NCard, |
| 286 | NCollapseTransition, |
| 287 | NEmpty, |
| 288 | NInput, |
| 289 | NModal, |
| 290 | NPagination, |
| 291 | NPopconfirm, |
| 292 | NPopover, |
| 293 | NScrollbar, |
| 294 | NSelect, |
| 295 | NSpin, |
| 296 | useMessage |
| 297 | } from "naive-ui" |
| 298 | import { computed, defineAsyncComponent, nextTick, onBeforeMount, provide, ref, watch } from "vue" |
| 299 | import Api from "@/api" |
| 300 | import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue" |
| 301 | import Icon from "@/components/common/Icon.vue" |
| 302 | import AlertItem from "./AlertItem.vue" |
| 303 | import AlertsFilters from "./AlertsFilters.vue" |
| 304 | |
| 305 | const { |
| 306 | highlight, |
| 307 | preset, |
| 308 | showFilters = true |
| 309 | } = defineProps<{ |
| 310 | highlight?: string | null |
| 311 | preset?: AlertsListFilter[] |
| 312 | showFilters?: boolean |
| 313 | }>() |
| 314 | |
| 315 | const AlertMergeCaseButton = defineAsyncComponent(() => import("./AlertMergeCaseButton.vue")) |
| 316 | |
| 317 | const FilterIcon = "carbon:filter-edit" |
| 318 | const TrashIcon = "carbon:trash-can" |
| 319 | const InfoIcon = "carbon:information" |
| 320 | |
| 321 | const checkedAlerts = ref<Alert[]>([]) |
| 322 | const checkedNoLinkedAlerts = computed(() => checkedAlerts.value.filter(alert => !alert.linked_cases.length)) |
| 323 | const message = useMessage() |
| 324 | const loading = ref(false) |
| 325 | const deleting = ref(false) |
| 326 | const deletingByTitle = ref(false) |
| 327 | const showDeleteByTitleModal = ref(false) |
| 328 | const titleFilterInput = ref("") |
| 329 | const showFiltersView = useStorage<boolean>("incident-management-alerts-list-filters-view-state", false, localStorage) |
| 330 | const alertsList = ref<Alert[]>([]) |
| 331 | const availableUsers = ref<string[]>([]) |
| 332 | const linkableCases = ref<Case[]>([]) |
| 333 | let abortController: AbortController | null = null |
| 334 | |
| 335 | const currentPage = ref(1) |
| 336 | const simpleMode = ref(false) |
| 337 | const showSizePicker = ref(true) |
| 338 | const pageSizes = [10, 25, 50, 100] |
| 339 | const pageSize = ref(pageSizes[1]) |
| 340 | const header = ref() |
| 341 | const pageSlot = ref(8) |
| 342 | const sort = defineModel<"asc" | "desc">("sort", { default: "desc" }) |
| 343 | const sortOptions = [ |
| 344 | { label: "Desc", value: "desc" }, |
| 345 | { label: "Asc", value: "asc" } |
| 346 | ] |
| 347 | |
| 348 | const totalFiltered = ref(0) |
| 349 | const total = ref(0) |
| 350 | const statusOpenTotal = ref(0) |
| 351 | const statusInProgressTotal = ref(0) |
| 352 | const statusCloseTotal = ref(0) |
| 353 | |
| 354 | const filtersCTX = ref<{ setFilter: (payload: AlertsListFilter[]) => void } | null>(null) |
| 355 | const filters = ref<AlertsListFilter[]>([]) |
| 356 | |
| 357 | const filtered = computed<boolean>(() => { |
| 358 | return !!filters.value.length |
| 359 | }) |
| 360 | |
| 361 | const highlightedItemFound = ref(!highlight) |
| 362 | const highlightedItemOpened = ref(!highlight) |
| 363 | |
| 364 | watch([currentPage, sort], () => { |
| 365 | getData() |
| 366 | }) |
| 367 | |
| 368 | watch(pageSize, () => { |
| 369 | if (currentPage.value === 1) { |
| 370 | getData() |
| 371 | } else { |
| 372 | currentPage.value = 1 |
| 373 | } |
| 374 | }) |
| 375 | |
| 376 | watch( |
| 377 | alertsList, |
| 378 | () => { |
| 379 | if ( |
| 380 | alertsList.value.length && |
| 381 | !alertsList.value.some(o => o.id.toString() === highlight) && |
| 382 | currentPage.value < totalFiltered.value && |
| 383 | !highlightedItemFound.value |
| 384 | ) { |
| 385 | nextTick(() => { |
| 386 | currentPage.value++ |
| 387 | }) |
| 388 | } |
| 389 | |
| 390 | if (alertsList.value.some(o => o.id.toString() === highlight)) { |
| 391 | highlightedItemFound.value = true |
| 392 | } |
| 393 | }, |
| 394 | { immediate: true } |
| 395 | ) |
| 396 | |
| 397 | provide("assignable-users", availableUsers) |
| 398 | |
| 399 | provide("linkable-cases", linkableCases) |
| 400 | |
| 401 | function applyFilters(newFilters: AlertsListFilter[]) { |
| 402 | filters.value = newFilters |
| 403 | getData() |
| 404 | } |
| 405 | |
| 406 | function updateAlert(updatedAlert: Alert) { |
| 407 | const alertIndex = alertsList.value.findIndex(o => o.id === updatedAlert.id) |
| 408 | if (alertIndex !== -1) { |
| 409 | alertsList.value[alertIndex] = updatedAlert |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | function isChecked(alert: Alert) { |
| 414 | return checkedAlerts.value.some(o => o.id === alert.id) |
| 415 | } |
| 416 | |
| 417 | function toggleCheck(alert: Alert) { |
| 418 | const alertIndex = checkedAlerts.value.findIndex(o => o.id === alert.id) |
| 419 | |
| 420 | if (checkedAlerts.value.some(o => o.id === alert.id)) { |
| 421 | checkedAlerts.value.splice(alertIndex, 1) |
| 422 | } else { |
| 423 | checkedAlerts.value.push(alert) |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | function resetChecked() { |
| 428 | checkedAlerts.value = [] |
| 429 | } |
| 430 | |
| 431 | function setALlChecked() { |
| 432 | for (const alert of alertsList.value) { |
| 433 | if (!isChecked(alert)) { |
| 434 | toggleCheck(alert) |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | function removeChecked(alert: Alert) { |
| 440 | const alertIndex = checkedAlerts.value.findIndex(o => o.id === alert.id) |
| 441 | |
| 442 | if (checkedAlerts.value.some(o => o.id === alert.id)) { |
| 443 | checkedAlerts.value.splice(alertIndex, 1) |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | function deleted(alert: Alert) { |
| 448 | removeChecked(alert) |
| 449 | getData() |
| 450 | } |
| 451 | |
| 452 | function getData() { |
| 453 | abortController?.abort() |
| 454 | abortController = new AbortController() |
| 455 | |
| 456 | loading.value = true |
| 457 | |
| 458 | const query: Partial<AlertsQuery> = { |
| 459 | page: currentPage.value, |
| 460 | pageSize: pageSize.value, |
| 461 | sort: sort.value |
| 462 | } |
| 463 | |
| 464 | if (filtered.value) { |
| 465 | query.filters = filters.value |
| 466 | } |
| 467 | |
| 468 | Api.incidentManagement.alerts |
| 469 | .getAlertsList(query, abortController.signal) |
| 470 | .then(res => { |
| 471 | if (res.data.success) { |
| 472 | alertsList.value = res.data?.alerts || [] |
| 473 | total.value = res.data.total || 0 |
| 474 | totalFiltered.value = res.data.total_filtered ?? total.value ?? 0 |
| 475 | statusCloseTotal.value = res.data.closed || 0 |
| 476 | statusInProgressTotal.value = res.data.in_progress || 0 |
| 477 | statusOpenTotal.value = res.data.open || 0 |
| 478 | } else { |
| 479 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 480 | } |
| 481 | loading.value = false |
| 482 | }) |
| 483 | .catch(err => { |
| 484 | if (!axios.isCancel(err)) { |
| 485 | alertsList.value = [] |
| 486 | |
| 487 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 488 | loading.value = false |
| 489 | } |
| 490 | }) |
| 491 | } |
| 492 | |
| 493 | function getAvailableUsers() { |
| 494 | Api.incidentManagement.alerts |
| 495 | .getAvailableUsers() |
| 496 | .then(res => { |
| 497 | if (res.data.success) { |
| 498 | availableUsers.value = res.data?.available_users || [] |
| 499 | } else { |
| 500 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 501 | } |
| 502 | }) |
| 503 | .catch(err => { |
| 504 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 505 | }) |
| 506 | } |
| 507 | |
| 508 | function getCases() { |
| 509 | Api.incidentManagement.cases |
| 510 | .getCasesList(undefined, { page: 1, pageSize: 9999 }) |
| 511 | .then(res => { |
| 512 | if (res.data.success) { |
| 513 | linkableCases.value = _orderBy(res.data?.cases || [], ["id"], ["desc"]) |
| 514 | } else { |
| 515 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 516 | } |
| 517 | }) |
| 518 | .catch(err => { |
| 519 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 520 | }) |
| 521 | } |
| 522 | |
| 523 | function deleteAlerts() { |
| 524 | deleting.value = true |
| 525 | |
| 526 | Api.incidentManagement.alerts |
| 527 | .deleteAlerts(checkedAlerts.value.map(o => o.id)) |
| 528 | .then(res => { |
| 529 | if (res.data.success) { |
| 530 | if (res.data.deleted_alert_ids.length) { |
| 531 | for (const id of res.data.deleted_alert_ids) { |
| 532 | toggleCheck({ id } as Alert) |
| 533 | } |
| 534 | |
| 535 | if (res.data.not_deleted_alert_ids.length) { |
| 536 | message.warning("Some alerts could not be deleted.") |
| 537 | } else { |
| 538 | message.success(res.data?.message || "Alerts deleted successfully.") |
| 539 | } |
| 540 | } else { |
| 541 | message.warning("The selected alerts could not be deleted") |
| 542 | } |
| 543 | |
| 544 | getData() |
| 545 | } else { |
| 546 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 547 | } |
| 548 | }) |
| 549 | .catch(err => { |
| 550 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 551 | }) |
| 552 | .finally(() => { |
| 553 | deleting.value = false |
| 554 | }) |
| 555 | } |
| 556 | |
| 557 | function deleteAlertsByTitle() { |
| 558 | if (!titleFilterInput.value) return |
| 559 | |
| 560 | deletingByTitle.value = true |
| 561 | |
| 562 | Api.incidentManagement.alerts |
| 563 | .deleteAlertsByTitle(titleFilterInput.value) |
| 564 | .then(res => { |
| 565 | if (res.data.success) { |
| 566 | const deletedCount = res.data.deleted_alert_ids.length |
| 567 | const skippedCount = res.data.not_deleted_alert_ids.length |
| 568 | |
| 569 | if (deletedCount > 0) { |
| 570 | if (skippedCount > 0) { |
| 571 | message.warning( |
| 572 | `Deleted ${deletedCount} alert(s). ${skippedCount} alert(s) were skipped (linked to cases).` |
| 573 | ) |
| 574 | } else { |
| 575 | message.success(`Successfully deleted ${deletedCount} alert(s).`) |
| 576 | } |
| 577 | |
| 578 | // Clear any checked alerts that were deleted |
| 579 | checkedAlerts.value = checkedAlerts.value.filter( |
| 580 | alert => !res.data.deleted_alert_ids.includes(alert.id) |
| 581 | ) |
| 582 | |
| 583 | // Refresh the list |
| 584 | getData() |
| 585 | } else { |
| 586 | message.info("No alerts were deleted. They may be linked to cases or not found.") |
| 587 | } |
| 588 | |
| 589 | // Close modal and reset input |
| 590 | showDeleteByTitleModal.value = false |
| 591 | titleFilterInput.value = "" |
| 592 | } else { |
| 593 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 594 | } |
| 595 | }) |
| 596 | .catch(err => { |
| 597 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 598 | }) |
| 599 | .finally(() => { |
| 600 | deletingByTitle.value = false |
| 601 | }) |
| 602 | } |
| 603 | |
| 604 | useResizeObserver(header, entries => { |
| 605 | const entry = entries[0] |
| 606 | if (!entry) return |
| 607 | |
| 608 | const { width } = entry.contentRect |
| 609 | |
| 610 | pageSlot.value = width < 700 ? 5 : 8 |
| 611 | simpleMode.value = width < 550 |
| 612 | }) |
| 613 | |
| 614 | onBeforeMount(() => { |
| 615 | if (!showFilters && preset?.length) { |
| 616 | filters.value = preset |
| 617 | } |
| 618 | |
| 619 | getData() |
| 620 | getAvailableUsers() |
| 621 | getCases() |
| 622 | }) |
| 623 | </script> |