leonardo 1 月之前
父節點
當前提交
9a83a4de62

+ 12 - 0
src/api/atendimentos.js

@@ -58,6 +58,18 @@ export function listarSetoresAvaliacoes() {
   return apiFetch("/api/atendimentos/avaliacoes/setores");
 }
 
+export function listarGoldenSet({ status, setor } = {}) {
+  const query = new URLSearchParams();
+  if (status) query.set("status", status);
+  if (setor) query.set("setor", setor);
+  const qs = query.toString();
+  return apiFetch(`/api/atendimentos/golden-set${qs ? `?${qs}` : ""}`);
+}
+
+export function salvarGoldenLabel(id, dados) {
+  return apiFetch(`/api/atendimentos/${id}/golden-label`, { method: "PATCH", body: dados });
+}
+
 // não usa apiFetch: a resposta é um PDF (blob), não JSON/texto
 export async function baixarRelatorioNaoResolvidos({ setor, sentimento, busca, ordenacao, dataInicio, dataFim } = {}) {
   const query = new URLSearchParams();

+ 1 - 0
src/layout/HeaderSistema.vue

@@ -31,6 +31,7 @@ const tituloPagina = computed(() => {
   if (nome === "configuracoes") return "Configurações";
   if (nome === "usuarios") return "Usuários";
   if (nome === "avaliacoes") return "Avaliações de Atendimento";
+  if (nome === "golden-set") return "Golden Set — Rotulagem manual";
   if (nome === "documentos") return "Documentos";
   if (nome === "documentos-carregar") return "Carregar documento";
   return "Oráculo";

+ 18 - 0
src/layout/SidebarSistema.vue

@@ -252,6 +252,24 @@ async function novaConversa() {
             <span :class="classeRotulo">Não resolvidos</span>
           </button>
 
+          <button
+            type="button"
+            :class="classeItemSidebar(rotaAtiva === 'golden-set')"
+            :title="sidebarRecolhida ? 'Golden set' : ''"
+            @click="irPara('golden-set')"
+          >
+            <svg
+              :class="classeIcon(rotaAtiva === 'golden-set')"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.8"
+            >
+              <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+            </svg>
+            <span :class="classeRotulo">Golden set</span>
+          </button>
+
           <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
         </template>
         <div

+ 3 - 0
src/router/index.js

@@ -7,6 +7,7 @@ import UsuariosView from "../views/usuarios/UsuariosView.vue";
 import AvaliacoesView from "../views/atendimentos/AvaliacoesView.vue";
 import NaoResolvidosView from "../views/atendimentos/NaoResolvidosView.vue";
 import AtendimentoView from "../views/atendimentos/AtendimentoView.vue";
+import GoldenSetView from "../views/atendimentos/GoldenSetView.vue";
 import ConversasWhatsappView from "../views/conversas/ConversasWhatsappView.vue";
 import DocumentosView from "../views/documentos/DocumentosView.vue";
 import CarregarDocumentoView from "../views/documentos/CarregarDocumentoView.vue";
@@ -30,6 +31,7 @@ export const router = createRouter({
       meta: { requiresAuth: true }
     },
     { path: "/atendimentos/:id(\\d+)", name: "atendimento", component: AtendimentoView, meta: { requiresAuth: true } },
+    { path: "/atendimentos/golden-set", name: "golden-set", component: GoldenSetView, meta: { requiresAuth: true } },
     { path: "/conversas-whatsapp", name: "conversas-whatsapp", component: ConversasWhatsappView, meta: { requiresAuth: true } },
     { path: "/documentos", name: "documentos", component: DocumentosView, meta: { requiresAuth: true } },
     { path: "/documentos/carregar", name: "documentos-carregar", component: CarregarDocumentoView, meta: { requiresAuth: true } },
@@ -56,6 +58,7 @@ router.beforeEach((to) => {
     to.name === "avaliacoes" ||
     to.name === "atendimentos-nao-resolvidos" ||
     to.name === "atendimento" ||
+    to.name === "golden-set" ||
     to.name === "documentos" ||
     to.name === "documentos-carregar"
   ) {

+ 5 - 0
src/utils/avaliacoes.js

@@ -14,6 +14,11 @@ export const sentimentoLabel = { positivo: "Positivo", neutro: "Neutro", negativ
 export const cancelamentoVariant = { cancelou: "danger", ameacou: "warning" };
 export const cancelamentoLabel = { cancelou: "Cancelou", ameacou: "Quer cancelar" };
 
+// enum completo de StatusCancelamento (cancelamentoLabel só cobre os 2 valores "notáveis"
+// pra badge — nao/indefinido ficam de fora de propósito pra não gerar badge de ruído nas
+// listas). Usado só onde o formulário precisa das 4 opções, ex. rotulagem do golden set.
+export const statusCancelamentoLabel = { ...cancelamentoLabel, nao: "Não", indefinido: "Indefinido" };
+
 // só "erro" indica falha real na integração; sem_erp_protocolo/sem_ticket/nao_verificado são
 // desfechos esperados e não devem gerar alerta visual (ver design da change fix-ixc-contexto-falha-silenciosa)
 export const CONTEXTO_IXC_ERRO_LABEL = "Contexto do ERP indisponível nesta avaliação";

+ 254 - 3
src/views/atendimentos/AtendimentoView.vue

@@ -3,8 +3,9 @@ import { computed, ref, watch } 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 ModalMidia from "../../components/ModalMidia.vue";
-import { obterAtendimento } from "../../api/atendimentos.js";
+import { obterAtendimento, salvarGoldenLabel } from "../../api/atendimentos.js";
 import {
   cancelamentoLabel,
   cancelamentoVariant,
@@ -14,9 +15,14 @@ import {
   resolvidoVariant,
   scoreVariant,
   sentimentoLabel,
-  sentimentoVariant
+  sentimentoVariant,
+  statusCancelamentoLabel
 } from "../../utils/avaliacoes.js";
 import { ehAudio as ehAudioMidia, ehImagem as ehImagemMidia, ehVideo as ehVideoMidia } from "../../utils/midia.js";
+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 route = useRoute();
 const router = useRouter();
@@ -27,12 +33,69 @@ const atendimento = ref(null);
 const mediaViewer = ref(null);
 
 const avaliacao = computed(() => atendimento.value?.avaliacao ?? null);
+const goldenLabel = computed(() => atendimento.value?.goldenLabel ?? null);
 const mensagens = computed(() => atendimento.value?.mensagens ?? []);
 
+const form = ref(criarFormulario(null));
+const salvando = ref(false);
+
+function criarFormulario(label) {
+  return {
+    ScoreAtendente: label?.ScoreAtendente ?? null,
+    SemAtendente: (label?.ScoreAtendente ?? null) === null,
+    JustificativaScore: label?.JustificativaScore ?? "",
+    Resolvido: label?.Resolvido ?? "",
+    Sentimento: label?.Sentimento ?? "",
+    Resumo: label?.Resumo ?? "",
+    SugestoesTexto: (label?.Sugestoes ?? []).join("\n"),
+    AtendentesTexto: (label?.Atendentes ?? []).join(", "),
+    VisitaAgendada: label?.VisitaAgendada === true ? "sim" : label?.VisitaAgendada === false ? "nao" : "",
+    HorarioVisitaInformado: label?.HorarioVisitaInformado ?? "",
+    HorarioVisita: label?.HorarioVisita ?? "",
+    StatusCancelamento: label?.StatusCancelamento ?? "",
+    MotivoCancelamento: label?.MotivoCancelamento ?? ""
+  };
+}
+
+function montarPayloadGoldenLabel() {
+  const f = form.value;
+  const sugestoes = f.SugestoesTexto.split("\n").map((s) => s.trim()).filter(Boolean);
+  const atendentes = f.AtendentesTexto.split(",").map((s) => s.trim()).filter(Boolean);
+
+  return {
+    ScoreAtendente: f.SemAtendente || f.ScoreAtendente === null || f.ScoreAtendente === "" ? null : Number(f.ScoreAtendente),
+    JustificativaScore: f.JustificativaScore.trim() || null,
+    Resolvido: f.Resolvido || null,
+    Sentimento: f.Sentimento || null,
+    Resumo: f.Resumo.trim() || null,
+    Sugestoes: sugestoes.length ? sugestoes : null,
+    Atendentes: atendentes.length ? atendentes : null,
+    VisitaAgendada: f.VisitaAgendada === "sim" ? true : f.VisitaAgendada === "nao" ? false : null,
+    HorarioVisitaInformado: f.HorarioVisitaInformado || null,
+    HorarioVisita: f.HorarioVisita.trim() || null,
+    StatusCancelamento: f.StatusCancelamento || null,
+    MotivoCancelamento: f.MotivoCancelamento.trim() || null
+  };
+}
+
+async function salvarRotulo() {
+  if (salvando.value || !atendimento.value) return;
+  salvando.value = true;
+  try {
+    const atualizado = await salvarGoldenLabel(atendimento.value.Id, montarPayloadGoldenLabel());
+    atendimento.value = { ...atendimento.value, goldenLabel: atualizado };
+    toast.sucesso("Rótulo salvo.");
+  } catch (err) {
+    toast.erro(err?.message ?? "Falha ao salvar o rótulo.");
+  } finally {
+    salvando.value = false;
+  }
+}
+
 // volta para a lista de origem (Avaliações ou Não resolvidos) preservando página, filtros
 // e item aberto (guardados na query string da rota anterior); se o gestor chegou por link
 // direto, não há histórico para voltar — vai para a lista de avaliações limpa
-const ROTAS_ORIGEM = ["/atendimentos/avaliacoes", "/atendimentos/nao-resolvidos"];
+const ROTAS_ORIGEM = ["/atendimentos/avaliacoes", "/atendimentos/nao-resolvidos", "/atendimentos/golden-set"];
 
 function voltar() {
   const anterior = window.history.state?.back;
@@ -105,6 +168,7 @@ async function carregar(id) {
     const resultado = await obterAtendimento(id);
     if (String(route.params.id) !== String(id)) return;
     atendimento.value = resultado;
+    form.value = criarFormulario(resultado.goldenLabel ?? null);
   } catch (err) {
     if (String(route.params.id) !== String(id)) return;
     erro.value = err?.message ?? "Falha ao carregar o atendimento";
@@ -282,6 +346,193 @@ watch(() => route.params.id, carregar, { immediate: true });
             </template>
           </div>
         </section>
+
+        <section
+          v-if="goldenLabel"
+          class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20"
+        >
+          <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
+            <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Rotulagem manual (golden set)</div>
+            <BaseBadge :variant="goldenLabel.Status === 'rotulado' ? 'success' : 'warning'">
+              {{ goldenLabel.Status === "rotulado" ? "Rotulado" : "Pendente" }}
+            </BaseBadge>
+          </div>
+
+          <div class="grid grid-cols-1 gap-5 p-4 sm:grid-cols-2">
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
+                Nota do atendente (1-10)
+              </label>
+              <div class="mt-1 flex items-center gap-2">
+                <input
+                  v-model="form.ScoreAtendente"
+                  type="number"
+                  min="1"
+                  max="10"
+                  :disabled="form.SemAtendente"
+                  class="w-24 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 disabled:opacity-50 dark:border-gray-700 dark:text-gray-200"
+                />
+                <label class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
+                  <input v-model="form.SemAtendente" type="checkbox" class="rounded" />
+                  Sem atendente humano
+                </label>
+              </div>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao?.ScoreAtendente ?? "sem nota" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Resolvido</label>
+              <select
+                v-model="form.Resolvido"
+                class="mt-1 w-full 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"
+              >
+                <option value="">-- não rotulado --</option>
+                <option v-for="(label, value) in resolvidoLabel" :key="value" :value="value">{{ label }}</option>
+              </select>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ resolvidoLabel[avaliacao?.Resolvido] ?? "—" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Sentimento do cliente</label>
+              <select
+                v-model="form.Sentimento"
+                class="mt-1 w-full 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"
+              >
+                <option value="">-- não rotulado --</option>
+                <option v-for="(label, value) in sentimentoLabel" :key="value" :value="value">{{ label }}</option>
+              </select>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ sentimentoLabel[avaliacao?.Sentimento] ?? "—" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Cancelamento</label>
+              <select
+                v-model="form.StatusCancelamento"
+                class="mt-1 w-full 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"
+              >
+                <option value="">-- não rotulado --</option>
+                <option v-for="(label, value) in statusCancelamentoLabel" :key="value" :value="value">{{ label }}</option>
+              </select>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ cancelamentoLabel[avaliacao?.StatusCancelamento] ?? "Não" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Visita agendada</label>
+              <select
+                v-model="form.VisitaAgendada"
+                class="mt-1 w-full 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"
+              >
+                <option value="">-- não informado --</option>
+                <option value="sim">Sim</option>
+                <option value="nao">Não</option>
+              </select>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao?.VisitaAgendada === true ? "Sim" : avaliacao?.VisitaAgendada === false ? "Não" : "—" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Horário de visita informado</label>
+              <select
+                v-model="form.HorarioVisitaInformado"
+                class="mt-1 w-full 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"
+              >
+                <option value="">-- não rotulado --</option>
+                <option v-for="(label, value) in HORARIO_VISITA_INFORMADO_LABEL" :key="value" :value="value">{{ label }}</option>
+              </select>
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ HORARIO_VISITA_INFORMADO_LABEL[avaliacao?.HorarioVisitaInformado] ?? "—" }}
+              </span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Horário da visita</label>
+              <input
+                v-model="form.HorarioVisita"
+                type="text"
+                :disabled="form.HorarioVisitaInformado !== 'sim'"
+                class="mt-1 w-full 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 disabled:opacity-50 dark:border-gray-700 dark:text-gray-200"
+              />
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">IA: {{ avaliacao?.HorarioVisita || "—" }}</span>
+            </div>
+
+            <div>
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
+                Atendentes (separados por vírgula)
+              </label>
+              <input
+                v-model="form.AtendentesTexto"
+                type="text"
+                class="mt-1 w-full 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"
+              />
+              <span class="mt-1 block text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao?.Atendentes?.join(", ") || "—" }}
+              </span>
+            </div>
+
+            <div class="sm:col-span-2">
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Justificativa da nota</label>
+              <textarea
+                v-model="form.JustificativaScore"
+                rows="2"
+                class="mt-1 w-full 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"
+              ></textarea>
+              <p v-if="avaliacao?.JustificativaScore" class="mt-1 text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao.JustificativaScore }}
+              </p>
+            </div>
+
+            <div class="sm:col-span-2">
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Resumo</label>
+              <textarea
+                v-model="form.Resumo"
+                rows="3"
+                class="mt-1 w-full 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"
+              ></textarea>
+              <p v-if="avaliacao?.Resumo" class="mt-1 text-xs text-gray-400 dark:text-gray-500">IA: {{ avaliacao.Resumo }}</p>
+            </div>
+
+            <div class="sm:col-span-2">
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
+                Sugestões de melhoria (uma por linha)
+              </label>
+              <textarea
+                v-model="form.SugestoesTexto"
+                rows="3"
+                class="mt-1 w-full 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"
+              ></textarea>
+              <p v-if="avaliacao?.Sugestoes?.length" class="mt-1 text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao.Sugestoes.join(" · ") }}
+              </p>
+            </div>
+
+            <div class="sm:col-span-2">
+              <label class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Motivo do cancelamento</label>
+              <textarea
+                v-model="form.MotivoCancelamento"
+                rows="2"
+                class="mt-1 w-full 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"
+              ></textarea>
+              <p v-if="avaliacao?.MotivoCancelamento" class="mt-1 text-xs text-gray-400 dark:text-gray-500">
+                IA: {{ avaliacao.MotivoCancelamento }}
+              </p>
+            </div>
+          </div>
+
+          <div class="flex items-center justify-end gap-3 border-t border-gray-200 px-4 py-3 dark:border-gray-700">
+            <BaseButton :loading="salvando" @click="salvarRotulo">
+              {{ salvando ? "Salvando..." : "Salvar rótulo" }}
+            </BaseButton>
+          </div>
+        </section>
       </div>
     </div>
 

+ 145 - 0
src/views/atendimentos/GoldenSetView.vue

@@ -0,0 +1,145 @@
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { useRouter } from "vue-router";
+import LayoutSistema from "../../layout/LayoutSistema.vue";
+import BaseBadge from "../../components/base/BaseBadge.vue";
+import BaseDropdown from "../../components/base/BaseDropdown.vue";
+import StatCard from "../../components/base/StatCard.vue";
+import { listarGoldenSet } from "../../api/atendimentos.js";
+
+const router = useRouter();
+
+const carregando = ref(true);
+const erro = ref("");
+const itens = ref([]);
+const filtroStatus = ref("");
+
+const opcoesStatus = [
+  { value: "pendente", label: "Pendente" },
+  { value: "rotulado", label: "Rotulado" }
+];
+
+const itensFiltrados = computed(() =>
+  filtroStatus.value ? itens.value.filter((i) => i.Status === filtroStatus.value) : itens.value
+);
+
+const totalPendentes = computed(() => itens.value.filter((i) => i.Status === "pendente").length);
+const totalRotulados = computed(() => itens.value.filter((i) => i.Status === "rotulado").length);
+
+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" });
+}
+
+async function carregar() {
+  carregando.value = true;
+  erro.value = "";
+  try {
+    const r = await listarGoldenSet();
+    itens.value = r.results ?? [];
+  } catch (err) {
+    erro.value = err?.message ?? "Falha ao carregar o golden set";
+  } finally {
+    carregando.value = false;
+  }
+}
+
+function abrir(id) {
+  router.push({ name: "atendimento", params: { id } });
+}
+
+onMounted(carregar);
+</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 items-center gap-4">
+            <div
+              class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary dark:bg-primary/20"
+            >
+              <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="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+              </svg>
+            </div>
+            <div class="min-w-0">
+              <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Golden Set</h2>
+              <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
+                Amostra de atendimentos rotulados à mão, usada para medir a acurácia do avaliador por IA.
+              </p>
+            </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 class="grid grid-cols-3 gap-3">
+          <StatCard label="Total na amostra" :value="itens.length" variant="default">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+              </svg>
+            </template>
+          </StatCard>
+          <StatCard label="Pendentes" :value="totalPendentes" variant="warning">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+              </svg>
+            </template>
+          </StatCard>
+          <StatCard label="Rotulados" :value="totalRotulados" variant="success">
+            <template #icon>
+              <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
+              </svg>
+            </template>
+          </StatCard>
+        </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">
+            <BaseDropdown v-model="filtroStatus" rotulo="Status" rotulo-padrao="todos" :opcoes="opcoesStatus" />
+            <div class="ml-auto text-xs text-gray-500 dark:text-gray-400">{{ itensFiltrados.length }} atendimentos</div>
+          </div>
+
+          <div v-if="carregando" class="p-6 text-sm text-gray-500 dark:text-gray-400">Carregando...</div>
+          <div v-else-if="itensFiltrados.length === 0" class="p-6 text-sm text-gray-500 dark:text-gray-400">
+            Nenhum atendimento encontrado. Rode o script <code>gerarAmostraGoldenSet.js --apply</code> para gerar a amostra.
+          </div>
+
+          <ul v-else class="divide-y divide-gray-200 dark:divide-gray-700">
+            <li v-for="item in itensFiltrados" :key="item.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="abrir(item.AtendimentoId)"
+              >
+                <div class="min-w-0 flex-1">
+                  <div class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
+                    {{ item.Codigo ?? `#${item.AtendimentoId}` }}
+                    <span class="font-normal text-gray-500 dark:text-gray-400"> — {{ item.ClienteNome ?? "cliente desconhecido" }}</span>
+                  </div>
+                  <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
+                    <span v-if="item.Setor">{{ item.Setor }} · </span>{{ formatData(item.Abertura) }}
+                  </div>
+                </div>
+                <BaseBadge :variant="item.Status === 'rotulado' ? 'success' : 'warning'">
+                  {{ item.Status === "rotulado" ? "Rotulado" : "Pendente" }}
+                </BaseBadge>
+              </button>
+            </li>
+          </ul>
+        </section>
+      </div>
+    </div>
+  </LayoutSistema>
+</template>