|
|
@@ -10,7 +10,15 @@ import {
|
|
|
listarAvaliacoes,
|
|
|
listarSetoresAvaliacoes
|
|
|
} from "../../api/atendimentos.js";
|
|
|
-import { resolvidoLabel, resolvidoVariant, scoreVariant, sentimentoLabel, sentimentoVariant } from "../../utils/avaliacoes.js";
|
|
|
+import {
|
|
|
+ cancelamentoLabel,
|
|
|
+ cancelamentoVariant,
|
|
|
+ resolvidoLabel,
|
|
|
+ resolvidoVariant,
|
|
|
+ scoreVariant,
|
|
|
+ sentimentoLabel,
|
|
|
+ sentimentoVariant
|
|
|
+} from "../../utils/avaliacoes.js";
|
|
|
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
@@ -26,14 +34,13 @@ const total = ref(0);
|
|
|
const setores = ref([]);
|
|
|
const pageSize = 20;
|
|
|
|
|
|
-// página, filtros e avaliação aberta vivem na query string para o gestor não perder
|
|
|
-// o ponto da revisão ao entrar na conversa completa e voltar
|
|
|
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 filtroResolvido = ref(typeof route.query.resolvido === "string" ? route.query.resolvido : "");
|
|
|
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");
|
|
|
|
|
|
function sincronizarQuery() {
|
|
|
const query = {};
|
|
|
@@ -42,6 +49,7 @@ function sincronizarQuery() {
|
|
|
if (filtroResolvido.value) query.resolvido = filtroResolvido.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 (expandido.value) query.aberto = String(expandido.value);
|
|
|
router.replace({ query });
|
|
|
}
|
|
|
@@ -93,8 +101,10 @@ async function carregarLista() {
|
|
|
pageSize,
|
|
|
setor: filtroSetor.value || undefined,
|
|
|
resolvido: filtroResolvido.value || undefined,
|
|
|
- sentimento: filtroSentimento.value || undefined,
|
|
|
- busca: filtroBusca.value.trim() || undefined
|
|
|
+ sentimento: filtroSentimento.value === "cancelados" ? undefined : filtroSentimento.value || undefined,
|
|
|
+ statusCancelamento: filtroSentimento.value === "cancelados" ? "cancelou" : undefined,
|
|
|
+ busca: filtroBusca.value.trim() || undefined,
|
|
|
+ ordenacao: filtroOrdenacao.value
|
|
|
});
|
|
|
avaliacoes.value = r.results ?? [];
|
|
|
total.value = r.total ?? 0;
|
|
|
@@ -113,7 +123,11 @@ function aplicarFiltros() {
|
|
|
carregarLista();
|
|
|
}
|
|
|
|
|
|
-// busca digitável: espera a pessoa parar de digitar antes de consultar
|
|
|
+function alternarOrdenacao() {
|
|
|
+ filtroOrdenacao.value = filtroOrdenacao.value === "asc" ? "desc" : "asc";
|
|
|
+ aplicarFiltros();
|
|
|
+}
|
|
|
+
|
|
|
let buscaTimer = null;
|
|
|
|
|
|
function onBuscaInput() {
|
|
|
@@ -246,7 +260,7 @@ onBeforeUnmount(() => {
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
- <section v-if="stats" class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
|
+ <section v-if="stats" class="grid grid-cols-2 gap-3 md:grid-cols-5">
|
|
|
<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</div>
|
|
|
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
@@ -276,6 +290,15 @@ onBeforeUnmount(() => {
|
|
|
{{ stats.sentimento?.negativo ?? 0 }} saíram insatisfeitos
|
|
|
</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">Cancelamentos</div>
|
|
|
+ <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
+ {{ pct(stats.cancelamento?.cancelou ?? 0, stats.avaliados) }}
|
|
|
+ </div>
|
|
|
+ <div class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ {{ stats.cancelamento?.cancelou ?? 0 }} cancelaram · {{ stats.cancelamento?.ameacou ?? 0 }} ameaçaram cancelar
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</section>
|
|
|
|
|
|
<section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">
|
|
|
@@ -339,7 +362,20 @@ onBeforeUnmount(() => {
|
|
|
<option value="neutro">Neutro</option>
|
|
|
<option value="negativo">Negativo</option>
|
|
|
<option value="indefinido">Indefinido</option>
|
|
|
+ <option value="cancelados">Cancelados</option>
|
|
|
</select>
|
|
|
+ <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 }} avaliações
|
|
|
</div>
|
|
|
@@ -375,6 +411,9 @@ onBeforeUnmount(() => {
|
|
|
<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>
|
|
|
</button>
|
|
|
|
|
|
<div v-if="expandido === a.AtendimentoId" class="space-y-3 px-4 pb-4 text-sm">
|
|
|
@@ -386,6 +425,10 @@ onBeforeUnmount(() => {
|
|
|
<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">
|