| 1 | <template> |
| 2 | <n-popover trigger="click" :show-arrow="false" to="body" placement="left" @update:show="loadList"> |
| 3 | <template #trigger> |
| 4 | <n-button :size :focusable="false" :loading="!!linkingCaseId || !!linkingAlertId" :secondary> |
| 5 | {{ label || "Link Alert" }} |
| 6 | </n-button> |
| 7 | </template> |
| 8 | |
| 9 | <n-spin :show="loading" class="min-h-50 max-w-100 min-w-50"> |
| 10 | <div class="mt-1 flex flex-col gap-2"> |
| 11 | <div class="flex flex-col gap-2"> |
| 12 | <CardEntity |
| 13 | v-for="item in cases" |
| 14 | :key="item.id" |
| 15 | size="small" |
| 16 | embedded |
| 17 | main-box-class="p-1!" |
| 18 | header-box-class="flex-nowrap!" |
| 19 | hoverable |
| 20 | > |
| 21 | <template #header-main> |
| 22 | <div class="px-1">#{{ item.id }} - {{ item.case_name }}</div> |
| 23 | </template> |
| 24 | <template #header-extra> |
| 25 | <n-button |
| 26 | type="primary" |
| 27 | size="tiny" |
| 28 | :loading="linkingCaseId === item.id" |
| 29 | :disabled="item.alerts.some(alert => alert.id === props.alertId)" |
| 30 | @click="linkAlert(item.id, props.alertId || 0)" |
| 31 | > |
| 32 | <template #icon> |
| 33 | <Icon name="carbon:link" /> |
| 34 | </template> |
| 35 | Link |
| 36 | </n-button> |
| 37 | </template> |
| 38 | </CardEntity> |
| 39 | |
| 40 | <CardEntity |
| 41 | v-for="item in alerts" |
| 42 | :key="item.id" |
| 43 | size="small" |
| 44 | embedded |
| 45 | main-box-class="p-1!" |
| 46 | header-box-class="flex-nowrap!" |
| 47 | hoverable |
| 48 | > |
| 49 | <template #header-main> |
| 50 | <div class="px-1">#{{ item.id }} - {{ item.alert_name }}</div> |
| 51 | </template> |
| 52 | <template #header-extra> |
| 53 | <n-button |
| 54 | type="primary" |
| 55 | size="tiny" |
| 56 | :loading="linkingAlertId === item.id" |
| 57 | :disabled="item.linked_cases.some(caseData => caseData.id === props.caseId)" |
| 58 | @click="linkAlert(props.caseId || 0, item.id)" |
| 59 | > |
| 60 | <template #icon> |
| 61 | <Icon name="carbon:link" /> |
| 62 | </template> |
| 63 | Link |
| 64 | </n-button> |
| 65 | </template> |
| 66 | </CardEntity> |
| 67 | </div> |
| 68 | <div class="flex justify-end"> |
| 69 | <n-pagination |
| 70 | v-if="pagination.total > pagination.pageSize" |
| 71 | v-model:page="pagination.page" |
| 72 | :page-size="pagination.pageSize" |
| 73 | :item-count="pagination.total" |
| 74 | :page-slot="6" |
| 75 | size="small" |
| 76 | /> |
| 77 | </div> |
| 78 | </div> |
| 79 | </n-spin> |
| 80 | </n-popover> |
| 81 | </template> |
| 82 | |
| 83 | <script setup lang="ts"> |
| 84 | import type { ButtonSize } from "naive-ui" |
| 85 | import type { Alert } from "@/types/alerts" |
| 86 | import type { Case } from "@/types/cases" |
| 87 | import type { ApiError, Pagination } from "@/types/common" |
| 88 | import { useDebounceFn } from "@vueuse/core" |
| 89 | import { NButton, NPagination, NPopover, NSpin, useMessage } from "naive-ui" |
| 90 | import { ref, watch } from "vue" |
| 91 | import Api from "@/api" |
| 92 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 93 | import Icon from "@/components/common/Icon.vue" |
| 94 | import { getApiErrorMessage } from "@/utils" |
| 95 | |
| 96 | const props = defineProps<{ |
| 97 | alertId?: number |
| 98 | caseId?: number |
| 99 | label?: string |
| 100 | secondary?: boolean |
| 101 | size?: ButtonSize |
| 102 | }>() |
| 103 | |
| 104 | const emit = defineEmits<{ |
| 105 | (e: "linked", value: number): void |
| 106 | }>() |
| 107 | |
| 108 | const linkingCaseId = ref<number | null>(null) |
| 109 | const linkingAlertId = ref<number | null>(null) |
| 110 | const loading = ref(false) |
| 111 | const message = useMessage() |
| 112 | const pagination = ref({ |
| 113 | page: 1, |
| 114 | pageSize: 5, |
| 115 | total: 0 |
| 116 | }) |
| 117 | const cases = ref<Case[]>([]) |
| 118 | const alerts = ref<Alert[]>([]) |
| 119 | |
| 120 | function linkAlert(caseId: number, alertId: number) { |
| 121 | if (!caseId || !alertId) { |
| 122 | message.error("Case and alert IDs are required") |
| 123 | return |
| 124 | } |
| 125 | |
| 126 | linkingCaseId.value = caseId |
| 127 | linkingAlertId.value = alertId |
| 128 | |
| 129 | Api.cases |
| 130 | .linkCaseToAlert(caseId, alertId) |
| 131 | .then(res => { |
| 132 | if (res.data.success) { |
| 133 | emit("linked", alertId) |
| 134 | message.success(res.data?.message || "Alert linked to case successfully") |
| 135 | } else { |
| 136 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 137 | } |
| 138 | }) |
| 139 | .catch(err => { |
| 140 | message.error(getApiErrorMessage(err as ApiError) || "An error occurred. Please try again later.") |
| 141 | }) |
| 142 | .finally(() => { |
| 143 | linkingCaseId.value = null |
| 144 | linkingAlertId.value = null |
| 145 | }) |
| 146 | } |
| 147 | |
| 148 | const loadList = useDebounceFn(async () => { |
| 149 | loading.value = true |
| 150 | |
| 151 | try { |
| 152 | const paginationPayload: Pagination = { |
| 153 | page: pagination.value.page, |
| 154 | pageSize: pagination.value.pageSize, |
| 155 | order: "desc" |
| 156 | } |
| 157 | |
| 158 | const method = props.alertId ? Api.cases.getCases(paginationPayload) : Api.alerts.getAlerts(paginationPayload) |
| 159 | |
| 160 | const response = await method |
| 161 | |
| 162 | if (props.alertId && "cases" in response.data) { |
| 163 | cases.value = response.data?.cases || [] |
| 164 | } |
| 165 | if (props.caseId && "alerts" in response.data) { |
| 166 | alerts.value = response.data?.alerts || [] |
| 167 | } |
| 168 | |
| 169 | pagination.value.total = response.data?.total || 0 |
| 170 | } catch (err) { |
| 171 | message.error(getApiErrorMessage(err as ApiError)) |
| 172 | } finally { |
| 173 | loading.value = false |
| 174 | } |
| 175 | }, 300) |
| 176 | |
| 177 | watch(() => pagination.value.page, loadList) |
| 178 | </script> |