| 1 | <template> |
| 2 | <div> |
| 3 | <CardEntity :loading :embedded hoverable> |
| 4 | <template #headerMain>{{ entity.name }}</template> |
| 5 | <template #headerExtra> |
| 6 | <div class="hidden font-sans sm:block"> |
| 7 | <ExclusionRuleStatusToggler |
| 8 | :entity |
| 9 | @loading="updatingStatus = $event" |
| 10 | @updated="setStatus($event)" |
| 11 | /> |
| 12 | </div> |
| 13 | </template> |
| 14 | <template #default> |
| 15 | {{ entity.title }} |
| 16 | </template> |
| 17 | <template #footerMain> |
| 18 | <div class="hidden flex-wrap items-center gap-3 sm:flex"> |
| 19 | <Badge type="splitted" color="primary"> |
| 20 | <template #iconLeft> |
| 21 | <Icon :name="TargetIcon" /> |
| 22 | </template> |
| 23 | <template #label>Match count</template> |
| 24 | <template #value>{{ entity.match_count }}</template> |
| 25 | </Badge> |
| 26 | |
| 27 | <Badge v-if="entity.last_matched_at" type="splitted" color="primary"> |
| 28 | <template #iconLeft> |
| 29 | <Icon :name="TimeIcon" /> |
| 30 | </template> |
| 31 | <template #label>Last match</template> |
| 32 | <template #value> |
| 33 | {{ formatDate(entity.last_matched_at, dFormats.datetimesec) }} |
| 34 | </template> |
| 35 | </Badge> |
| 36 | |
| 37 | <Badge v-if="entity.customer_code" type="splitted"> |
| 38 | <template #label>Customer</template> |
| 39 | <template #value> |
| 40 | <code |
| 41 | class="text-primary cursor-pointer leading-none" |
| 42 | @click.stop="routeCustomer({ code: entity.customer_code }).navigate()" |
| 43 | > |
| 44 | #{{ entity.customer_code }} |
| 45 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 46 | </code> |
| 47 | </template> |
| 48 | </Badge> |
| 49 | </div> |
| 50 | </template> |
| 51 | |
| 52 | <template #footerExtra> |
| 53 | <div class="flex items-center gap-3"> |
| 54 | <div class="block sm:hidden"> |
| 55 | <ExclusionRuleStatusToggler |
| 56 | :entity |
| 57 | @loading="updatingStatus = $event" |
| 58 | @updated="setStatus($event)" |
| 59 | /> |
| 60 | </div> |
| 61 | |
| 62 | <n-button size="small" @click.stop="openDetails()"> |
| 63 | <template #icon> |
| 64 | <Icon :name="DetailsIcon" /> |
| 65 | </template> |
| 66 | Details |
| 67 | </n-button> |
| 68 | </div> |
| 69 | </template> |
| 70 | </CardEntity> |
| 71 | |
| 72 | <n-modal |
| 73 | v-model:show="showDetails" |
| 74 | :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(480px, 90vh)', overflow: 'hidden' }" |
| 75 | display-directive="show" |
| 76 | > |
| 77 | <n-card |
| 78 | content-class="flex flex-col p-0!" |
| 79 | :title="`#${entity.id} • ${entity.name}`" |
| 80 | closable |
| 81 | :bordered="false" |
| 82 | segmented |
| 83 | role="modal" |
| 84 | @close="closeDetails()" |
| 85 | > |
| 86 | <n-spin :show="loadingDelete"> |
| 87 | <ExclusionRuleForm v-if="editing" :entity class="p-6" @submitted="updateEntity($event)"> |
| 88 | <template #additionalActions> |
| 89 | <n-button v-if="editing" @click="editing = false">Close</n-button> |
| 90 | </template> |
| 91 | </ExclusionRuleForm> |
| 92 | <ExclusionRuleDetails v-else :entity /> |
| 93 | </n-spin> |
| 94 | |
| 95 | <template #footer> |
| 96 | <div v-if="!editing" class="flex items-center justify-end gap-4"> |
| 97 | <n-button text type="error" ghost :loading="loadingDelete" @click="handleDelete"> |
| 98 | <template #icon> |
| 99 | <Icon :name="DeleteIcon" :size="15" /> |
| 100 | </template> |
| 101 | Delete |
| 102 | </n-button> |
| 103 | <n-button :disabled="loadingDelete" @click="editing = true"> |
| 104 | <template #icon> |
| 105 | <Icon :name="EditIcon" :size="14" /> |
| 106 | </template> |
| 107 | Edit |
| 108 | </n-button> |
| 109 | </div> |
| 110 | </template> |
| 111 | </n-card> |
| 112 | </n-modal> |
| 113 | </div> |
| 114 | </template> |
| 115 | |
| 116 | <script setup lang="ts"> |
| 117 | import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d" |
| 118 | import { NButton, NCard, NModal, NSpin, useDialog, useMessage } from "naive-ui" |
| 119 | import { computed, h, ref, toRefs } from "vue" |
| 120 | import Api from "@/api" |
| 121 | import Badge from "@/components/common/Badge.vue" |
| 122 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 123 | import Icon from "@/components/common/Icon.vue" |
| 124 | import { useNavigation } from "@/composables/useNavigation" |
| 125 | import { useSettingsStore } from "@/stores/settings" |
| 126 | import { formatDate } from "@/utils/format" |
| 127 | import ExclusionRuleDetails from "./ExclusionRuleDetails.vue" |
| 128 | import ExclusionRuleForm from "./ExclusionRuleForm.vue" |
| 129 | import ExclusionRuleStatusToggler from "./ExclusionRuleStatusToggler.vue" |
| 130 | |
| 131 | const props = defineProps<{ |
| 132 | entity: ExclusionRule |
| 133 | embedded?: boolean |
| 134 | }>() |
| 135 | |
| 136 | const emit = defineEmits<{ |
| 137 | (e: "deleted"): void |
| 138 | (e: "updated"): void |
| 139 | }>() |
| 140 | |
| 141 | const { entity, embedded } = toRefs(props) |
| 142 | |
| 143 | const TimeIcon = "carbon:time" |
| 144 | const LinkIcon = "carbon:launch" |
| 145 | const DetailsIcon = "carbon:settings-adjust" |
| 146 | const DeleteIcon = "ph:trash" |
| 147 | const EditIcon = "uil:edit-alt" |
| 148 | const TargetIcon = "zondicons:target" |
| 149 | |
| 150 | const message = useMessage() |
| 151 | const dialog = useDialog() |
| 152 | const updatingStatus = ref(false) |
| 153 | const loadingDelete = ref(false) |
| 154 | const loading = computed(() => updatingStatus.value || loadingDelete.value) |
| 155 | const editing = ref(false) |
| 156 | const showDetails = ref(false) |
| 157 | const { routeCustomer } = useNavigation() |
| 158 | const dFormats = useSettingsStore().dateFormat |
| 159 | |
| 160 | function openDetails() { |
| 161 | showDetails.value = true |
| 162 | } |
| 163 | |
| 164 | function closeDetails() { |
| 165 | showDetails.value = false |
| 166 | } |
| 167 | |
| 168 | function setStatus(value: ExclusionRule) { |
| 169 | entity.value.enabled = value.enabled |
| 170 | } |
| 171 | |
| 172 | function updateEntity(value: ExclusionRule) { |
| 173 | entity.value.name = value.name |
| 174 | entity.value.description = value.description |
| 175 | entity.value.channel = value.channel |
| 176 | entity.value.title = value.title |
| 177 | entity.value.field_matches = value.field_matches |
| 178 | entity.value.enabled = value.enabled |
| 179 | entity.value.customer_code = value.customer_code |
| 180 | |
| 181 | editing.value = false |
| 182 | emit("updated") |
| 183 | } |
| 184 | |
| 185 | function deleteExclusionRules() { |
| 186 | loadingDelete.value = true |
| 187 | |
| 188 | Api.incidentManagement.exclusionRules |
| 189 | .deleteExclusionRules(entity.value.id) |
| 190 | .then(res => { |
| 191 | if (res.data.success) { |
| 192 | showDetails.value = false |
| 193 | emit("deleted") |
| 194 | } else { |
| 195 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 196 | } |
| 197 | }) |
| 198 | .catch(err => { |
| 199 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 200 | }) |
| 201 | .finally(() => { |
| 202 | loadingDelete.value = false |
| 203 | }) |
| 204 | } |
| 205 | |
| 206 | function handleDelete() { |
| 207 | dialog.warning({ |
| 208 | title: "Confirm", |
| 209 | content: () => |
| 210 | h("div", { |
| 211 | innerHTML: `Are you sure you want to delete the Exclusion Rule: <strong>${entity.value.name}</strong> ?` |
| 212 | }), |
| 213 | positiveText: "Yes I'm sure", |
| 214 | negativeText: "Cancel", |
| 215 | onPositiveClick: () => { |
| 216 | deleteExclusionRules() |
| 217 | }, |
| 218 | onNegativeClick: () => { |
| 219 | message.info("Delete canceled") |
| 220 | } |
| 221 | }) |
| 222 | } |
| 223 | </script> |