| 1 | <template> |
| 2 | <div class="flex flex-col gap-4"> |
| 3 | <div class="flex items-center justify-between gap-4"> |
| 4 | <div class="text-secondary text-sm"> |
| 5 | Recent notification dispatches for this customer (newest first, capped at 100). |
| 6 | </div> |
| 7 | <n-button size="small" :disabled="loading" @click="refreshList()"> |
| 8 | <template #icon> |
| 9 | <Icon :name="RefreshIcon" :size="14" /> |
| 10 | </template> |
| 11 | Refresh |
| 12 | </n-button> |
| 13 | </div> |
| 14 | |
| 15 | <n-spin :show="loading"> |
| 16 | <div class="min-h-52"> |
| 17 | <n-empty |
| 18 | v-if="!loading && !entries.length" |
| 19 | description="No dispatches yet" |
| 20 | class="h-48 justify-center" |
| 21 | /> |
| 22 | |
| 23 | <n-data-table |
| 24 | v-else-if="entries.length" |
| 25 | :columns |
| 26 | :data="entries" |
| 27 | :scroll-x="1000" |
| 28 | size="small" |
| 29 | :row-key="(r: DispatchLogEntry) => r.id" |
| 30 | /> |
| 31 | </div> |
| 32 | </n-spin> |
| 33 | </div> |
| 34 | </template> |
| 35 | |
| 36 | <script setup lang="ts"> |
| 37 | import type { DataTableColumns } from "naive-ui" |
| 38 | import type { NotificationDispatchLogEntry as DispatchLogEntry } from "@/types/notifications.d" |
| 39 | import { NButton, NDataTable, NEmpty, NSpin, useMessage } from "naive-ui" |
| 40 | import { computed, h, onBeforeMount, ref } from "vue" |
| 41 | import Api from "@/api" |
| 42 | import Badge from "@/components/common/Badge.vue" |
| 43 | import Icon from "@/components/common/Icon.vue" |
| 44 | import { useSettingsStore } from "@/stores/settings" |
| 45 | import { getApiErrorMessage } from "@/utils" |
| 46 | import { formatDate } from "@/utils/format" |
| 47 | |
| 48 | const { customerCode } = defineProps<{ |
| 49 | customerCode: string |
| 50 | }>() |
| 51 | |
| 52 | const RefreshIcon = "carbon:renew" |
| 53 | |
| 54 | const message = useMessage() |
| 55 | const loading = ref(false) |
| 56 | const entries = ref<DispatchLogEntry[]>([]) |
| 57 | |
| 58 | const dFormats = useSettingsStore().dateFormat |
| 59 | |
| 60 | function statusColor(status: string): "success" | "warning" | "danger" | undefined { |
| 61 | if (status === "sent") return "success" |
| 62 | if (status === "skipped") return "warning" |
| 63 | if (status === "failed") return "danger" |
| 64 | return undefined |
| 65 | } |
| 66 | |
| 67 | const columns = computed<DataTableColumns<DispatchLogEntry>>(() => [ |
| 68 | { |
| 69 | title: "When", |
| 70 | key: "dispatched_at", |
| 71 | width: 160, |
| 72 | render: row => String(formatDate(row.dispatched_at, dFormats.datetime)) |
| 73 | }, |
| 74 | { |
| 75 | title: "Alert", |
| 76 | key: "alert_id", |
| 77 | width: 80, |
| 78 | render: row => `#${row.alert_id}` |
| 79 | }, |
| 80 | { |
| 81 | title: "Trigger", |
| 82 | key: "trigger", |
| 83 | width: 220, |
| 84 | render: row => (row.trigger === "investigation_complete" ? "Every investigation" : "Critical / High only") |
| 85 | }, |
| 86 | { |
| 87 | title: "Status", |
| 88 | key: "status", |
| 89 | minWidth: 130, |
| 90 | render: row => |
| 91 | h( |
| 92 | Badge, |
| 93 | { type: "splitted", color: statusColor(row.status), class: "whitespace-nowrap" }, |
| 94 | { |
| 95 | label: () => "Status", |
| 96 | value: () => row.status |
| 97 | } |
| 98 | ) |
| 99 | }, |
| 100 | { |
| 101 | title: "Latency", |
| 102 | key: "latency_ms", |
| 103 | width: 100, |
| 104 | render: row => (row.latency_ms == null ? "—" : `${row.latency_ms} ms`) |
| 105 | }, |
| 106 | { |
| 107 | title: "Error / Preview", |
| 108 | key: "detail", |
| 109 | maxWidth: 200, |
| 110 | ellipsis: { tooltip: { to: "body ", class: "max-w-[90vw] text-sm" } }, |
| 111 | render: row => row.error_message || row.payload_preview || "" |
| 112 | } |
| 113 | ]) |
| 114 | |
| 115 | function refreshList() { |
| 116 | loading.value = true |
| 117 | Api.notifications |
| 118 | .listDispatchLog(customerCode) |
| 119 | .then(res => { |
| 120 | if (res.data.success) { |
| 121 | entries.value = res.data.entries |
| 122 | } else { |
| 123 | message.warning(res.data.message || "Failed to load dispatch log") |
| 124 | } |
| 125 | }) |
| 126 | .catch(err => { |
| 127 | message.error(getApiErrorMessage(err) || "Failed to load dispatch log") |
| 128 | }) |
| 129 | .finally(() => { |
| 130 | loading.value = false |
| 131 | }) |
| 132 | } |
| 133 | |
| 134 | onBeforeMount(refreshList) |
| 135 | </script> |