Newer
Older
gn-synapse / frontend / src / views / DeliveriesView.vue
<script setup>
// Лог доставок: последние N с фильтрами по каналу и статусу.
import { onMounted, ref } from "vue";
import {
  GnBadge,
  GnButton,
  GnIconButton,
  GnLoader,
  GnPageHeader,
  GnSelect,
  GnStatusCard,
  GnTable,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { api } from "../api.js";
import { badgeFor, fmtTime, truncate } from "../ui.js";

const toast = useToast();

const rows = ref([]);
const loading = ref(true);
const error = ref("");

const CHANNELS = [
  { value: "", label: "все" },
  { value: "telegram", label: "telegram" },
  { value: "email", label: "email" },
  { value: "s2s", label: "s2s" },
  { value: "internal_log", label: "internal_log" },
];
const STATUSES = [
  { value: "", label: "все" },
  { value: "pending", label: "pending" },
  { value: "delivered", label: "delivered" },
  { value: "failed", label: "failed" },
  { value: "skipped", label: "skipped" },
];

const channel = ref("");
const status = ref("");

async function load() {
  loading.value = true;
  error.value = "";
  try {
    const query = { limit: 100 };
    if (channel.value) query.channel = channel.value;
    if (status.value) query.status = status.value;
    rows.value = await api.listDeliveries(query);
  } catch (ex) {
    error.value = ex.message;
    toast.error(ex.message);
  } finally {
    loading.value = false;
  }
}

onMounted(load);
</script>

<template>
  <main class="container page">
    <GnPageHeader
      kicker="Synapse"
      title="Доставки"
      subtitle="Лог попыток: канал, цель, статус, ретраи и текст, реально ушедший в канал"
    >
      <template #actions>
        <GnSelect v-model="status" label="" :options="STATUSES" @update:model-value="load" />
        <GnSelect v-model="channel" label="" :options="CHANNELS" @update:model-value="load" />
        <GnIconButton icon="ph-arrow-counter-clockwise" label="Обновить" @click="load" />
      </template>
    </GnPageHeader>

    <GnLoader v-if="loading" />
    <GnStatusCard v-else-if="error" variant="error" icon="ph-warning-circle" :text="error" />
    <template v-else>
      <GnTable
        :rows="rows"
        empty-text="Доставок ещё не было"
        :columns="[
          { key: 'created_at', label: 'Время' },
          { key: 'event', label: 'Событие' },
          { key: 'channel', label: 'Канал' },
          { key: 'target_name', label: 'Цель' },
          { key: 'status', label: 'Статус' },
          { key: 'attempts', label: 'Попытки' },
          { key: 'last_error', label: 'Ошибка' },
          { key: 'rendered_message', label: 'Сообщение' },
        ]"
      >
        <template #cell-created_at="{ value }">{{ fmtTime(value) }}</template>
        <template #cell-event="{ row }">
          <code class="event-id">{{ truncate(row.event_id, 13) }}</code>
        </template>
        <template #cell-status="{ value }">
          <GnBadge :variant="badgeFor(value).variant" outline>{{ value }}</GnBadge>
        </template>
        <template #cell-attempts="{ value }">{{ value ?? 0 }}</template>
        <template #cell-last_error="{ value }">{{ truncate(value, 60) || "—" }}</template>
        <template #cell-rendered_message="{ value }">
          <span :title="value || ''">{{ truncate(value, 60) || "—" }}</span>
        </template>
      </GnTable>
    </template>
  </main>
</template>

<style scoped>
.page {
  max-width: 1440px;
  margin-inline: auto;
  padding-block: var(--gn-space-6);
  display: flex;
  flex-direction: column;
  gap: var(--gn-space-4);
}
.event-id {
  font-size: 0.78em;
}
</style>