|
|
@@ -5,7 +5,8 @@ import LayoutSistema from "../../layout/LayoutSistema.vue";
|
|
|
import BaseBadge from "../../components/base/BaseBadge.vue";
|
|
|
import BaseButton from "../../components/base/BaseButton.vue";
|
|
|
import ModalMidia from "../../components/ModalMidia.vue";
|
|
|
-import { obterAtendimento, salvarGoldenLabel } from "../../api/atendimentos.js";
|
|
|
+import { obterAtendimento, salvarGoldenLabel, sincronizarAtendimento, avaliarAtendimento } from "../../api/atendimentos.js";
|
|
|
+import { useAuth } from "../../composables/useAuth.js";
|
|
|
import {
|
|
|
cancelamentoLabel,
|
|
|
cancelamentoVariant,
|
|
|
@@ -23,6 +24,8 @@ import { useToast } from "../../composables/useToast.js";
|
|
|
|
|
|
const HORARIO_VISITA_INFORMADO_LABEL = { sim: "Sim", nao: "Não", nao_se_aplica: "Não se aplica" };
|
|
|
const toast = useToast();
|
|
|
+const { user } = useAuth();
|
|
|
+const isAdmin = computed(() => String(user.value?.Nivel) === "3");
|
|
|
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
@@ -31,6 +34,8 @@ const carregando = ref(true);
|
|
|
const erro = ref("");
|
|
|
const atendimento = ref(null);
|
|
|
const mediaViewer = ref(null);
|
|
|
+const sincronizandoIfbot = ref(false);
|
|
|
+const reavaliando = ref(false);
|
|
|
|
|
|
const avaliacao = computed(() => atendimento.value?.avaliacao ?? null);
|
|
|
const goldenLabel = computed(() => atendimento.value?.goldenLabel ?? null);
|
|
|
@@ -177,6 +182,39 @@ async function carregar(id) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// re-sincroniza este atendimento direto pelo Id, sem passar pelo filtro de "protocolos
|
|
|
+// abertos" — funciona mesmo se o atendimento já estiver fechado no ifbot, cobrindo o
|
|
|
+// caso de alguém ter transcrito um áudio manualmente depois do encerramento
|
|
|
+async function atualizarDoIfbot() {
|
|
|
+ if (sincronizandoIfbot.value || !atendimento.value) return;
|
|
|
+ sincronizandoIfbot.value = true;
|
|
|
+ try {
|
|
|
+ await sincronizarAtendimento(atendimento.value.Id);
|
|
|
+ await carregar(atendimento.value.Id);
|
|
|
+ toast.sucesso("Atendimento atualizado com os dados mais recentes do ifbot.");
|
|
|
+ } catch (err) {
|
|
|
+ toast.erro(err?.message ?? "Falha ao atualizar o atendimento.");
|
|
|
+ } finally {
|
|
|
+ sincronizandoIfbot.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// roda a avaliação da IA de novo pra este atendimento — útil depois de "Atualizar do
|
|
|
+// ifbot" trazer uma transcrição nova, já que a avaliação anterior não se atualiza sozinha
|
|
|
+async function reavaliar() {
|
|
|
+ if (reavaliando.value || !atendimento.value) return;
|
|
|
+ reavaliando.value = true;
|
|
|
+ try {
|
|
|
+ await avaliarAtendimento(atendimento.value.Id);
|
|
|
+ await carregar(atendimento.value.Id);
|
|
|
+ toast.sucesso("Atendimento reavaliado.");
|
|
|
+ } catch (err) {
|
|
|
+ toast.erro(err?.message ?? "Falha ao reavaliar o atendimento.");
|
|
|
+ } finally {
|
|
|
+ reavaliando.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// watch (em vez de onMounted) porque o Vue Router reaproveita esta instância quando se
|
|
|
// navega entre duas URLs /atendimentos/:id sem desmontar o componente
|
|
|
watch(() => route.params.id, carregar, { immediate: true });
|
|
|
@@ -214,22 +252,42 @@ watch(() => route.params.id, carregar, { immediate: true });
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-if="avaliacao" class="flex flex-wrap items-center gap-2">
|
|
|
- <BaseBadge :variant="scoreVariant(avaliacao.ScoreAtendente)">
|
|
|
- {{ avaliacao.ScoreAtendente !== null && avaliacao.ScoreAtendente !== undefined ? `Nota ${avaliacao.ScoreAtendente}` : "Sem nota" }}
|
|
|
- </BaseBadge>
|
|
|
- <BaseBadge :variant="resolvidoVariant[avaliacao.Resolvido] ?? 'default'">
|
|
|
- {{ resolvidoLabel[avaliacao.Resolvido] ?? avaliacao.Resolvido }}
|
|
|
- </BaseBadge>
|
|
|
- <BaseBadge :variant="sentimentoVariant[avaliacao.Sentimento] ?? 'default'">
|
|
|
- {{ sentimentoLabel[avaliacao.Sentimento] ?? avaliacao.Sentimento }}
|
|
|
- </BaseBadge>
|
|
|
- <BaseBadge v-if="cancelamentoLabel[avaliacao.StatusCancelamento]" :variant="cancelamentoVariant[avaliacao.StatusCancelamento]">
|
|
|
- {{ cancelamentoLabel[avaliacao.StatusCancelamento] }}
|
|
|
- </BaseBadge>
|
|
|
- <BaseBadge v-if="contextoIxcFalhou(avaliacao.ContextoIxcStatus)" variant="warning" :title="CONTEXTO_IXC_ERRO_LABEL">
|
|
|
- Contexto ERP indisponível
|
|
|
- </BaseBadge>
|
|
|
+ <div class="flex flex-wrap items-center gap-2">
|
|
|
+ <BaseButton
|
|
|
+ v-if="isAdmin && atendimento"
|
|
|
+ variant="secondary"
|
|
|
+ :loading="sincronizandoIfbot"
|
|
|
+ :disabled="sincronizandoIfbot"
|
|
|
+ @click="atualizarDoIfbot"
|
|
|
+ >
|
|
|
+ {{ sincronizandoIfbot ? "Atualizando..." : "Atualizar do ifbot" }}
|
|
|
+ </BaseButton>
|
|
|
+ <BaseButton
|
|
|
+ v-if="isAdmin && atendimento"
|
|
|
+ variant="secondary"
|
|
|
+ :loading="reavaliando"
|
|
|
+ :disabled="reavaliando"
|
|
|
+ @click="reavaliar"
|
|
|
+ >
|
|
|
+ {{ reavaliando ? "Reavaliando..." : "Reavaliar" }}
|
|
|
+ </BaseButton>
|
|
|
+ <template v-if="avaliacao">
|
|
|
+ <BaseBadge :variant="scoreVariant(avaliacao.ScoreAtendente)">
|
|
|
+ {{ avaliacao.ScoreAtendente !== null && avaliacao.ScoreAtendente !== undefined ? `Nota ${avaliacao.ScoreAtendente}` : "Sem nota" }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge :variant="resolvidoVariant[avaliacao.Resolvido] ?? 'default'">
|
|
|
+ {{ resolvidoLabel[avaliacao.Resolvido] ?? avaliacao.Resolvido }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge :variant="sentimentoVariant[avaliacao.Sentimento] ?? 'default'">
|
|
|
+ {{ sentimentoLabel[avaliacao.Sentimento] ?? avaliacao.Sentimento }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge v-if="cancelamentoLabel[avaliacao.StatusCancelamento]" :variant="cancelamentoVariant[avaliacao.StatusCancelamento]">
|
|
|
+ {{ cancelamentoLabel[avaliacao.StatusCancelamento] }}
|
|
|
+ </BaseBadge>
|
|
|
+ <BaseBadge v-if="contextoIxcFalhou(avaliacao.ContextoIxcStatus)" variant="warning" :title="CONTEXTO_IXC_ERRO_LABEL">
|
|
|
+ Contexto ERP indisponível
|
|
|
+ </BaseBadge>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</div>
|
|
|
|