diff --git a/src/components/conversations/ConversationDrawer.vue b/src/components/conversations/ConversationDrawer.vue index 33b459e..d8a0389 100644 --- a/src/components/conversations/ConversationDrawer.vue +++ b/src/components/conversations/ConversationDrawer.vue @@ -1103,6 +1103,45 @@ function insertEmoji(emoji) { + +
', message: '' } — priorizamos message.
-function friendlySendError(code, providedMessage) {
- if (providedMessage) return providedMessage;
+// Classifica o erro do envio em { message, hint, action } pra UI mostrar
+// um banner persistente no chat (não só toast). Diferente do friendly antigo
+// que retornava só string, esse devolve estrutura — assim o componente pode
+// renderizar título + dica explicativa + botão de ação contextual.
+//
+// Inputs:
+// code — string vinda de error.error / data.error
+// providedMessage — string vinda de error.message / data.message
+// status — HTTP status (error.context?.status) — captura 502/503/504
+//
+// O edge function retorna { ok: false, error: '', message: '' }.
+function classifySendError(code, providedMessage, status) {
const c = String(code || '').toLowerCase();
- if (c === 'insufficient_credits') return 'Saldo de créditos insuficiente. Compre um pacote em Configurações → Créditos WhatsApp.';
- if (c.startsWith('twilio_send_failed')) return 'Não conseguimos enviar pelo WhatsApp oficial. Verifique se o canal está conectado em Configurações → WhatsApp.';
- if (c.includes('canal whatsapp nao configurado') || c.includes('canal whatsapp inativo')) return 'Nenhum canal WhatsApp ativo. Configure em Configurações → WhatsApp.';
- if (c.includes('credenciais evolution incompletas')) return 'As credenciais do WhatsApp Pessoal estão incompletas. Acesse Configurações → WhatsApp.';
- if (c.includes('twilio credenciais incompletas')) return 'As credenciais do WhatsApp Oficial estão incompletas. Contate o suporte.';
- if (c.startsWith('evolution retornou')) return 'O servidor do WhatsApp Pessoal não respondeu. Verifique se o celular está conectado.';
- if (c === 'auth') return 'Sua sessão expirou. Faça login novamente.';
- if (c === 'forbidden') return 'Você não tem permissão pra enviar por este canal.';
- if (c === 'edge function returned a non-2xx status code') return 'O servidor recusou o envio. Tente novamente ou contate o suporte.';
- // Fallback: retorna o código original se nada bateu
- return code || 'Falha ao enviar. Tente novamente.';
+ const hasMsg = !!providedMessage;
+ const httpStatus = Number(status) || null;
+
+ // ── 5xx (gateway/server offline): caso típico de Evolution/Twilio fora ──
+ // Quando a edge function de envio retorna 502/503/504, normalmente o
+ // provedor (Evolution self-hosted ou Twilio) está fora ou inalcançável.
+ if (httpStatus === 502 || httpStatus === 503 || httpStatus === 504) {
+ return {
+ code: c || `http_${httpStatus}`,
+ status: httpStatus,
+ message: 'Servidor de WhatsApp temporariamente fora do ar.',
+ hint: 'Os envios costumam voltar em poucos minutos. Se você ainda não configurou ou contratou o serviço, verifique abaixo.',
+ action: { label: 'Configurar WhatsApp', url: '/configuracoes/whatsapp' },
+ secondaryAction: { label: 'Comprar créditos', url: '/configuracoes/creditos-whatsapp' }
+ };
+ }
+
+ if (c === 'insufficient_credits') {
+ return {
+ code: c, status: httpStatus,
+ message: 'Saldo de créditos insuficiente.',
+ hint: 'Você precisa de créditos pra enviar mensagens pelo WhatsApp Oficial.',
+ action: { label: 'Comprar créditos', url: '/configuracoes/creditos-whatsapp' }
+ };
+ }
+
+ if (c.includes('canal whatsapp nao configurado') || c.includes('canal whatsapp inativo')) {
+ return {
+ code: c, status: httpStatus,
+ message: 'Nenhum canal WhatsApp ativo.',
+ hint: 'Você ainda não configurou um número. Conecte um WhatsApp Pessoal (gratuito) ou contrate o WhatsApp Oficial.',
+ action: { label: 'Configurar agora', url: '/configuracoes/whatsapp' }
+ };
+ }
+
+ if (c.includes('credenciais evolution incompletas')) {
+ return {
+ code: c, status: httpStatus,
+ message: 'WhatsApp Pessoal está com credenciais incompletas.',
+ hint: 'Termine a conexão do seu número (escaneie o QR Code novamente, se necessário).',
+ action: { label: 'Configurações WhatsApp', url: '/configuracoes/whatsapp' }
+ };
+ }
+
+ if (c.includes('twilio credenciais incompletas')) {
+ return {
+ code: c, status: httpStatus,
+ message: 'WhatsApp Oficial está com credenciais incompletas.',
+ hint: 'Contate o suporte pra finalizar a configuração da sua conta Twilio.',
+ action: null
+ };
+ }
+
+ if (c.startsWith('evolution retornou')) {
+ return {
+ code: c, status: httpStatus,
+ message: 'WhatsApp Pessoal não respondeu.',
+ hint: 'Verifique se o celular conectado está com internet e a sessão do WhatsApp ativa.',
+ action: { label: 'Ver status', url: '/configuracoes/whatsapp' }
+ };
+ }
+
+ if (c.startsWith('twilio_send_failed')) {
+ return {
+ code: c, status: httpStatus,
+ message: 'WhatsApp Oficial recusou o envio.',
+ hint: 'Verifique se o canal está conectado e se o número de destino aceita mensagens template fora da janela de 24h.',
+ action: { label: 'Configurações WhatsApp', url: '/configuracoes/whatsapp' }
+ };
+ }
+
+ if (c === 'auth') {
+ return {
+ code: c, status: httpStatus,
+ message: 'Sua sessão expirou.',
+ hint: 'Faça login novamente pra continuar.',
+ action: null
+ };
+ }
+
+ if (c === 'forbidden') {
+ return {
+ code: c, status: httpStatus,
+ message: 'Você não tem permissão pra enviar por este canal.',
+ hint: 'Verifique seu plano ou contate o admin do tenant.',
+ action: null
+ };
+ }
+
+ if (c === 'edge function returned a non-2xx status code') {
+ return {
+ code: c, status: httpStatus,
+ message: 'O servidor recusou o envio.',
+ hint: 'Tente novamente em instantes. Se persistir, verifique configuração ou contate o suporte.',
+ action: { label: 'Configurar WhatsApp', url: '/configuracoes/whatsapp' }
+ };
+ }
+
+ // Fallback genérico — usa providedMessage se houver, senão o código
+ return {
+ code: c || 'unknown',
+ status: httpStatus,
+ message: hasMsg ? providedMessage : 'Não foi possível enviar a mensagem.',
+ hint: 'Tente novamente em alguns instantes. Se persistir, verifique sua conexão e a configuração do WhatsApp.',
+ action: { label: 'Configurações WhatsApp', url: '/configuracoes/whatsapp' }
+ };
}
export const useConversationDrawerStore = defineStore('conversationDrawer', {
@@ -39,6 +142,9 @@ export const useConversationDrawerStore = defineStore('conversationDrawer', {
messages: [],
loading: false,
sending: false,
+ // Último erro de envio (estruturado, com hint + action) — fica no chat
+ // como banner persistente até user enviar com sucesso ou dispensar.
+ lastSendError: null, // { code, status, message, hint, action: { label, url }, secondaryAction? }
error: null,
_realtimeChannel: null,
// cache compartilhado
@@ -61,6 +167,8 @@ export const useConversationDrawerStore = defineStore('conversationDrawer', {
if (!thread) return;
this.thread = { ...thread };
this.isOpen = true;
+ // Erros de envio são por-conversa — não vazam pra próxima
+ this.lastSendError = null;
await this.loadMessages();
this._ensureTenantName();
this._subscribeRealtime();
@@ -216,7 +324,11 @@ export const useConversationDrawerStore = defineStore('conversationDrawer', {
async sendMessage(text) {
const cleanText = String(text || '').trim();
if (!cleanText || this.sending) return { ok: false, error: 'Mensagem vazia' };
- if (!this.thread?.contact_number) return { ok: false, error: 'Conversa sem número de contato' };
+ if (!this.thread?.contact_number) {
+ const cls = { code: 'no_contact', status: null, message: 'Conversa sem número de contato', hint: 'Vincule um paciente com telefone à conversa antes de enviar.', action: null };
+ this.lastSendError = cls;
+ return { ok: false, error: cls.message, classification: cls };
+ }
const tenantStore = useTenantStore();
this.sending = true;
@@ -230,27 +342,41 @@ export const useConversationDrawerStore = defineStore('conversationDrawer', {
}
});
- // Erro HTTP (não-2xx) — extrai body da resposta pra mostrar msg amigável
+ // Erro HTTP (não-2xx) — extrai status + body da resposta
if (error) {
+ const status = error.context?.status || null;
let body = null;
try {
body = await error.context?.json?.();
- } catch { /* noop */ }
- return { ok: false, error: friendlySendError(body?.error, body?.message) };
+ } catch { /* noop — pode não ter body JSON em 502 */ }
+ const cls = classifySendError(body?.error, body?.message, status);
+ this.lastSendError = cls;
+ return { ok: false, error: cls.message, classification: cls };
}
if (!data?.ok) {
- return { ok: false, error: friendlySendError(data?.error, data?.message) };
+ const cls = classifySendError(data?.error, data?.message, null);
+ this.lastSendError = cls;
+ return { ok: false, error: cls.message, classification: cls };
}
+ // Sucesso — limpa banner de erro anterior
+ this.lastSendError = null;
this.thread.kanban_status = 'awaiting_patient';
return { ok: true, data };
} catch (err) {
- return { ok: false, error: friendlySendError(err?.message || String(err)) };
+ // Provavelmente erro de rede (fetch falhou). Sem `error.context`.
+ const cls = classifySendError(err?.message || String(err), null, null);
+ this.lastSendError = cls;
+ return { ok: false, error: cls.message, classification: cls };
} finally {
this.sending = false;
}
},
+ dismissSendError() {
+ this.lastSendError = null;
+ },
+
async setKanbanStatus(status) {
if (!['urgent', 'awaiting_us', 'awaiting_patient', 'resolved'].includes(status)) return;
if (!this.thread) return;