Explorar el Código

ajustes atendimento via whatsapp

leonardo hace 1 mes
padre
commit
20bf582961

+ 105 - 1
src/views/configuracoes/ConfiguracoesView.vue

@@ -1,14 +1,66 @@
 <script setup>
-import { computed, ref } from "vue";
+import { computed, ref, onMounted, onUnmounted } from "vue";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import BotaoAlterarTema from "../../components/BotaoAlterarTema.vue";
 import BaseButton from "../../components/base/BaseButton.vue";
+import BaseBadge from "../../components/base/BaseBadge.vue";
+import ModalWhatsappConexao from "../../components/ModalWhatsappConexao.vue";
 import { useAuth } from "../../composables/useAuth.js";
+import { useWhatsappConexao } from "../../composables/useWhatsappConexao.js";
 import { sincronizarAtendimentos, sincronizarTodosAtendimentos } from "../../api/atendimentos.js";
 
 const { user } = useAuth();
 const isAdmin = computed(() => String(user.value?.Nivel) === "3");
 
+const {
+  status: whatsappStatus,
+  qrDataUrl,
+  telefone: whatsappTelefone,
+  carregando: conectandoWhatsapp,
+  erro: erroWhatsapp,
+  atualizarStatus: atualizarStatusWhatsapp,
+  conectar: conectarWhatsapp,
+  desconectar: desconectarWhatsapp,
+  pararPolling: pararPollingWhatsapp
+} = useWhatsappConexao();
+
+const mostrarModalConexao = ref(false);
+
+const badgeConexaoVariant = computed(() => {
+  if (whatsappStatus.value === "conectado") return "success";
+  if (whatsappStatus.value === "qr_pendente" || whatsappStatus.value === "conectando") return "warning";
+  return "danger";
+});
+
+const badgeConexaoTexto = computed(() => {
+  if (whatsappStatus.value === "conectado") return `Conectado — ${whatsappTelefone.value}`;
+  if (whatsappStatus.value === "qr_pendente") return "Aguardando leitura do QR";
+  if (whatsappStatus.value === "conectando") return "Conectando...";
+  return "Desconectado";
+});
+
+async function abrirConexao() {
+  mostrarModalConexao.value = true;
+  await conectarWhatsapp();
+}
+
+function fecharConexao() {
+  mostrarModalConexao.value = false;
+  pararPollingWhatsapp();
+}
+
+async function desconectar() {
+  await desconectarWhatsapp();
+}
+
+onMounted(() => {
+  if (isAdmin.value) atualizarStatusWhatsapp();
+});
+
+onUnmounted(() => {
+  pararPollingWhatsapp();
+});
+
 const sincronizando = ref(false);
 const importandoTudo = ref(false);
 const syncOcupado = computed(() => sincronizando.value || importandoTudo.value);
@@ -160,6 +212,49 @@ async function importarTudo() {
           </div>
         </section>
 
+        <section v-if="isAdmin">
+          <div class="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
+            WhatsApp
+          </div>
+
+          <div class="w-full rounded-2xl border border-gray-200 bg-background/70 p-5 shadow-sm dark:border-gray-700 dark:bg-background/20">
+            <div class="flex items-center justify-between gap-4">
+              <div class="flex items-center gap-3">
+                <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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.86 9.86 0 0 1-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8Z" />
+                  </svg>
+                </div>
+                <div>
+                  <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Número conectado</div>
+                  <div class="text-xs text-gray-500 dark:text-gray-400">Gerencie a conexão do WhatsApp usada no Atendimento.</div>
+                </div>
+              </div>
+              <div class="flex shrink-0 items-center gap-3">
+                <BaseBadge :variant="badgeConexaoVariant">{{ badgeConexaoTexto }}</BaseBadge>
+                <button
+                  v-if="whatsappStatus === 'conectado'"
+                  type="button"
+                  class="rounded-xl border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-white/5"
+                  :disabled="conectandoWhatsapp"
+                  @click="desconectar"
+                >
+                  Desconectar
+                </button>
+                <button
+                  v-else
+                  type="button"
+                  class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50"
+                  :disabled="conectandoWhatsapp"
+                  @click="abrirConexao"
+                >
+                  Conectar número
+                </button>
+              </div>
+            </div>
+          </div>
+        </section>
+
         <section>
           <div class="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
             Aparência
@@ -184,5 +279,14 @@ async function importarTudo() {
         </section>
       </div>
     </div>
+
+    <ModalWhatsappConexao
+      v-if="mostrarModalConexao"
+      :status="whatsappStatus"
+      :qr-data-url="qrDataUrl"
+      :telefone="whatsappTelefone"
+      :erro="erroWhatsapp"
+      @close="fecharConexao"
+    />
   </LayoutSistema>
 </template>

+ 290 - 153
src/views/conversas/ConversasWhatsappView.vue

@@ -1,14 +1,13 @@
 <script setup>
 import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
-import ModalWhatsappConexao from "../../components/ModalWhatsappConexao.vue";
 import ModalMidia from "../../components/ModalMidia.vue";
 import BaseBadge from "../../components/base/BaseBadge.vue";
 import { useConversas } from "../../composables/useConversas.js";
-import { useWhatsappConexao } from "../../composables/useWhatsappConexao.js";
 import { useAuth } from "../../composables/useAuth.js";
 import { useToast } from "../../composables/useToast.js";
 import { ehAudio, ehImagem, ehVideo } from "../../utils/midia.js";
+import { avatarIniciais, avatarCor } from "../../utils/avatar.js";
 
 const toast = useToast();
 
@@ -32,6 +31,7 @@ const {
   fecharConversa,
   enviarResposta,
   excluirConversa,
+  buscarSugestao,
   descartarSugestao
 } = useConversas();
 
@@ -76,47 +76,6 @@ async function onExcluir(id) {
 const { user } = useAuth();
 const isAdmin = computed(() => String(user.value?.Nivel) === "3");
 
-const {
-  status: whatsappStatus,
-  qrDataUrl,
-  telefone: whatsappTelefone,
-  carregando: conectandoWhatsapp,
-  erro: erroWhatsapp,
-  atualizarStatus: atualizarStatusWhatsapp,
-  conectar: conectarWhatsapp,
-  desconectar: desconectarWhatsapp,
-  pararPolling: pararPollingWhatsapp
-} = useWhatsappConexao();
-
-const mostrarModalConexao = ref(false);
-
-const badgeConexaoVariant = computed(() => {
-  if (whatsappStatus.value === "conectado") return "success";
-  if (whatsappStatus.value === "qr_pendente" || whatsappStatus.value === "conectando") return "warning";
-  return "danger";
-});
-
-const badgeConexaoTexto = computed(() => {
-  if (whatsappStatus.value === "conectado") return `Conectado — ${whatsappTelefone.value}`;
-  if (whatsappStatus.value === "qr_pendente") return "Aguardando leitura do QR";
-  if (whatsappStatus.value === "conectando") return "Conectando...";
-  return "Desconectado";
-});
-
-async function abrirConexao() {
-  mostrarModalConexao.value = true;
-  await conectarWhatsapp();
-}
-
-function fecharConexao() {
-  mostrarModalConexao.value = false;
-  pararPollingWhatsapp();
-}
-
-async function desconectar() {
-  await desconectarWhatsapp();
-}
-
 const texto = ref("");
 const inputResposta = ref(null);
 
@@ -139,6 +98,65 @@ function formatPrevia(conversa) {
   return conversa.NomeContato || conversa.Telefone;
 }
 
+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 formatUltimaMensagem(value) {
+  if (!value) return "";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return "";
+  const hoje = new Date().setHours(0, 0, 0, 0);
+  const ontem = hoje - 86400000;
+  const dia = new Date(value).setHours(0, 0, 0, 0);
+  if (dia === hoje) return formatHora(value);
+  if (dia === ontem) return "Ontem";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
+}
+
+const conversasAgrupadas = computed(() => {
+  const hoje = new Date().setHours(0, 0, 0, 0);
+  const ontem = hoje - 86400000;
+  const semana = hoje - 6 * 86400000;
+  const grupos = [
+    { label: "Hoje", items: [] },
+    { label: "Ontem", items: [] },
+    { label: "Últimos 7 dias", items: [] },
+    { label: "Mais antigas", items: [] }
+  ];
+  for (const c of conversas.value) {
+    const raw = c.UltimaMensagemEm ?? c.CreatedAt;
+    const t = raw ? new Date(raw).getTime() : 0;
+    if (t >= hoje) grupos[0].items.push(c);
+    else if (t >= ontem) grupos[1].items.push(c);
+    else if (t >= semana) grupos[2].items.push(c);
+    else grupos[3].items.push(c);
+  }
+  return grupos.filter((g) => g.items.length > 0);
+});
+
+function statusBadgeVariant(status) {
+  return status === "aberta" ? "success" : "default";
+}
+
+function statusBadgeTexto(status) {
+  if (status === "aberta") return "Aberta";
+  if (status === "fechada") return "Fechada";
+  return status || "—";
+}
+
+function novoDia(idx) {
+  const atual = mensagens.value[idx]?.Timestamp;
+  if (!atual) return false;
+  if (idx === 0) return true;
+  const anterior = mensagens.value[idx - 1]?.Timestamp;
+  if (!anterior) return true;
+  return new Date(atual).toDateString() !== new Date(anterior).toDateString();
+}
+
 function mensagemErroEnvio(err) {
   if (err?.message === "whatsapp_nao_conectado") return "WhatsApp desconectado. Peça a um admin para reconectar.";
   return `Erro ao enviar: ${err?.message ?? "erro desconhecido"}`;
@@ -161,107 +179,106 @@ let refreshTimer = null;
 onMounted(() => {
   carregarConversas();
   refreshTimer = setInterval(carregarConversas, 5000);
-  if (isAdmin.value) atualizarStatusWhatsapp();
 });
 
 onUnmounted(() => {
   if (refreshTimer) clearInterval(refreshTimer);
   fecharConversa();
-  pararPollingWhatsapp();
 });
 </script>
 
 <template>
   <LayoutSistema>
-    <div v-if="isAdmin" class="mb-4 flex items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-gray-100/50 px-4 py-3 shadow-sm dark:border-gray-700 dark:bg-secondary/50">
-      <div class="flex items-center gap-2">
-        <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Número conectado:</span>
-        <BaseBadge :variant="badgeConexaoVariant">{{ badgeConexaoTexto }}</BaseBadge>
-      </div>
-      <button
-        v-if="whatsappStatus === 'conectado'"
-        type="button"
-        class="rounded-xl border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-white/5"
-        :disabled="conectandoWhatsapp"
-        @click="desconectar"
-      >
-        Desconectar
-      </button>
-      <button
-        v-else
-        type="button"
-        class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50"
-        :disabled="conectandoWhatsapp"
-        @click="abrirConexao"
+    <div class="grid h-[calc(100vh-var(--layout-header-height)-4rem)] grid-cols-[300px_1fr] gap-4 max-[760px]:grid-cols-1 max-[760px]:gap-0">
+      <section
+        class="flex min-h-0 flex-col rounded-2xl border border-gray-200 bg-gray-100/50 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50"
+        :class="conversaAtivaId ? 'max-[760px]:hidden' : ''"
       >
-        Conectar número
-      </button>
-    </div>
-
-    <ModalWhatsappConexao
-      v-if="mostrarModalConexao"
-      :status="whatsappStatus"
-      :qr-data-url="qrDataUrl"
-      :telefone="whatsappTelefone"
-      :erro="erroWhatsapp"
-      @close="fecharConexao"
-    />
-
-    <div class="grid h-[calc(100vh-var(--layout-header-height)-4rem)] grid-cols-[280px_1fr] gap-4">
-      <section class="flex min-h-0 flex-col rounded-2xl border border-gray-200 bg-gray-100/50 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50">
-        <div class="border-b border-gray-200 px-4 py-3 text-sm font-semibold text-gray-900 dark:border-gray-700 dark:text-gray-100">
-          Conversas
+        <div class="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
+          <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">Conversas</span>
+          <span v-if="conversas.length" class="rounded-full bg-black/[0.04] px-2 py-0.5 text-[11px] font-medium text-gray-500 dark:bg-white/[0.06] dark:text-gray-400">
+            {{ conversas.length }}
+          </span>
         </div>
 
-        <div v-if="conversasLoading && !conversas.length" class="p-4 text-sm text-gray-500 dark:text-gray-400">Carregando...</div>
+        <div v-if="conversasLoading && !conversas.length" class="flex flex-1 items-center justify-center p-4 text-sm text-gray-500 dark:text-gray-400">
+          Carregando...
+        </div>
         <div v-else-if="conversasError" class="m-4 rounded-lg bg-red-50 px-4 py-3 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
           {{ conversasError }}
         </div>
-        <div v-else-if="!conversas.length" class="p-4 text-sm text-gray-500 dark:text-gray-400">
-          Nenhuma conversa ainda. Mande uma mensagem para o número de teste no WhatsApp.
+        <div v-else-if="!conversas.length" class="flex flex-1 flex-col items-center justify-center gap-2 p-4 text-center">
+          <svg class="h-8 w-8 text-gray-300 dark:text-gray-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+            <path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.86 9.86 0 0 1-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8Z" />
+          </svg>
+          <p class="text-sm text-gray-500 dark:text-gray-400">Nenhuma conversa ainda.</p>
+          <p class="text-xs text-gray-400 dark:text-gray-500">Mande uma mensagem para o número de teste no WhatsApp.</p>
         </div>
 
-        <div v-else class="min-h-0 flex-1 overflow-y-auto p-2 space-y-1">
-          <div
-            v-for="c in conversas"
-            :key="c.Id"
-            class="group relative rounded-xl transition-colors"
-            :class="
-              c.Id === conversaAtivaId
-                ? 'bg-primary/10 dark:bg-primary/20'
-                : 'hover:bg-black/[0.03] dark:hover:bg-white/[0.04]'
-            "
-          >
-            <button type="button" class="block w-full p-3 pr-9 text-left" @click="abrirConversa(c.Id)">
-              <div class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{{ formatPrevia(c) }}</div>
-              <div class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">{{ c.Telefone }}</div>
-            </button>
-
-            <button
-              v-if="isAdmin"
-              type="button"
-              class="absolute right-1 top-1 rounded-lg border border-transparent p-1.5 text-gray-400 opacity-0 transition-opacity hover:border-red-200 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
-              title="Excluir conversa"
-              @click="confirmandoExclusao = c.Id"
-            >
-              <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="m19 7-.867 12.142A2 2 0 0 1 16.138 21H7.862a2 2 0 0 1-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v3M4 7h16" />
-              </svg>
-            </button>
-
+        <div v-else class="conv-scroll min-h-0 flex-1 overflow-y-auto p-2">
+          <template v-for="grupo in conversasAgrupadas" :key="grupo.label">
+            <div class="px-2 pt-2 pb-1 text-[10px] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500">
+              {{ grupo.label }}
+            </div>
+            <div class="space-y-1">
             <div
-              v-if="confirmandoExclusao === c.Id"
-              class="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-background p-3 dark:bg-[#101827]"
+              v-for="c in grupo.items"
+              :key="c.Id"
+              class="group relative rounded-xl transition-colors"
+              :class="
+                c.Id === conversaAtivaId
+                  ? 'bg-primary/10 dark:bg-primary/20'
+                  : 'hover:bg-black/[0.03] dark:hover:bg-white/[0.04]'
+              "
             >
-              <span class="text-xs text-gray-500 dark:text-gray-400">Excluir esta conversa?</span>
-              <div class="flex items-center gap-1.5">
-                <button
-                  type="button"
-                  class="rounded-lg bg-red-600 px-3 py-1 text-xs font-medium text-white disabled:opacity-50"
-                  :disabled="excluindo === c.Id"
-                  @click="onExcluir(c.Id)"
+              <button type="button" class="flex w-full items-center gap-3 p-2.5 pr-9 text-left" @click="abrirConversa(c.Id)">
+                <div
+                  class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white ring-1 ring-white/20"
+                  :class="avatarCor(c.NomeContato || c.Telefone)"
                 >
-                  Confirmar
+                  {{ avatarIniciais(c.NomeContato || c.Telefone) }}
+                </div>
+                <div class="min-w-0 flex-1">
+                  <div class="flex items-center justify-between gap-2">
+                    <span class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{{ formatPrevia(c) }}</span>
+                    <span v-if="formatUltimaMensagem(c.UltimaMensagemEm ?? c.CreatedAt)" class="shrink-0 text-[11px] text-gray-400 dark:text-gray-500">
+                      {{ formatUltimaMensagem(c.UltimaMensagemEm ?? c.CreatedAt) }}
+                    </span>
+                  </div>
+                  <div class="mt-0.5 flex items-center justify-between gap-2">
+                    <span class="truncate text-xs text-gray-500 dark:text-gray-400">{{ c.Telefone }}</span>
+                    <BaseBadge :variant="statusBadgeVariant(c.Status)" class="shrink-0 !px-2 !py-0.5 text-[10px]">
+                      {{ statusBadgeTexto(c.Status) }}
+                    </BaseBadge>
+                  </div>
+                </div>
+              </button>
+
+              <button
+                v-if="isAdmin"
+                type="button"
+                class="absolute right-1 top-1 rounded-lg border border-transparent p-1.5 text-gray-400 opacity-0 transition-opacity hover:border-red-200 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
+                title="Excluir conversa"
+                @click="confirmandoExclusao = c.Id"
+              >
+                <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="m19 7-.867 12.142A2 2 0 0 1 16.138 21H7.862a2 2 0 0 1-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v3M4 7h16" />
+                </svg>
+              </button>
+
+              <div
+                v-if="confirmandoExclusao === c.Id"
+                class="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-background p-3 dark:bg-[#101827]"
+              >
+                <span class="text-xs text-gray-500 dark:text-gray-400">Excluir esta conversa?</span>
+                <div class="flex items-center gap-1.5">
+                  <button
+                    type="button"
+                    class="rounded-lg bg-red-600 px-3 py-1 text-xs font-medium text-white disabled:opacity-50"
+                    :disabled="excluindo === c.Id"
+                    @click="onExcluir(c.Id)"
+                  >
+                    Confirmar
                 </button>
                 <button
                   type="button"
@@ -272,11 +289,16 @@ onUnmounted(() => {
                 </button>
               </div>
             </div>
+            </div>
           </div>
+        </template>
         </div>
       </section>
 
-      <section class="flex min-h-0 flex-col rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">
+      <section
+        class="flex min-h-0 flex-col rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20"
+        :class="!conversaAtivaId ? 'max-[760px]:hidden' : ''"
+      >
         <template v-if="!conversaAtivaId">
           <div class="flex flex-1 items-center justify-center text-sm text-gray-500 dark:text-gray-400">
             Selecione uma conversa para começar a responder.
@@ -284,11 +306,36 @@ onUnmounted(() => {
         </template>
 
         <template v-else>
-          <div class="border-b border-gray-200 px-4 py-3 text-sm font-semibold text-gray-900 dark:border-gray-700 dark:text-gray-100">
-            {{ conversaAtiva ? formatPrevia(conversaAtiva) : "" }}
+          <div class="flex items-center gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
+            <button
+              type="button"
+              class="hidden h-8 w-8 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-black/[0.03] max-[760px]:inline-flex dark:text-gray-400 dark:hover:bg-white/[0.04]"
+              title="Voltar para a lista"
+              @click="fecharConversa"
+            >
+              <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="M15 19l-7-7 7-7" />
+              </svg>
+            </button>
+            <div
+              v-if="conversaAtiva"
+              class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white ring-1 ring-white/20"
+              :class="avatarCor(conversaAtiva.NomeContato || conversaAtiva.Telefone)"
+            >
+              {{ avatarIniciais(conversaAtiva.NomeContato || conversaAtiva.Telefone) }}
+            </div>
+            <div class="min-w-0 flex-1">
+              <div class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
+                {{ conversaAtiva ? formatPrevia(conversaAtiva) : "" }}
+              </div>
+              <div v-if="conversaAtiva" class="truncate text-xs text-gray-500 dark:text-gray-400">{{ conversaAtiva.Telefone }}</div>
+            </div>
+            <BaseBadge v-if="conversaAtiva" :variant="statusBadgeVariant(conversaAtiva.Status)" class="shrink-0">
+              {{ statusBadgeTexto(conversaAtiva.Status) }}
+            </BaseBadge>
           </div>
 
-          <div class="min-h-0 flex-1 overflow-y-auto p-4">
+          <div class="conv-scroll min-h-0 flex-1 overflow-y-auto p-4">
             <div v-if="mensagensLoading" class="text-sm text-gray-500 dark:text-gray-400">Carregando conversa...</div>
             <div v-else-if="mensagensError" class="rounded-lg bg-red-50 px-4 py-3 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
               {{ mensagensError }}
@@ -298,7 +345,13 @@ onUnmounted(() => {
             </div>
 
             <div v-else class="space-y-2">
-              <div v-for="m in mensagens" :key="m.Id" class="flex" :class="m.Direcao === 'saida' ? 'justify-end' : 'justify-start'">
+              <template v-for="(m, i) in mensagens" :key="m.Id">
+                <div v-if="novoDia(i)" class="flex justify-center py-2 first:pt-0">
+                  <span class="rounded-full bg-black/[0.05] px-3 py-0.5 text-[11px] font-medium text-gray-500 dark:bg-white/[0.06] dark:text-gray-400">
+                    {{ formatData(m.Timestamp) }}
+                  </span>
+                </div>
+              <div class="flex" :class="m.Direcao === 'saida' ? 'justify-end' : 'justify-start'">
                 <div
                   class="max-w-[75%] rounded-2xl px-3.5 py-2 shadow-sm"
                   :class="
@@ -387,46 +440,91 @@ onUnmounted(() => {
                   <div class="mt-0.5 text-right text-[10px] text-gray-400 dark:text-gray-500">{{ formatHora(m.Timestamp) }}</div>
                 </div>
               </div>
+              </template>
             </div>
           </div>
 
+          <Transition name="sugestao-fade">
           <div v-if="sugestao" class="mx-3 mb-2 rounded-xl border border-primary/30 bg-primary/5 p-3 dark:border-primary/40 dark:bg-primary/10">
-            <div class="mb-1 flex items-center justify-between">
-              <div class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-primary">
-                <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                  <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6.34 6.34l1.42 1.42M16.24 16.24l1.42 1.42M6.34 17.66l1.42-1.42M16.24 7.76l1.42-1.42" />
-                </svg>
-                Sugestão do star-atendente
+            <div class="mb-1.5 flex items-center justify-between gap-2">
+              <div class="flex min-w-0 items-center gap-2">
+                <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/15 text-primary dark:bg-primary/25">
+                  <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6.34 6.34l1.42 1.42M16.24 16.24l1.42 1.42M6.34 17.66l1.42-1.42M16.24 7.76l1.42-1.42" />
+                  </svg>
+                </span>
+                <span class="truncate text-[11px] font-semibold uppercase tracking-wide text-primary">Sugestão do star-atendente</span>
+              </div>
+              <div class="flex shrink-0 items-center gap-0.5">
+                <button
+                  type="button"
+                  class="rounded-lg p-1 text-gray-400 transition-colors hover:bg-black/[0.04] hover:text-gray-600 disabled:opacity-40 dark:text-gray-500 dark:hover:bg-white/[0.06] dark:hover:text-gray-300"
+                  title="Gerar outra sugestão"
+                  aria-label="Gerar outra sugestão"
+                  :disabled="sugestaoLoading"
+                  @click="buscarSugestao"
+                >
+                  <svg class="h-3.5 w-3.5" :class="{ 'animate-spin': sugestaoLoading }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
+                  </svg>
+                </button>
+                <button
+                  type="button"
+                  class="rounded-lg p-1 text-gray-400 transition-colors hover:bg-black/[0.04] hover:text-gray-600 dark:text-gray-500 dark:hover:bg-white/[0.06] dark:hover:text-gray-300"
+                  title="Descartar sugestão"
+                  aria-label="Descartar sugestão"
+                  @click="descartarSugestao"
+                >
+                  <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M18 6L6 18M6 6l12 12" />
+                  </svg>
+                </button>
               </div>
-              <button
-                type="button"
-                class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
-                aria-label="Descartar sugestão"
-                @click="descartarSugestao"
-              >
-                <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                  <path d="M18 6L6 18M6 6l12 12" />
-                </svg>
-              </button>
             </div>
-            <p class="whitespace-pre-wrap break-words text-sm text-gray-800 dark:text-gray-200">{{ sugestao }}</p>
+            <p
+              class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-800 transition-opacity dark:text-gray-200"
+              :class="{ 'opacity-50': sugestaoLoading }"
+            >
+              {{ sugestao }}
+            </p>
             <button
               type="button"
-              class="mt-2 rounded-lg bg-primary px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-primary/90"
+              class="mt-2 rounded-lg bg-primary px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-primary/90 disabled:opacity-50"
+              :disabled="sugestaoLoading"
               @click="usarSugestao"
             >
               Usar esta resposta
             </button>
           </div>
-          <div v-else-if="sugestaoLoading" class="mx-3 mb-2 flex items-center gap-1 rounded-xl border border-primary/30 bg-primary/5 px-3 py-2 dark:border-primary/40 dark:bg-primary/10">
-            <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 0ms" />
-            <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 150ms" />
-            <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 300ms" />
-            <span class="ml-1 text-xs text-gray-400 dark:text-gray-500">Gerando sugestão...</span>
+          <div v-else-if="sugestaoLoading" class="mx-3 mb-2 flex items-center gap-2 rounded-xl border border-primary/30 bg-primary/5 p-3 dark:border-primary/40 dark:bg-primary/10">
+            <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/15 text-primary dark:bg-primary/25">
+              <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6.34 6.34l1.42 1.42M16.24 16.24l1.42 1.42M6.34 17.66l1.42-1.42M16.24 7.76l1.42-1.42" />
+              </svg>
+            </span>
+            <span class="flex items-center gap-1">
+              <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 0ms" />
+              <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 150ms" />
+              <span class="h-1.5 w-1.5 animate-bounce rounded-full bg-primary" style="animation-delay: 300ms" />
+            </span>
+            <span class="text-xs text-gray-500 dark:text-gray-400">Gerando sugestão...</span>
           </div>
-          <div v-else-if="sugestaoError" class="mx-3 mb-2 rounded-lg bg-red-50 px-3 py-2 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
-            Não foi possível gerar a sugestão: {{ sugestaoError }}
+          <div v-else-if="sugestaoError" class="mx-3 mb-2 flex items-start gap-2 rounded-xl border border-red-200 bg-red-50 p-3 dark:border-red-900/40 dark:bg-red-900/20">
+            <svg class="mt-0.5 h-4 w-4 shrink-0 text-red-500 dark:text-red-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+              <path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+            </svg>
+            <div class="min-w-0 flex-1">
+              <p class="text-xs text-red-800 dark:text-red-300">Não foi possível gerar a sugestão: {{ sugestaoError }}</p>
+              <button
+                type="button"
+                class="mt-1.5 text-xs font-medium text-red-700 underline hover:text-red-900 dark:text-red-300 dark:hover:text-red-100"
+                @click="buscarSugestao"
+              >
+                Tentar novamente
+              </button>
+            </div>
           </div>
+          </Transition>
 
           <form class="flex items-center gap-2 border-t border-gray-200 p-3 dark:border-gray-700" @submit.prevent="enviar">
             <input
@@ -457,3 +555,42 @@ onUnmounted(() => {
     />
   </LayoutSistema>
 </template>
+
+<style scoped>
+.sugestao-fade-enter-active,
+.sugestao-fade-leave-active {
+  transition: opacity 0.15s ease, transform 0.15s ease;
+}
+.sugestao-fade-enter-from,
+.sugestao-fade-leave-to {
+  opacity: 0;
+  transform: translateY(4px);
+}
+
+.conv-scroll {
+  scrollbar-width: thin;
+  scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
+}
+.conv-scroll::-webkit-scrollbar {
+  width: 3px;
+}
+.conv-scroll::-webkit-scrollbar-track {
+  background: transparent;
+}
+.conv-scroll::-webkit-scrollbar-thumb {
+  background: rgba(0, 0, 0, 0.15);
+  border-radius: 999px;
+}
+.conv-scroll::-webkit-scrollbar-thumb:hover {
+  background: rgba(0, 0, 0, 0.25);
+}
+:is(.dark) .conv-scroll {
+  scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
+}
+:is(.dark) .conv-scroll::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.12);
+}
+:is(.dark) .conv-scroll::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.22);
+}
+</style>