|
|
@@ -0,0 +1,452 @@
|
|
|
+<script setup>
|
|
|
+import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import LayoutSistema from "../../layout/LayoutSistema.vue";
|
|
|
+import BaseBadge from "../../components/base/BaseBadge.vue";
|
|
|
+import BaseButton from "../../components/base/BaseButton.vue";
|
|
|
+import BaseDropdown from "../../components/base/BaseDropdown.vue";
|
|
|
+import { estatisticasAvaliacoes, listarAvaliacoes, listarSetoresAvaliacoes } from "../../api/atendimentos.js";
|
|
|
+import {
|
|
|
+ cancelamentoLabel,
|
|
|
+ cancelamentoVariant,
|
|
|
+ CONTEXTO_IXC_ERRO_LABEL,
|
|
|
+ contextoIxcFalhou,
|
|
|
+ resolvidoLabel,
|
|
|
+ resolvidoVariant,
|
|
|
+ scoreVariant,
|
|
|
+ sentimentoLabel,
|
|
|
+ sentimentoVariant
|
|
|
+} from "../../utils/avaliacoes.js";
|
|
|
+
|
|
|
+const opcoesSentimento = Object.entries(sentimentoLabel).map(([value, label]) => ({ value, label }));
|
|
|
+
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+function formatISO(date) {
|
|
|
+ return date.toISOString().slice(0, 10);
|
|
|
+}
|
|
|
+
|
|
|
+function padraoDataInicio() {
|
|
|
+ const d = new Date();
|
|
|
+ d.setDate(d.getDate() - 30);
|
|
|
+ return formatISO(d);
|
|
|
+}
|
|
|
+
|
|
|
+const carregando = ref(false);
|
|
|
+const erro = ref("");
|
|
|
+
|
|
|
+const stats = ref(null);
|
|
|
+const statsGeral = ref(null);
|
|
|
+const avaliacoes = ref([]);
|
|
|
+const total = ref(0);
|
|
|
+const setores = ref([]);
|
|
|
+const pageSize = 20;
|
|
|
+
|
|
|
+const page = ref(Math.max(1, Number(route.query.page) || 1));
|
|
|
+const expandido = ref(route.query.aberto ? Number(route.query.aberto) : null);
|
|
|
+const filtroSetor = ref(typeof route.query.setor === "string" ? route.query.setor : "");
|
|
|
+const filtroSentimento = ref(typeof route.query.sentimento === "string" ? route.query.sentimento : "");
|
|
|
+const filtroBusca = ref(typeof route.query.q === "string" ? route.query.q : "");
|
|
|
+const filtroOrdenacao = ref(typeof route.query.ordenacao === "string" ? route.query.ordenacao : "desc");
|
|
|
+const filtroDataInicio = ref(typeof route.query.dataInicio === "string" ? route.query.dataInicio : padraoDataInicio());
|
|
|
+const filtroDataFim = ref(typeof route.query.dataFim === "string" ? route.query.dataFim : formatISO(new Date()));
|
|
|
+
|
|
|
+function sincronizarQuery() {
|
|
|
+ const query = {};
|
|
|
+ if (page.value > 1) query.page = String(page.value);
|
|
|
+ if (filtroSetor.value) query.setor = filtroSetor.value;
|
|
|
+ if (filtroSentimento.value) query.sentimento = filtroSentimento.value;
|
|
|
+ if (filtroBusca.value.trim()) query.q = filtroBusca.value.trim();
|
|
|
+ if (filtroOrdenacao.value === "asc") query.ordenacao = "asc";
|
|
|
+ if (filtroDataInicio.value) query.dataInicio = filtroDataInicio.value;
|
|
|
+ if (filtroDataFim.value) query.dataFim = filtroDataFim.value;
|
|
|
+ if (expandido.value) query.aberto = String(expandido.value);
|
|
|
+ router.replace({ query });
|
|
|
+}
|
|
|
+
|
|
|
+const totalPaginas = computed(() => Math.max(1, Math.ceil(total.value / pageSize)));
|
|
|
+
|
|
|
+function formatData(value) {
|
|
|
+ if (!value) return "";
|
|
|
+ const d = new Date(value);
|
|
|
+ if (Number.isNaN(d.getTime())) return "";
|
|
|
+ return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
|
+}
|
|
|
+
|
|
|
+function pct(parte, todo) {
|
|
|
+ if (!todo) return "0%";
|
|
|
+ return `${Math.round((parte / todo) * 100)}%`;
|
|
|
+}
|
|
|
+
|
|
|
+const setoresOrdenados = computed(() => {
|
|
|
+ const mapa = stats.value?.porSetor ?? {};
|
|
|
+ return Object.entries(mapa)
|
|
|
+ .sort((a, b) => b[1] - a[1])
|
|
|
+ .slice(0, 6);
|
|
|
+});
|
|
|
+
|
|
|
+let statsRequestId = 0;
|
|
|
+
|
|
|
+async function carregarStats() {
|
|
|
+ const requestId = ++statsRequestId;
|
|
|
+ try {
|
|
|
+ const filtrosComuns = {
|
|
|
+ setor: filtroSetor.value || undefined,
|
|
|
+ sentimento: filtroSentimento.value || undefined,
|
|
|
+ busca: filtroBusca.value.trim() || undefined,
|
|
|
+ dataInicio: filtroDataInicio.value || undefined,
|
|
|
+ dataFim: filtroDataFim.value || undefined
|
|
|
+ };
|
|
|
+ // duas chamadas: "geral" (sem resolvido) só pra saber o total avaliado no período, usado
|
|
|
+ // no % de não resolvidos; "nao" filtrada pra score médio e distribuição por setor
|
|
|
+ // específicos dos não resolvidos (senão o porSetor mistura todos os status)
|
|
|
+ const [geral, naoResolvidos] = await Promise.all([
|
|
|
+ estatisticasAvaliacoes(filtrosComuns),
|
|
|
+ estatisticasAvaliacoes({ ...filtrosComuns, resolvido: "nao" })
|
|
|
+ ]);
|
|
|
+ if (requestId !== statsRequestId) return;
|
|
|
+ statsGeral.value = geral;
|
|
|
+ stats.value = naoResolvidos;
|
|
|
+ } catch {
|
|
|
+ if (requestId !== statsRequestId) return;
|
|
|
+ statsGeral.value = null;
|
|
|
+ stats.value = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function carregarSetores() {
|
|
|
+ try {
|
|
|
+ const r = await listarSetoresAvaliacoes();
|
|
|
+ setores.value = r.results ?? [];
|
|
|
+ } catch {
|
|
|
+ setores.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+let listaRequestId = 0;
|
|
|
+
|
|
|
+async function carregarLista() {
|
|
|
+ const requestId = ++listaRequestId;
|
|
|
+ carregando.value = true;
|
|
|
+ erro.value = "";
|
|
|
+ try {
|
|
|
+ const r = await listarAvaliacoes({
|
|
|
+ page: page.value,
|
|
|
+ pageSize,
|
|
|
+ resolvido: "nao",
|
|
|
+ setor: filtroSetor.value || undefined,
|
|
|
+ sentimento: filtroSentimento.value || undefined,
|
|
|
+ busca: filtroBusca.value.trim() || undefined,
|
|
|
+ ordenacao: filtroOrdenacao.value,
|
|
|
+ dataInicio: filtroDataInicio.value || undefined,
|
|
|
+ dataFim: filtroDataFim.value || undefined
|
|
|
+ });
|
|
|
+ if (requestId !== listaRequestId) return;
|
|
|
+ avaliacoes.value = r.results ?? [];
|
|
|
+ total.value = r.total ?? 0;
|
|
|
+ } catch (err) {
|
|
|
+ if (requestId !== listaRequestId) return;
|
|
|
+ erro.value = err?.message ?? "Falha ao carregar atendimentos não resolvidos";
|
|
|
+ } finally {
|
|
|
+ if (requestId === listaRequestId) carregando.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function aplicarFiltros() {
|
|
|
+ page.value = 1;
|
|
|
+ expandido.value = null;
|
|
|
+ sincronizarQuery();
|
|
|
+ carregarStats();
|
|
|
+ carregarLista();
|
|
|
+}
|
|
|
+
|
|
|
+function alternarOrdenacao() {
|
|
|
+ filtroOrdenacao.value = filtroOrdenacao.value === "asc" ? "desc" : "asc";
|
|
|
+ aplicarFiltros();
|
|
|
+}
|
|
|
+
|
|
|
+let buscaTimer = null;
|
|
|
+
|
|
|
+function onBuscaInput() {
|
|
|
+ clearTimeout(buscaTimer);
|
|
|
+ buscaTimer = setTimeout(aplicarFiltros, 400);
|
|
|
+}
|
|
|
+
|
|
|
+function onBuscaEnter() {
|
|
|
+ clearTimeout(buscaTimer);
|
|
|
+ aplicarFiltros();
|
|
|
+}
|
|
|
+
|
|
|
+function irParaPagina(nova) {
|
|
|
+ if (nova < 1 || nova > totalPaginas.value || nova === page.value) return;
|
|
|
+ page.value = nova;
|
|
|
+ expandido.value = null;
|
|
|
+ sincronizarQuery();
|
|
|
+ carregarLista();
|
|
|
+}
|
|
|
+
|
|
|
+function alternarExpandido(id) {
|
|
|
+ expandido.value = expandido.value === id ? null : id;
|
|
|
+ sincronizarQuery();
|
|
|
+}
|
|
|
+
|
|
|
+function verConversa(id) {
|
|
|
+ router.push({ name: "atendimento", params: { id } });
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ carregarStats();
|
|
|
+ carregarSetores();
|
|
|
+ await carregarLista();
|
|
|
+
|
|
|
+ if (expandido.value) {
|
|
|
+ await nextTick();
|
|
|
+ document.getElementById(`nao-resolvido-${expandido.value}`)?.scrollIntoView({ block: "center" });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ clearTimeout(buscaTimer);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <LayoutSistema>
|
|
|
+ <div class="min-h-[calc(100vh-var(--layout-header-height))] px-4 pt-8 pb-12">
|
|
|
+ <div class="mx-auto w-full max-w-[960px] space-y-6">
|
|
|
+ <section
|
|
|
+ class="rounded-2xl border border-gray-200 bg-gray-100/50 p-6 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50"
|
|
|
+ >
|
|
|
+ <div class="flex flex-wrap items-center justify-between gap-4">
|
|
|
+ <div class="flex items-center gap-4">
|
|
|
+ <div
|
|
|
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red-500/10 text-red-600 dark:bg-red-500/20 dark:text-red-400"
|
|
|
+ >
|
|
|
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a1.5 1.5 0 0 0 1.3 2.25h17.76a1.5 1.5 0 0 0 1.3-2.25L13.71 3.86a1.5 1.5 0 0 0-2.42 0Z" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="min-w-0">
|
|
|
+ <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
|
+ Atendimentos Não Resolvidos (IA)
|
|
|
+ </h2>
|
|
|
+ <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
|
|
+ Atendimentos marcados como não resolvidos pelo avaliador automático, por período e setor.
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <input
|
|
|
+ v-model="filtroDataInicio"
|
|
|
+ type="date"
|
|
|
+ class="rounded-lg border border-gray-200 bg-background px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200"
|
|
|
+ @change="aplicarFiltros"
|
|
|
+ />
|
|
|
+ <span class="text-sm text-gray-400 dark:text-gray-500">até</span>
|
|
|
+ <input
|
|
|
+ v-model="filtroDataFim"
|
|
|
+ type="date"
|
|
|
+ class="rounded-lg border border-gray-200 bg-background px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200"
|
|
|
+ @change="aplicarFiltros"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="erro" class="mt-4 rounded-lg bg-red-50 px-4 py-3 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
|
|
|
+ {{ erro }}
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section v-if="stats" class="grid grid-cols-2 gap-3 md:grid-cols-3">
|
|
|
+ <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">Não resolvidos no período</div>
|
|
|
+ <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
+ {{ stats.avaliados }}
|
|
|
+ </div>
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ {{ pct(stats.avaliados, statsGeral?.avaliados) }} dos {{ statsGeral?.avaliados ?? 0 }} avaliados
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">Score médio dos não resolvidos</div>
|
|
|
+ <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
+ {{ stats.scoreMedio ?? "—" }}<span class="text-sm font-normal text-gray-400"> / 10</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="rounded-2xl border border-gray-200 bg-background/70 p-4 shadow-sm dark:border-gray-700 dark:bg-background/20">
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">Por setor</div>
|
|
|
+ <div class="mt-1.5 space-y-1">
|
|
|
+ <div v-if="setoresOrdenados.length === 0" class="text-sm text-gray-400 dark:text-gray-500">—</div>
|
|
|
+ <div v-for="[sigla, qtd] in setoresOrdenados" :key="sigla" class="flex items-center justify-between text-xs">
|
|
|
+ <span class="text-gray-600 dark:text-gray-300">{{ sigla }}</span>
|
|
|
+ <span class="font-medium text-gray-900 dark:text-gray-100">{{ qtd }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">
|
|
|
+ <div class="flex flex-wrap items-center gap-3 border-b border-gray-200 p-4 dark:border-gray-700">
|
|
|
+ <div class="relative min-w-[240px] flex-1 sm:max-w-[340px]">
|
|
|
+ <svg
|
|
|
+ class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ >
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.35-4.35M17 10.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0Z" />
|
|
|
+ </svg>
|
|
|
+ <input
|
|
|
+ v-model="filtroBusca"
|
|
|
+ type="search"
|
|
|
+ placeholder="Protocolo, cliente, atendente ou data (dd/mm/aaaa)"
|
|
|
+ class="w-full rounded-lg border border-gray-200 bg-background py-1.5 pl-9 pr-8 text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary/30 dark:border-gray-700 dark:text-gray-200 dark:placeholder:text-gray-500"
|
|
|
+ @input="onBuscaInput"
|
|
|
+ @keyup.enter="onBuscaEnter"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ v-if="filtroBusca"
|
|
|
+ type="button"
|
|
|
+ class="absolute right-2 top-1/2 -translate-y-1/2 rounded bg-transparent p-0.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
|
|
+ title="Limpar busca"
|
|
|
+ @click="filtroBusca = ''; onBuscaEnter();"
|
|
|
+ >
|
|
|
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="flex shrink-0 items-center gap-2">
|
|
|
+ <BaseDropdown
|
|
|
+ v-model="filtroSetor"
|
|
|
+ rotulo="Setor"
|
|
|
+ rotulo-padrao="todos"
|
|
|
+ :opcoes="setores.map((s) => ({ value: s.sigla, label: `${s.nome} (${s.total})` }))"
|
|
|
+ @change="aplicarFiltros"
|
|
|
+ />
|
|
|
+ <BaseDropdown
|
|
|
+ v-model="filtroSentimento"
|
|
|
+ rotulo="Sentimento"
|
|
|
+ rotulo-padrao="todos"
|
|
|
+ :opcoes="opcoesSentimento"
|
|
|
+ @change="aplicarFiltros"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <BaseButton
|
|
|
+ variant="secondary"
|
|
|
+ size="sm"
|
|
|
+ title="Alternar ordem por data da avaliação"
|
|
|
+ @click="alternarOrdenacao"
|
|
|
+ >
|
|
|
+ Data
|
|
|
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <path v-if="filtroOrdenacao === 'asc'" stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
|
|
|
+ <path v-else stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
|
+ </svg>
|
|
|
+ </BaseButton>
|
|
|
+ <div class="ml-auto text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ {{ total }} não resolvidos
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="carregando" class="p-6 text-sm text-gray-500 dark:text-gray-400">Carregando...</div>
|
|
|
+ <div v-else-if="avaliacoes.length === 0" class="p-6 text-sm text-gray-500 dark:text-gray-400">
|
|
|
+ Nenhum atendimento não resolvido encontrado para os filtros selecionados.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ul v-else class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <li v-for="a in avaliacoes" :id="`nao-resolvido-${a.AtendimentoId}`" :key="a.AtendimentoId">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="flex w-full flex-wrap items-center gap-2 bg-transparent px-4 py-3 text-left hover:bg-black/[0.03] dark:hover:bg-white/[0.04]"
|
|
|
+ @click="alternarExpandido(a.AtendimentoId)"
|
|
|
+ >
|
|
|
+ <div class="min-w-0 flex-1">
|
|
|
+ <div class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
|
+ {{ a.Codigo ?? `#${a.AtendimentoId}` }}
|
|
|
+ <span class="font-normal text-gray-500 dark:text-gray-400"> — {{ a.ClienteNome ?? "cliente desconhecido" }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ <span v-if="a.Setor">{{ a.Setor }} · </span>{{ formatData(a.Abertura) }}
|
|
|
+ <span v-if="a.Atendentes?.length"> · {{ a.Atendentes.join(", ") }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <BaseBadge :variant="scoreVariant(a.ScoreAtendente)">
|
|
|
+ {{ a.ScoreAtendente !== null && a.ScoreAtendente !== undefined ? `Nota ${a.ScoreAtendente}` : "Sem nota" }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge :variant="resolvidoVariant[a.Resolvido] ?? 'default'">{{ resolvidoLabel[a.Resolvido] ?? a.Resolvido }}</BaseBadge>
|
|
|
+ <BaseBadge :variant="sentimentoVariant[a.Sentimento] ?? 'default'">{{ sentimentoLabel[a.Sentimento] ?? a.Sentimento }}</BaseBadge>
|
|
|
+ <BaseBadge v-if="a.VisitaAgendada" variant="violet">
|
|
|
+ Visita{{ a.HorarioVisitaInformado === "sim" ? " agendada" : " sem horário" }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge v-if="cancelamentoLabel[a.StatusCancelamento]" :variant="cancelamentoVariant[a.StatusCancelamento]">
|
|
|
+ {{ cancelamentoLabel[a.StatusCancelamento] }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge v-if="contextoIxcFalhou(a.ContextoIxcStatus)" variant="warning" :title="CONTEXTO_IXC_ERRO_LABEL">
|
|
|
+ Contexto ERP indisponível
|
|
|
+ </BaseBadge>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div v-if="expandido === a.AtendimentoId" class="space-y-3 px-4 pb-4 text-sm">
|
|
|
+ <div v-if="a.Resumo">
|
|
|
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Resumo</div>
|
|
|
+ <p class="mt-1 text-gray-700 dark:text-gray-300">{{ a.Resumo }}</p>
|
|
|
+ </div>
|
|
|
+ <div v-if="a.JustificativaScore">
|
|
|
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Justificativa da nota</div>
|
|
|
+ <p class="mt-1 text-gray-700 dark:text-gray-300">{{ a.JustificativaScore }}</p>
|
|
|
+ </div>
|
|
|
+ <div v-if="a.MotivoCancelamento">
|
|
|
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Motivo do cancelamento</div>
|
|
|
+ <p class="mt-1 text-gray-700 dark:text-gray-300">{{ a.MotivoCancelamento }}</p>
|
|
|
+ </div>
|
|
|
+ <div v-if="a.Sugestoes?.length">
|
|
|
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Itens a melhorar</div>
|
|
|
+ <ul class="mt-1 list-disc space-y-0.5 pl-5 text-gray-700 dark:text-gray-300">
|
|
|
+ <li v-for="(s, i) in a.Sugestoes" :key="i">{{ s }}</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
+ <BaseButton variant="secondary" size="sm" @click="verConversa(a.AtendimentoId)">
|
|
|
+ Ver conversa completa
|
|
|
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
|
+ </svg>
|
|
|
+ </BaseButton>
|
|
|
+ <div class="text-xs text-gray-400 dark:text-gray-500">
|
|
|
+ Avaliado por {{ a.Modelo }} em {{ formatData(a.UpdatedAt) }} · {{ a.TotalMensagens }} mensagens
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="totalPaginas > 1"
|
|
|
+ class="flex items-center justify-between border-t border-gray-200 px-4 py-3 text-sm dark:border-gray-700"
|
|
|
+ >
|
|
|
+ <BaseButton
|
|
|
+ variant="secondary"
|
|
|
+ size="sm"
|
|
|
+ :disabled="page <= 1 || carregando"
|
|
|
+ @click="irParaPagina(page - 1)"
|
|
|
+ >
|
|
|
+ Anterior
|
|
|
+ </BaseButton>
|
|
|
+ <span class="text-gray-500 dark:text-gray-400">Página {{ page }} de {{ totalPaginas }}</span>
|
|
|
+ <BaseButton
|
|
|
+ variant="secondary"
|
|
|
+ size="sm"
|
|
|
+ :disabled="page >= totalPaginas || carregando"
|
|
|
+ @click="irParaPagina(page + 1)"
|
|
|
+ >
|
|
|
+ Próxima
|
|
|
+ </BaseButton>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </LayoutSistema>
|
|
|
+</template>
|