Files
chatc2/src/public/chat.html
T
AyronSantos 9cf5f00243 ponytail: remove resolucaoSetup, compressVideo, sendEvolutionMessage, sendEvolution; add migration #24
- delete: src/resolucaoSetup.js (migration paralela, consolidado em #24)
- shrink: chatController.sendEvolutionMessage (140 -> 6 linhas, delega ao evolutionService)
- shrink: triageController.sendEvolution (55 -> 10 linhas, delega ao evolutionService)
- delete: compressVideo em chatController (codigo morto, chamado so pelo old sendEvolutionMessage)
- add: migration #24 consolida DDL que estava espalhada em runtime
- fix: configController.remove garantirEstrutura calls (arquivo removido)
- net: -270 linhas, -1 arquivo
2026-07-01 13:24:06 +00:00

2485 lines
98 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
/* ===== CHAT PAGE - Estilos específicos ===== */
body { background:var(--surface-3); display:flex; height:100vh; overflow:hidden; }
/* ===== SIDEBAR ESQUERDA ===== */
.sidebar-left {
width: 300px;
background: linear-gradient(180deg, #1e1b4b 0%, #312e81 100%);
display: flex;
flex-direction: column;
flex-shrink: 0;
color: #fff;
}
.admin-only-nav { display: none; }
.sidebar-left .header {
padding: 18px 14px 14px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.sidebar-left .header h2 {
font-size: 17px;
font-weight: 700;
color: #fff;
letter-spacing: -0.2px;
}
.sidebar-left .header-nav {
display: flex;
gap: 3px;
margin-top: 6px;
}
.sidebar-left .header-nav a {
padding: 6px 9px;
border-radius: 6px;
color: rgba(255,255,255,0.55);
text-decoration: none;
font-size: 17px;
transition: all .15s;
display: flex;
align-items: center;
}
.sidebar-left .header-nav a:hover {
background: rgba(255,255,255,0.14);
color: #fff;
}
/* Tabs de navegação */
.sidebar-left .nav-tabs {
display: flex;
border-bottom: 1px solid rgba(255,255,255,0.08);
padding: 0 6px;
gap: 2px;
}
.sidebar-left .nav-tabs a {
flex: 1;
text-align: center;
padding: 9px 4px;
font-size: 11px;
font-weight: 500;
color: rgba(255,255,255,0.45);
text-decoration: none;
border-bottom: 2px solid transparent;
transition: all .15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-left .nav-tabs a:hover { color: rgba(255,255,255,0.8); }
.sidebar-left .nav-tabs a.active {
color: #fff;
border-bottom-color: #a78bfa;
font-weight: 600;
}
/* Search box */
.sidebar-left .search-box {
padding: 10px 12px;
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.sidebar-left .search-box input[type="text"] {
flex: 1;
min-width: 0;
padding: 8px 12px;
border: 1px solid rgba(255,255,255,0.18);
border-radius: 8px;
font-size: 13px;
outline: none;
background: rgba(255,255,255,0.09);
color: #fff;
transition: all .15s;
width: auto;
box-shadow: none;
}
.sidebar-left .search-box input[type="text"]::placeholder { color: rgba(255,255,255,0.35); }
.sidebar-left .search-box input[type="text"]:focus {
border-color: rgba(255,255,255,0.38);
background: rgba(255,255,255,0.14);
box-shadow: none;
}
.sidebar-left .search-box label {
font-size: 11px;
color: rgba(255,255,255,0.45);
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
cursor: pointer;
}
.sidebar-left .search-box input[type="checkbox"] {
width: 13px;
height: 13px;
flex: none;
padding: 0;
cursor: pointer;
accent-color: #a78bfa;
box-shadow: none;
border: none;
background: transparent;
}
/* Lista de conversas */
.sidebar-left .conversations-list { flex: 1; overflow-y: auto; }
.sidebar-left .conversations-list::-webkit-scrollbar { width: 4px; }
.sidebar-left .conversations-list::-webkit-scrollbar-track { background: transparent; }
.sidebar-left .conversations-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 4px; }
.conv-item {
padding: 11px 14px;
border-bottom: 1px solid rgba(255,255,255,0.05);
cursor: pointer;
transition: background .15s;
display: flex;
gap: 10px;
align-items: center;
}
.conv-item:hover { background: rgba(255,255,255,0.08); }
.conv-item.active { background: rgba(255,255,255,0.16); }
.conv-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
letter-spacing: -0.5px;
}
.conv-body { flex: 1; min-width: 0; }
.conv-item .conv-nome {
font-size: 13px;
font-weight: 600;
color: rgba(255,255,255,0.95);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conv-item .conv-ultima {
font-size: 12px;
color: rgba(255,255,255,0.45);
margin-top: 1px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-item .conv-status {
font-size: 10px;
color: rgba(255,255,255,0.35);
margin-top: 2px;
}
.conv-item .conv-badges { display: flex; gap: 3px; margin-top: 3px; flex-wrap: wrap; }
.conv-item .conv-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
color: #fff;
font-weight: 500;
opacity: 0.9;
}
/* Sidebar footer */
.sidebar-footer {
padding: 10px 12px;
border-top: 1px solid rgba(255,255,255,0.08);
flex-shrink: 0;
}
.sidebar-footer button.dark-mode-toggle {
width: 100%;
margin-bottom: 4px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.12);
font-size: 13px;
color: rgba(255,255,255,0.75);
transition: all .15s;
text-align: left;
display: block;
}
.sidebar-footer button.dark-mode-toggle:hover {
background: rgba(255,255,255,0.14);
color: #fff;
}
.sidebar-footer a {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
color: rgba(255,255,255,0.52);
text-decoration: none;
font-size: 13px;
cursor: pointer;
transition: all .15s;
}
.sidebar-footer a:hover {
background: rgba(255,255,255,0.10);
color: #fca5a5;
}
/* ===== CHAT CENTER ===== */
.chat-center { flex: 1; display: flex; flex-direction: column; background: #fff; min-width: 0; }
.chat-header {
padding: 11px 18px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
flex-shrink: 0;
}
.chat-header .info h3 { font-size: 15px; font-weight: 600; color: var(--text-primary); }
.chat-header .info span { font-size: 12px; color: var(--text-muted); }
.chat-header .actions { display: flex; gap: 7px; }
.chat-header .actions button {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
font-size: 12px;
font-weight: 500;
cursor: pointer;
color: var(--text-secondary);
transition: all .15s;
}
.chat-header .actions button:hover { background: var(--surface-3); border-color: #d1d5db; }
.chat-header .actions .btn-finalizar {
background: var(--danger);
color: #fff;
border-color: var(--danger);
font-weight: 600;
}
.chat-header .actions .btn-finalizar:hover { background: var(--danger-text); border-color: var(--danger-text); }
/* Área de mensagens - estilo chat */
.messages-area {
flex: 1;
padding: 16px 20px;
overflow-y: auto;
background-color: #e5ddd5;
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23bfb6ab' fill-opacity='0.18'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
display: flex;
flex-direction: column;
gap: 6px;
}
.messages-area::-webkit-scrollbar { width: 5px; }
.messages-area::-webkit-scrollbar-track { background: transparent; }
.messages-area::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 4px; }
/* Bolhas de mensagem */
.msg {
max-width: 70%;
padding: 8px 12px 5px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
position: relative;
word-break: break-word;
}
.msg.cliente {
align-self: flex-start;
background: #fff;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
color: var(--text-primary);
}
.msg.usuario {
align-self: flex-end;
background: #dcf8c6;
color: var(--text-primary);
border-bottom-right-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.msg.sistema {
align-self: center;
background: rgba(255,255,255,0.75);
color: var(--text-muted);
font-size: 12px;
padding: 4px 14px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
max-width: 60%;
text-align: center;
}
.msg.privada {
background: #fef9c3;
color: #854d0e;
align-self: flex-end;
border: 1px solid #fde047;
border-bottom-right-radius: 4px;
}
.msg .hora {
font-size: 10px;
opacity: 0.55;
margin-top: 3px;
text-align: right;
display: block;
}
.msg img {
max-width: 260px;
border-radius: 8px;
cursor: pointer;
display: block;
}
/* Estado: enviando */
.msg.enviando {
opacity: 0.55;
}
.msg.enviando .hora::after {
content: ' ⌛';
font-size: 10px;
}
/* Estado: erro ao enviar */
.msg.erro {
border: 1px solid var(--danger);
background: var(--danger-bg);
}
.msg.erro .msg-erro-acoes {
display: flex;
gap: 6px;
margin-top: 6px;
align-items: center;
font-size: 12px;
color: var(--danger-text);
}
.msg.erro .btn-reenviar {
padding: 3px 10px;
border: 1px solid var(--danger-text);
border-radius: 6px;
background: #fff;
color: var(--danger-text);
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all .15s;
}
.msg.erro .btn-reenviar:hover {
background: var(--danger-text);
color: #fff;
}
/* Indicador de conexão */
.connection-status {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 500;
padding: 2px 10px;
border-radius: 9999px;
margin-left: 8px;
}
.connection-status.online {
background: var(--success-bg);
color: var(--success-text);
}
.connection-status.offline {
background: var(--danger-bg);
color: var(--danger-text);
}
.connection-status .conn-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.connection-status.online .conn-dot { background: var(--success); }
.connection-status.offline .conn-dot { background: var(--danger); animation: pulse 1.5s infinite; }
/* Input de chat */
.chat-input {
padding: 10px 14px;
border-top: 1px solid var(--border);
display: flex;
gap: 6px;
align-items: flex-end;
background: #f0f2f5;
flex-shrink: 0;
}
.chat-input textarea {
flex: 1;
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: 9999px;
font-size: 14px;
resize: none;
outline: none;
max-height: 120px;
font-family: inherit;
background: #fff;
color: var(--text-primary);
line-height: 1.4;
transition: border-color .15s;
}
.chat-input textarea:focus { border-color: var(--primary); }
.chat-input textarea::placeholder { color: var(--text-faint); }
.chat-input .btn-enviar {
width: 40px;
height: 40px;
background: #25d366;
color: #fff;
border: none;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all .15s;
flex-shrink: 0;
padding: 0;
}
.chat-input .btn-enviar:hover { background: #22c55e; transform: scale(1.06); }
.chat-input .btn-enviar:disabled {
opacity: 0.35;
cursor: not-allowed;
transform: none;
}
.chat-input .btn-enviar:disabled:hover {
background: #25d366;
transform: none;
}
/* Botões de ação agrupados no menu "+" */
.chat-input .btn-action {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 18px;
color: var(--text-muted);
transition: all .15s;
padding: 0;
}
.chat-input .btn-action:hover { background: var(--border); color: var(--text-secondary); }
@media (max-width: 768px) {
.chat-input .btn-action {
width: 44px;
height: 44px;
font-size: 20px;
}
.chat-input .btn-enviar {
width: 44px;
height: 44px;
font-size: 20px;
}
}
/* Menu "+" dropdown */
.chat-input .action-menu-wrap {
position: relative;
flex-shrink: 0;
}
.chat-input .action-menu {
display: none;
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 6px;
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
padding: 6px;
z-index: 50;
min-width: 160px;
}
.chat-input .action-menu.show { display: block; }
.chat-input .action-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
border: none;
border-radius: 6px;
background: transparent;
font-size: 13px;
cursor: pointer;
color: var(--text-secondary);
white-space: nowrap;
text-align: left;
transition: background .15s;
}
.chat-input .action-menu button:hover { background: var(--surface-3); }
/* Toggle de mensagem privada com texto */
.chat-input .privada-toggle {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 9999px;
background: #fff;
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
transition: all .15s;
white-space: nowrap;
flex-shrink: 0;
}
.chat-input .privada-toggle:hover { background: var(--surface-3); }
.chat-input .privada-toggle.ativa { background: var(--warning-bg); color: #d97706; border-color: #fde047; }
.chat-input .privada-toggle .priv-icon { font-size: 14px; }
/* Gravação */
.chat-input .btn-audio.recording {
background: var(--danger-bg);
color: var(--danger);
animation: pulse 1s infinite;
}
@keyframes pulse { 0%{transform:scale(1)} 50%{transform:scale(1.15)} 100%{transform:scale(1)} }
/* ===== SIDEBAR DIREITA ===== */
.sidebar-right {
width: 280px;
background: #fff;
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
}
.sidebar-right .header {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
flex-shrink: 0;
}
.sidebar-right .header h3 {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
}
/* Accordions no painel direito */
.sidebar-right .accordion-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 16px;
border: none;
border-bottom: 1px solid var(--surface-3);
background: none;
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
cursor: pointer;
transition: background .15s;
}
.sidebar-right .accordion-toggle:hover { background: var(--surface-2); }
.sidebar-right .accordion-toggle .arrow {
font-size: 10px;
transition: transform .15s;
}
.sidebar-right .accordion-toggle.open .arrow { transform: rotate(90deg); }
.sidebar-right .accordion-body {
display: none;
}
.sidebar-right .accordion-body.open { display: block; }
.sidebar-right .cliente-foto {
width: 70px;
height: 70px;
border-radius: 50%;
margin: 18px auto 10px;
background: linear-gradient(135deg, var(--primary), #764ba2);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
font-weight: 700;
overflow: hidden;
box-shadow: 0 4px 14px rgba(102,126,234,0.3);
flex-shrink: 0;
}
.sidebar-right .cliente-foto img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-right .info-section {
padding: 10px 16px;
border-bottom: 1px solid var(--surface-3);
}
.sidebar-right .info-section .label {
font-size: 10px;
text-transform: uppercase;
color: var(--text-faint);
letter-spacing: 0.07em;
font-weight: 600;
margin-bottom: 4px;
}
.sidebar-right .info-section .value { font-size: 13px; color: var(--text-secondary); }
.sidebar-right .info-section select {
width: 100%;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
outline: none;
color: var(--text-secondary);
background: #fff;
cursor: pointer;
transition: border-color .15s;
}
.sidebar-right .info-section select:focus { border-color: var(--primary); }
.sidebar-right .info-section .inadimplente { color: var(--danger); font-weight: 600; }
.sidebar-right .info-section .adimplente { color: var(--success); font-weight: 600; }
.sidebar-right .acoes { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
.sidebar-right .acoes button {
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-align: left;
color: var(--text-secondary);
transition: all .15s;
}
.sidebar-right .acoes button:hover { background: var(--surface-3); border-color: #d1d5db; }
.sidebar-right .acoes button .icon { margin-right: 6px; }
.sidebar-right .labels-section { padding: 10px 16px; }
.sidebar-right .labels-section .label-tag {
display: inline-block;
padding: 3px 11px;
border-radius: 12px;
font-size: 12px;
color: #fff;
margin: 2px;
font-weight: 500;
cursor: pointer;
transition: opacity .15s;
}
.sidebar-right .labels-section .label-tag:hover { opacity: 0.82; }
/* ===== MODAL ===== */
.modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.45);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.show { display: flex; }
.modal {
background: #fff;
border-radius: 16px;
padding: 26px;
width: 90%;
max-width: 420px;
box-shadow: 0 24px 64px rgba(0,0,0,0.2);
animation: modalIn .18s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.96) translateY(-8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal h3 { margin-bottom: 14px; font-size: 16px; font-weight: 700; color: var(--text-primary); }
.modal input, .modal select {
width: 100%;
padding: 9px 13px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 14px;
margin-bottom: 12px;
outline: none;
transition: border-color .15s;
background: var(--surface-2);
color: var(--text-primary);
}
.modal input:focus { border-color: var(--primary); background: #fff; box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.modal .btn-group { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.modal .btn-group button {
padding: 9px 18px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: #fff;
color: var(--text-secondary);
transition: all .15s;
}
.modal .btn-group button:hover { background: var(--surface-3); }
.modal .btn-group .btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
.modal .btn-group .btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
/* ===== DRAG OVERLAY ===== */
.drag-overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(102,126,234,0.12);
border: 3px dashed var(--primary);
z-index: 999;
pointer-events: none;
font-size: 22px;
font-weight: 600;
color: var(--primary);
}
.drag-overlay.show { display: flex; align-items: center; justify-content: center; }
/* ===== EMPTY STATE ===== */
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: var(--text-faint);
gap: 6px;
user-select: none;
}
.empty-state .icon { font-size: 52px; opacity: 0.45; }
.empty-state p { font-size: 15px; color: var(--text-muted); }
/* Botões só do mobile (voltar / info) */
.mobile-only { display: none; }
/* ===== RESPONSIVO: navegação de painel único (lista → conversa → info) ===== */
@media (max-width: 768px) {
.sidebar-left { width: 100%; }
.chat-center { display: none; }
/* Quando uma conversa está aberta, mostra o chat e esconde a lista */
body.chat-aberto .sidebar-left { display: none; }
body.chat-aberto .chat-center { display: flex; }
/* Painel de informações vira overlay deslizante (oculto por padrão) */
.sidebar-right {
position: fixed;
top: 0; right: 0; bottom: 0;
width: 88%; max-width: 340px;
z-index: 1200;
box-shadow: -4px 0 24px rgba(0,0,0,0.28);
display: none !important;
}
body.info-aberto .sidebar-right { display: flex !important; }
.mobile-only { display: inline-flex !important; align-items: center; justify-content: center; }
.chat-header { gap: 8px; }
.chat-header .info h3 { font-size: 14px; }
}
/* ===== DARK MODE: Novos componentes ===== */
body.dark-mode .connection-status.online { background: #0a2e0a; color: #4ade80; }
body.dark-mode .connection-status.offline { background: #2e0a0a; color: #ff6b6b; }
body.dark-mode .action-menu { background: #16213e; border-color: #0f3460; }
body.dark-mode .action-menu button { color: #e0e0e0; }
body.dark-mode .action-menu button:hover { background: #1a2744; }
body.dark-mode .privada-toggle { background: #1a1a2e; border-color: #0f3460; color: #888; }
body.dark-mode .privada-toggle:hover { background: #16213e; }
body.dark-mode .privada-toggle.ativa { background: #3a2e0a; color: #ffd700; border-color: #5a4a0a; }
body.dark-mode .accordion-toggle { color: #888; border-bottom-color: #0f3460; }
body.dark-mode .accordion-toggle:hover { background: #1a2744; }
body.dark-mode .msg.erro { background: #2e0a0a; border-color: #8b0000; }
body.dark-mode .msg.erro .msg-erro-acoes { color: #ff6b6b; }
body.dark-mode .msg.erro .btn-reenviar { background: #1a1a2e; border-color: #8b0000; color: #ff6b6b; }
body.dark-mode .msg.erro .btn-reenviar:hover { background: #8b0000; color: #fff; }
body.dark-mode .msg.enviando { opacity: 0.4; }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
</head>
<body>
<!-- Drag overlay -->
<div class="drag-overlay" id="dragOverlay">📎 Solte o arquivo aqui</div>
<!-- Modal confirmar finalização -->
<div class="modal-overlay" id="modalConfirmarFinalizar">
<div class="modal" style="max-width:420px">
<h3>⚠️ Finalizar conversa</h3>
<p style="font-size:13px;color:#6b7280;margin-bottom:12px">Tem certeza que deseja finalizar este atendimento?</p>
<!-- Agendamento -->
<div id="agendamentoBox" style="margin-bottom:14px;padding:14px;background:var(--surface-2);border-radius:8px;border:1px solid var(--border)">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--text-secondary);cursor:pointer;margin-bottom:0">
<input type="checkbox" id="chkAgendar" onchange="toggleAgendamento()" style="accent-color:var(--primary);width:16px;height:16px"> 📅 Agendar retorno
</label>
<div id="agendamentoCampos" style="display:none;margin-top:12px">
<div style="display:flex;gap:8px;margin-bottom:8px">
<div style="flex:1">
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Data</label>
<input type="date" id="agData" style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;outline:none;background:var(--surface);color:var(--text-primary)">
</div>
<div style="width:90px">
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Hora</label>
<input type="time" id="agHora" style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;outline:none;background:var(--surface);color:var(--text-primary)">
</div>
</div>
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Anotação</label>
<textarea id="agNota" rows="2" placeholder="Ex: Confirmar pagamento do boleto..." style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;outline:none;background:var(--surface);color:var(--text-primary)"></textarea>
</div>
</div>
<div class="btn-group">
<button onclick="fecharModal('modalConfirmarFinalizar')">Cancelar</button>
<button class="btn-primary" onclick="fecharModal('modalConfirmarFinalizar');finalizarConversa()">Finalizar</button>
</div>
</div>
</div>
<!-- Modal retorno agendado -->
<div class="modal-overlay" id="modalRetorno">
<div class="modal" style="max-width:460px">
<h3>📅 Retorno agendado</h3>
<p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">Conversa criada. Revise o texto antes de enviar:</p>
<input type="hidden" id="retornoConversaId">
<div class="form-group">
<textarea id="retornoTextoSugerido" rows="4" style="width:100%;padding:10px 14px;border:2px solid var(--border);border-radius:8px;font-size:14px;font-family:inherit;resize:vertical;outline:none;background:var(--surface-2);color:var(--text-primary)"></textarea>
</div>
<p style="font-size:11px;color:var(--text-faint);margin-bottom:16px">Use [CLIENTE], [CONTATO], [EMPRESAF] para dados automáticos.</p>
<div class="btn-group">
<button onclick="document.getElementById('retornoConversaId').value='';fecharModal('modalRetorno')">Cancelar</button>
<button class="btn-primary" onclick="confirmarRetorno()">✅ Abrir conversa</button>
</div>
</div>
</div>
<!-- Modal associar cliente -->
<div class="modal-overlay" id="modalAssociar">
<div class="modal">
<h3>🔗 Associar</h3>
<div style="display:flex;gap:12px;margin-bottom:12px">
<label style="cursor:pointer;font-size:13px"><input type="radio" name="tipoAssoc" value="cliente" checked onchange="mudarTipoAssoc()"> Cliente</label>
<label style="cursor:pointer;font-size:13px"><input type="radio" name="tipoAssoc" value="dependente" onchange="mudarTipoAssoc()"> Dependente</label>
</div>
<p style="font-size:13px;color:#6b7280;margin-bottom:12px" id="assocLabel">Digite o nome, CPF ou matrícula do cliente:</p>
<input type="text" id="associarBusca" placeholder="Buscar..." onkeyup="buscarClienteAssociar(event)">
<div id="associarResultados" style="max-height:200px;overflow-y:auto"></div>
<div class="btn-group">
<button onclick="fecharModal('modalAssociar')">Cancelar</button>
</div>
</div>
</div>
<!-- Modal enviar boleto -->
<div class="modal-overlay" id="modalBoleto">
<div class="modal">
<h3>💳 Enviar boleto</h3>
<p style="font-size:13px;color:#6b7280;margin-bottom:12px">Selecione o título a enviar para o cliente:</p>
<div id="boletoLista" style="max-height:320px;overflow-y:auto"></div>
<div class="btn-group">
<button onclick="fecharModalBoleto()">Fechar</button>
</div>
</div>
</div>
<div class="sidebar-left">
<div class="header">
<h2>💬 Chatc2</h2>
<div class="header-nav">
<a id="navDashChat" href="#" title="Dashboard">📊</a>
<a id="navClientsChat" href="#" title="Clientes">👥</a>
<a id="navConfigChat" class="admin-only-nav" href="#" title="Configurações">⚙️</a>
</div>
</div>
<div id="empresaSwitcherWrap" style="display:none;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.08)">
<select id="empresaSwitcher" onchange="trocarEmpresa(this.value)" style="width:100%;padding:6px 8px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;font-size:12px;background:rgba(255,255,255,0.06);color:#fff;outline:none"></select>
</div>
<div class="nav-tabs">
<a href="#" class="active" onclick="mudarFiltro('mine',this)">Minhas (0)</a>
<a href="#" onclick="mudarFiltro('unassigned',this)">Sem atend. (0)</a>
<a href="#" id="tabTodasConvs" onclick="mudarFiltro('all',this)" class="admin-only-nav">Todas</a>
<a href="#" onclick="mudarFiltro('equipe',this)" id="tabEquipeConvs">Equipe (0)</a>
<a href="#" onclick="mudarFiltro('scheduled',this)" id="tabAgendadas">📅 Agendadas (0)</a>
</div>
<div class="search-box">
<input type="text" id="searchConv" placeholder="🔍 Buscar conversas..." onkeyup="carregarConversas()">
<button id="btnNovaConversa" onclick="mostrarModalNovaConversa()" title="Nova conversa" style="background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.2);color:#fff;border-radius:8px;width:32px;height:32px;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s" onmouseover="this.style.background='rgba(255,255,255,0.22)'" onmouseout="this.style.background='rgba(255,255,255,0.12)'">+</button>
<label>
<input type="checkbox" id="chkFinalizadas" onchange="carregarConversas()"> Finalizadas
</label>
</div>
<div class="conversations-list" id="conversationsList"></div>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()">🌙 Escuro</button>
<a onclick="logout()">🚪 Sair</a>
</div>
</div>
<div class="chat-center">
<div class="chat-header" id="chatHeader" style="display:none">
<button class="mobile-only" onclick="voltarLista()" title="Voltar" style="background:none;border:none;font-size:20px;cursor:pointer;padding:4px 6px;color:#374151" aria-label="Voltar para lista de conversas"></button>
<button class="menu-hamburger" onclick="toggleSidebar()" aria-label="Menu" style="display:none;margin-right:4px"></button>
<div class="info">
<h3 id="chatNome"></h3>
<span id="chatStatus"></span>
<span class="connection-status online" id="connectionStatus" style="display:none">
<span class="conn-dot"></span>
<span id="connectionLabel">Conectado</span>
</span>
<h3 id="chatNome"></h3>
<span id="chatStatus"></span>
</div>
<div class="actions">
<button class="mobile-only" onclick="toggleInfo()" title="Informações" aria-label="Informações do cliente"></button>
<button class="btn-finalizar" onclick="confirmarFinalizar()" aria-label="Finalizar conversa">✅ Finalizar</button>
</div>
</div>
<div class="messages-area" id="messagesArea">
<div class="empty-state">
<div class="icon">💬</div>
<p>Selecione uma conversa</p>
</div>
</div>
<div class="chat-input" id="chatInput" style="display:none">
<button class="privada-toggle" id="privadaToggle" onclick="togglePrivada()" title="Alternar mensagem privada" aria-label="Mensagem privada">
<span class="priv-icon">🔒</span> <span id="privadaLabel">Normal</span>
</button>
<div class="action-menu-wrap">
<button class="btn-action" id="btnPlus" onclick="toggleActionMenu()" title="Mais ações" aria-label="Mais ações">+</button>
<div class="action-menu" id="actionMenu">
<button onclick="document.getElementById('fileInput').click();toggleActionMenu()" aria-label="Anexar arquivo">📎 Anexar</button>
<button id="btnAudio" class="btn-audio" onclick="toggleGravacao();toggleActionMenu()" aria-label="Gravar áudio">🎤 Gravar</button>
</div>
</div>
<input type="file" id="fileInput" style="display:none" accept="image/*,audio/*" onchange="enviarMidia(this)" aria-label="Selecionar arquivo">
<textarea id="msgInput" placeholder="Digite uma mensagem..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();enviarMensagem()}" oninput="toggleBtnEnviar()" aria-label="Mensagem de texto"></textarea>
<button class="btn-enviar" id="btnEnviar" onclick="enviarMensagem()" title="Enviar mensagem" aria-label="Enviar mensagem" disabled></button>
<div id="audioPreview" style="display:none;align-items:center;gap:8px;flex:1">
<audio controls style="height:38px;flex:1" id="audioPreviewPlayer" aria-label="Pré-visualização do áudio"></audio>
<button class="btn-enviar" onclick="enviarAudioGravado()" title="Enviar áudio" aria-label="Enviar áudio gravado"></button>
<button class="btn-action" onclick="cancelarAudioGravado()" title="Cancelar" aria-label="Cancelar gravação" style="background:#fef2f2;color:#ef4444;width:36px;height:36px"></button>
</div>
</div>
</div>
<div class="sidebar-right" id="sidebarRight" style="display:none">
<div class="header"><h3>📋 Informações</h3></div>
<div class="cliente-foto" id="clienteFoto">?</div>
<button class="accordion-toggle open" onclick="toggleAccordion(this,'accordionCliente')">
DADOS DO CLIENTE <span class="arrow"></span>
</button>
<div class="accordion-body open" id="accordionCliente">
<div class="info-section" id="clienteInfoContainer"></div>
</div>
<!-- Fluxo de Resolução -->
<button class="accordion-toggle" id="accResolucaoToggle" style="display:none" onclick="toggleAccordion(this,'accordionResolucao')">
RESOLUÇÃO <span class="arrow"></span>
</button>
<div class="accordion-body" id="accordionResolucao">
<div class="info-section" id="resolucaoPanel" style="display:none">
<div class="label" style="font-size:10px;text-transform:uppercase;color:#9ca3af;margin-bottom:6px">RESOLUÇÃO DO ATENDIMENTO</div>
<div id="motivoWrap" style="display:none;margin-bottom:8px">
<select id="selectMotivo" style="width:100%;padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;outline:none">
<option value="">Selecione o motivo...</option>
</select>
</div>
<div id="resolucaoWrap" style="display:none">
<textarea id="campoResolucao" rows="3" placeholder="Descreva a resolução do atendimento..." style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;outline:none;resize:vertical;font-family:inherit"></textarea>
</div>
</div>
</div>
<button class="accordion-toggle open" onclick="toggleAccordion(this,'accordionClassificacao')">
CLASSIFICAÇÃO <span class="arrow"></span>
</button>
<div class="accordion-body open" id="accordionClassificacao">
<div class="info-section">
<div class="label" style="font-size:10px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">ATENDENTE</div>
<select id="selectAtendente" style="width:100%;padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;outline:none" onchange="mudarAtendente(this)">
<option value="">Selecione...</option>
</select>
</div>
<div class="info-section">
<div class="label" style="font-size:10px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">EQUIPE</div>
<select id="selectEquipe" style="width:100%;padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;outline:none" onchange="mudarEquipe(this)">
<option value="">Sem equipe</option>
</select>
</div>
<div class="labels-section" id="labelsContainer">
<div class="label" style="font-size:10px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">ETIQUETAS</div>
<div id="etiquetasAtivas" style="margin-bottom:6px"></div>
<select id="selectEtiqueta" style="width:100%;padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;outline:none" onchange="adicionarEtiqueta(this)">
<option value="">Adicionar etiqueta...</option>
</select>
</div>
</div>
<div class="acoes" id="acoesContainer">
<button onclick="mostrarModalAssociar()"><span class="icon">🔗</span> Associar</button>
<button onclick="irPaginaCliente()"><span class="icon">📋</span> Dados do Titular</button>
</div>
</div>
<script>
(function(){
'use strict';
const token = localStorage.getItem('chatc2_token');
const pathParts = window.location.pathname.split('/');
const alias = pathParts[2];
let empresaId = pathParts[4];
const conversaId = pathParts[6];
const usuarioLogado = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
// Navegacao rapida no header do chat
var tc = usuarioLogado.tipoChat || "A";
if (tc === "G") {
var els = document.querySelectorAll(".admin-only-nav");
for (var i = 0; i < els.length; i++) els[i].style.display = "";
}
document.getElementById("navDashChat").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/dashboard"; };
document.getElementById("navClientsChat").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/clients"; };
var cfgChat = document.getElementById("navConfigChat");
if (cfgChat) cfgChat.onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/settings"; };
if (!token) { window.location.href = '/app/' + alias + '/login'; return; }
let conversaAtiva = conversaId || null;
let mensagens = [];
let carregandoMsgs = false;
// Escapa HTML para evitar XSS armazenado (conteúdo vem do WhatsApp/cliente)
function esc(s){ return String(s == null ? '' : s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
// ===== FLUXO DE RESOLUÇÃO =====
var cfgResolucao = { motivoVisualizar:'N', motivoObrigatorio:'N', resolucaoVisualizar:'N', resolucaoObrigatorio:'N' };
var motivosList = [];
async function carregarConfigResolucao() {
try {
var cd = await (await fetch('/api/' + alias + '/company/config?empresaId=' + empresaId, { headers: { 'Authorization': 'Bearer ' + token } })).json();
if (cd.success && cd.data) cfgResolucao = cd.data;
var md = await (await fetch('/api/' + alias + '/motivos?empresaId=' + empresaId, { headers: { 'Authorization': 'Bearer ' + token } })).json();
if (md.success) motivosList = md.data || [];
} catch(e) {}
}
function aplicarPainelResolucao(conv) {
var panel = document.getElementById('resolucaoPanel');
var accToggle = document.getElementById('accResolucaoToggle');
var accBody = document.getElementById('accordionResolucao');
if (!panel) return;
var verMot = cfgResolucao.motivoVisualizar === 'S';
var verRes = cfgResolucao.resolucaoVisualizar === 'S';
var mostrar = verMot || verRes;
panel.style.display = mostrar ? 'block' : 'none';
if (accToggle) accToggle.style.display = mostrar ? 'flex' : 'none';
if (mostrar && accBody) {
accToggle.classList.add('open');
accBody.classList.add('open');
}
document.getElementById('motivoWrap').style.display = verMot ? 'block' : 'none';
document.getElementById('resolucaoWrap').style.display = verRes ? 'block' : 'none';
if (verMot) {
var sel = document.getElementById('selectMotivo');
sel.innerHTML = '<option value="">Selecione o motivo...</option>' +
motivosList.map(function(m){ return '<option value="' + m.id + '">' + esc(m.descricao) + '</option>'; }).join('');
sel.value = (conv && conv.motivoId) ? String(conv.motivoId) : '';
}
if (verRes) {
document.getElementById('campoResolucao').value = (conv && conv.resolucao) ? conv.resolucao : '';
}
}
// ===== NAVEGAÇÃO =====
document.querySelectorAll('.sidebar-left .nav-tabs a').forEach(function(a) {
a.addEventListener('click', function(e) { e.preventDefault(); });
});
// ===== CARREGAR CONVERSAS =====
async function carregarConversas() {
// Aba Agendadas: carrega da API de agendamentos
if (filtroAtual === 'scheduled') {
return carregarAgendadas();
}
const busca = document.getElementById('searchConv').value;
var statusFilter = document.getElementById('chkFinalizadas').checked ? 'A,E,F' : 'A,E';
try {
const res = await fetch('/api/' + alias + '/conversations?empresaId=' + empresaId + '&status=' + statusFilter + '&filter=' + filtroAtual, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (!data.success) return;
// Atualiza contagens nos filtros
if (data.contagens) {
var filtros = document.querySelectorAll('.nav-tabs a');
filtros.forEach(function(f) {
var txt = f.textContent.replace(/\s*\(\d+\)/, '').trim();
if (f.getAttribute('onclick') && f.getAttribute('onclick').includes('mine')) {
f.innerHTML = txt + ' (' + (data.contagens.mine || 0) + ')';
} else if (f.getAttribute('onclick') && f.getAttribute('onclick').includes('unassigned')) {
f.innerHTML = txt + ' (' + (data.contagens.unassigned || 0) + ')';
} else if (f.getAttribute('onclick') && f.getAttribute('onclick').includes('equipe')) {
f.innerHTML = txt + ' (' + (data.contagens.equipe || 0) + ')';
}
});
}
renderConversas(data.data, busca);
} catch(e) { if (Chatc2Toast) Chatc2Toast.error('Erro ao carregar conversas'); }
}
var AVATAR_COLORS = ['#667eea','#764ba2','#f093fb','#4facfe','#43e97b','#fa709a','#fee140','#30cfd0','#a18cd1','#fda085'];
function avatarColor(str) {
var h = 0;
for (var i = 0; i < (str||'').length; i++) h = (h * 31 + str.charCodeAt(i)) & 0xffff;
return AVATAR_COLORS[h % AVATAR_COLORS.length];
}
function avatarInitials(name) {
if (!name) return '?';
var parts = name.trim().split(' ');
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
return name.substring(0, 2).toUpperCase();
}
function formatTime(dt) {
if (!dt) return '';
var d = new Date(dt);
var now = new Date();
var diff = now - d;
if (diff < 60000) return 'agora';
if (diff < 3600000) return Math.floor(diff/60000) + 'min';
if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'});
return d.toLocaleDateString('pt-BR', {day:'2-digit',month:'2-digit'});
}
function renderConversas(convs, busca) {
const list = document.getElementById('conversationsList');
let filtered = convs;
if (busca) {
const q = busca.toLowerCase();
filtered = convs.filter(function(c) {
return (c.nomeContato && c.nomeContato.toLowerCase().includes(q)) ||
(c.numero && c.numero.includes(q));
});
}
if (filtered.length === 0) {
list.innerHTML = '<div style="padding:24px;text-align:center;color:rgba(255,255,255,0.4);font-size:13px">Nenhuma conversa</div>';
return;
}
list.innerHTML = filtered.map(function(c) {
const isActive = c.id == conversaAtiva ? 'active' : '';
const nome = c.nomeContato || c.numero || 'Desconhecido';
const cor = avatarColor(nome);
const iniciais = avatarInitials(nome);
const statusDot = c.status === 'E' ? '🟡' : c.status === 'A' ? '🟢' : '⚫';
const labelsHtml = (c.labels || []).map(function(l) {
return '<span class="conv-badge" style="background:' + esc(l.cor || '#667eea') + '">' + esc(l.nome || '') + '</span>';
}).join('');
const ultimaMsg = (c.ultimaMsg || '').substring(0, 60);
// Preview com ícone por tipo de mensagem
var previewHtml;
if (!c.ultimaMsg) {
previewHtml = '<em>Sem mensagens</em>';
} else if (c.ultimaTipo === 'audio' || c.ultimaTipo === 'ptt') {
previewHtml = '🎤 Mensagem de voz';
} else if (c.ultimaTipo === 'image') {
previewHtml = '🖼️ Imagem';
} else if (c.ultimaTipo === 'video') {
previewHtml = '🎥 Vídeo';
} else if (c.ultimaTipo === 'document') {
previewHtml = '📎 Documento';
} else if (c.ultimaTipo === 'triagem') {
previewHtml = '🤖 Menu de triagem';
} else {
previewHtml = esc(ultimaMsg);
}
const tempo = formatTime(c.dtUltimaMsg);
return '<div class="conv-item ' + isActive + '" onclick="abrirConversa(' + c.id + ')" data-id="' + c.id + '">' +
'<div class="conv-avatar" style="background:' + cor + '">' + iniciais + '</div>' +
'<div class="conv-body">' +
'<div style="display:flex;justify-content:space-between;align-items:baseline;gap:4px">' +
'<div class="conv-nome">' + esc(nome) + '</div>' +
'<div style="font-size:10px;color:rgba(255,255,255,0.35);flex-shrink:0">' + tempo + '</div>' +
'</div>' +
'<div class="conv-ultima">' + previewHtml + '</div>' +
'<div class="conv-status">' + statusDot + ' ' + (c.status === 'E' ? 'Em espera' : c.status === 'A' ? 'Aberto' : 'Finalizado') + '</div>' +
(labelsHtml ? '<div class="conv-badges">' + labelsHtml + '</div>' : '') +
'</div>' +
'</div>';
}).join('');
}
// ===== ABRIR CONVERSA =====
window.abrirConversa = async function(id, skipFotoRefresh) {
conversaAtiva = id;
// Mobile: alterna para a visão da conversa (e fecha o painel de info)
document.body.classList.add('chat-aberto');
document.body.classList.remove('info-aberto');
document.querySelectorAll('.conv-item').forEach(function(el) { el.classList.remove('active'); });
var item = document.querySelector('.conv-item[data-id="' + id + '"]');
if (item) item.classList.add('active');
// Atualiza a URL sem recarregar a página
var novaUrl = '/app/' + alias + '/company/' + empresaId + '/conversation/' + id;
window.history.pushState({ conversaId: id }, '', novaUrl);
document.getElementById('chatHeader').style.display = 'flex';
document.getElementById('chatInput').style.display = 'flex';
document.getElementById('sidebarRight').style.display = 'flex';
try {
var res = await fetch('/api/' + alias + '/conversations/' + id, {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success) return;
renderInfoConversa(data.data);
// Foto automática: se o cliente está vinculado mas sem foto, busca via Evolution
// em segundo plano e recarrega o painel quando a foto chegar.
if (!skipFotoRefresh && data.data && data.data.cliente && !data.data.cliente.foto) {
autoAtualizarFoto(id);
}
} catch(e) {}
carregarMensagens();
};
// Busca a foto do cliente via Evolution (fetchProfile) em segundo plano, sem bloquear.
function autoAtualizarFoto(id) {
fetch('/api/' + alias + '/evolution/refresh-photo/' + id, {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r){ return r.json(); }).then(function(r){
// Recarrega apenas se a foto foi atualizada e ainda estamos na mesma conversa
if (r && r.success && r.atualizada && conversaAtiva === id) abrirConversa(id, true);
}).catch(function(){});
}
function renderInfoConversa(conv) {
document.getElementById('chatNome').textContent = conv.nomeContato || conv.numero || 'Desconhecido';
document.getElementById('chatStatus').textContent = conv.status === 'E' ? '🟡 Em espera' : conv.status === 'A' ? '🟢 Em atendimento' : '⚫ Finalizado';
if (conv.status === 'F') {
document.querySelector('.btn-finalizar').textContent = '✅ Finalizado';
document.querySelector('.btn-finalizar').disabled = true;
document.querySelector('.btn-finalizar').style.opacity = '0.6';
} else {
document.querySelector('.btn-finalizar').textContent = '✅ Finalizar';
document.querySelector('.btn-finalizar').disabled = false;
document.querySelector('.btn-finalizar').style.opacity = '1';
}
window._convAtual = conv;
aplicarPainelResolucao(conv);
// Info cliente
var infoContainer = document.getElementById('clienteInfoContainer');
var fotoContainer = document.getElementById('clienteFoto');
if (conv.cliente) {
var c = conv.cliente;
if (c.foto) {
var srcFoto = c.foto;
if (srcFoto.indexOf('data:') !== 0 && srcFoto.length > 50) {
srcFoto = 'data:image/jpeg;base64,' + srcFoto;
}
fotoContainer.innerHTML = '<img src="' + esc(srcFoto) + '">';
} else {
fotoContainer.innerHTML = c.nome ? esc(c.nome.charAt(0)) : '?';
}
var inadClass = c.inadimplente ? 'inadimplente' : 'adimplente';
var inadLabel = c.inadimplente ? '🔴 Inadimplente' : '🟢 Adimplente';
infoContainer.innerHTML =
'<div class="value" style="font-size:16px;font-weight:600;margin-bottom:8px">' + esc(c.nome) + '</div>' +
(c.cidade || c.bairro ? '<div class="field"><div class="label">Cidade/Bairro</div><div class="value">' + esc(c.cidade || '') + (c.bairro ? ' - ' + esc(c.bairro) : '') + '</div></div>' : '') +
(c.matricula ? '<div class="field"><div class="label">Matrícula</div><div class="value">' + esc(c.matricula) + '</div></div>' : '') +
(c.plano ? '<div class="field"><div class="label">Plano</div><div class="value">' + esc(c.plano) + '</div></div>' : '') +
'<div class="field"><div class="label">Contato</div><div class="value">' + esc(c.celular || c.telefone || conv.numero || '-') + '</div></div>' +
'<div class="field"><div class="label ' + inadClass + '">' + inadLabel + '</div></div>';
} else if (conv.dependente) {
var dep = conv.dependente;
fotoContainer.textContent = (dep.nome || '?').charAt(0).toUpperCase();
var inads = dep.titularInadimplente ? '<span style="color:#ef4444;font-weight:600">🔴 Inadimplente</span>' : '<span style="color:#059669;font-weight:600">🟢 Adimplente</span>';
infoContainer.innerHTML =
'<div style="font-size:16px;font-weight:600;color:#111827">' + esc(dep.nome) + '</div>' +
'<div style="font-size:12px;color:#667eea;font-weight:500;margin:2px 0 8px">📋 Dependente</div>' +
(dep.titularCidade || dep.titularBairro ? '<div class="field"><div class="label">Cidade/Bairro</div><div class="value">' + esc(dep.titularCidade || '') + (dep.titularBairro ? ' - ' + esc(dep.titularBairro) : '') + '</div></div>' : '') +
(dep.titularMatricula ? '<div class="field"><div class="label">Matrícula (Titular)</div><div class="value">' + esc(dep.titularMatricula) + '</div></div>' : '') +
(dep.titularPlano ? '<div class="field"><div class="label">Plano</div><div class="value">' + esc(dep.titularPlano) + '</div></div>' : '') +
'<div class="field"><div class="label">Contato</div><div class="value">' + esc(dep.telefone || conv.numero || '-') + '</div></div>' +
'<div class="field"><div class="label">' + inads + '</div></div>';
} else {
// Sem cliente vinculado: mostra o número e nome do contato WhatsApp
var numeroExibir = (conv.numero || '').replace(/^(\d{2})(\d{2})(\d{5})(\d{4})$/, '($1) $2 $3-$4').replace(/^(\d{2})(\d{5})(\d{4})$/, '($1) $2-$3');
if (!numeroExibir) numeroExibir = conv.numero || '-';
var nomeContato = (conv.nomeContato || '').trim();
// Se o nome do contato for igual ao número (formato puro), mostra só o número formatado
var nomeLimpo = nomeContato.replace(/\D/g, '');
var numLimpo = (conv.numero || '').replace(/\D/g, '');
if (nomeLimpo === numLimpo) nomeContato = '';
fotoContainer.innerHTML = '<span style="font-weight:600">?</span>';
infoContainer.innerHTML =
'<div style="font-size:13px;font-weight:600;color:#d97706;margin-bottom:6px">⚠️ Número não identificado</div>' +
'<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(nomeContato || numeroExibir) + '</div>' +
(nomeContato ? '<div class="field"><div class="label">WhatsApp</div><div class="value">' + esc(numeroExibir) + '</div></div>' : '') +
'<div class="field"><div class="label">Status</div><div class="value" style="color:var(--text-muted)">Sem cadastro no sistema</div></div>';
}
// Botão "Enviar boleto" (aparece se a empresa habilitar e houver titular)
var boletoClienteId = conv.cliente ? conv.cliente.id : (conv.dependente ? conv.dependente.titularId : null);
if (cfgResolucao.enviarBoleto === 'S' && boletoClienteId && conv.status !== 'F') {
infoContainer.innerHTML +=
'<button id="btnEnviarBoleto" onclick="abrirModalBoleto(' + boletoClienteId + ')" style="margin-top:10px;width:100%;padding:8px 10px;border:1px solid #c7d2fe;border-radius:6px;background:#eef2ff;cursor:pointer;font-size:13px;color:#4338ca;font-weight:600">💳 Enviar boleto</button>';
}
// Verifica convalescentes pendentes (situação E = entrega)
var clienteId = conv.cliente ? conv.cliente.id : (conv.dependente ? conv.dependente.titularId : null);
if (clienteId) {
(async function() {
try {
var r = await fetch('/api/' + alias + '/clients/' + clienteId + '/convalescentes', {
headers: { 'Authorization': 'Bearer ' + token }
});
var d = await r.json();
if (d.success && d.data && d.data.length > 0) {
// Apenas situação 'E' = Entrega pendente
var pendentes = d.data.filter(function(c) {
return (c.situacao || '').toUpperCase() === 'E';
});
if (pendentes.length > 0) {
infoContainer.innerHTML += '<div class="field" style="margin-top:8px;padding-top:8px;border-top:1px solid #e5e7eb"><div class="label" style="color:#d97706;font-weight:600">🛏️ <strong>' + pendentes.length + ' convalescente(s) para entregar</strong></div></div>';
}
}
} catch(e) { /* ignora */ }
})();
}
// Carrega dropdowns de atendente, equipe e etiquetas
carregarAtendentes(conv.usuarioId);
carregarEquipes(conv.equipeId);
carregarEtiquetasDisponiveis(conv.labels);
// Etiquetas ativas
renderEtiquetasAtivas(conv.labels || []);
window.conversaAtual = conv;
}
// ===== CARREGAR ATENDENTES (FILTRADO POR EQUIPE) =====
var _todosUsuarios = null;
function carregarAtendentes(selectedId) {
var sel = document.getElementById('selectAtendente');
if (_todosUsuarios) {
filtrarAtendentesPorEquipe(sel, _todosUsuarios, selectedId);
return;
}
fetch('/api/' + alias + '/company/users?empresaId=' + empresaId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
_todosUsuarios = d.data;
filtrarAtendentesPorEquipe(sel, d.data, selectedId);
}
}).catch(function() {});
}
function getMembrosDaEquipe(equipeId) {
if (!equipeId || !_equipesCache) return null;
var equipe = _equipesCache.find(function(e) { return e.id === equipeId; });
return equipe ? (equipe.membros || []).map(function(m) { return m.id; }) : null;
}
function filtrarAtendentesPorEquipe(sel, users, selectedId) {
var equipeId = parseInt(document.getElementById('selectEquipe').value);
var membrosIds = getMembrosDaEquipe(equipeId);
var filtrados = users;
if (membrosIds) {
filtrados = users.filter(function(u) { return membrosIds.includes(u.id); });
}
sel.innerHTML = '<option value="">' + (membrosIds ? 'Nenhum da equipe' : 'Nenhum') + '</option>';
filtrados.forEach(function(u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.nome + ' (' + u.login + ')';
if (u.id === selectedId) opt.selected = true;
sel.appendChild(opt);
});
if (membrosIds && filtrados.length === 0) {
sel.innerHTML = '<option value="">Nenhum usuário nesta equipe</option>';
}
}
window.mudarAtendente = function(sel) {
var val = sel.value;
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ usuarioId: val ? parseInt(val) : null })
}).catch(function() {});
};
// ===== CARREGAR EQUIPES =====
var _equipesCache = null;
function carregarEquipes(selectedId) {
var sel = document.getElementById('selectEquipe');
if (_equipesCache) {
preencherEquipes(sel, _equipesCache, selectedId);
return;
}
fetch('/api/' + alias + '/teams?empresaId=' + empresaId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
_equipesCache = d.data;
preencherEquipes(sel, d.data, selectedId);
}
}).catch(function() {});
}
function preencherEquipes(sel, teams, selectedId) {
sel.innerHTML = '<option value="">Sem equipe</option>';
teams.forEach(function(t) {
var opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.nome;
if (t.id === selectedId) opt.selected = true;
sel.appendChild(opt);
});
}
window.mudarEquipe = function(sel) {
var val = sel.value;
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/assign-team', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ equipeId: val ? parseInt(val) : null })
}).catch(function() {});
// Recarrega atendentes filtrando pela nova equipe
if (_todosUsuarios) {
var selAtend = document.getElementById('selectAtendente');
var currentAttendant = window._convAtual ? window._convAtual.usuarioId : null;
filtrarAtendentesPorEquipe(selAtend, _todosUsuarios, currentAttendant);
}
};
// ===== ETIQUETAS =====
var _etiquetasCache = null;
function carregarEtiquetasDisponiveis(ativas) {
var sel = document.getElementById('selectEtiqueta');
if (_etiquetasCache) {
preencherEtiquetas(sel, _etiquetasCache, ativas);
return;
}
fetch('/api/' + alias + '/labels?empresaId=' + empresaId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
_etiquetasCache = d.data;
preencherEtiquetas(sel, d.data, ativas);
}
}).catch(function() {});
}
function preencherEtiquetas(sel, labels, ativas) {
var ativasIds = (ativas || []).map(function(l) { return l.id; });
sel.innerHTML = '<option value="">Adicionar etiqueta...</option>';
labels.forEach(function(l) {
if (!ativasIds.includes(l.id)) {
var opt = document.createElement('option');
opt.value = l.id;
opt.textContent = l.nome;
sel.appendChild(opt);
}
});
}
window.adicionarEtiqueta = function(sel) {
var val = sel.value;
if (!val) return;
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/labels', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ etiquetaId: parseInt(val), ativar: true })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
// Recarrega a conversa para mostrar a nova etiqueta
abrirConversa(conversaAtiva);
}
}).catch(function() {});
sel.value = '';
};
function renderEtiquetasAtivas(labels) {
var div = document.getElementById('etiquetasAtivas');
if (!labels || labels.length === 0) {
div.innerHTML = '<span style="font-size:12px;color:#9ca3af">Nenhuma</span>';
return;
}
div.innerHTML = labels.map(function(l) {
return '<span class="label-tag" style="background:' + (l.cor || '#667eea') + ';cursor:pointer;position:relative" onclick="removerEtiqueta(' + l.id + ')" title="Clique para remover">' + (l.nome || '') + ' ✕</span>';
}).join(' ');
}
window.removerEtiqueta = function(etiquetaId) {
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/labels', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ etiquetaId: etiquetaId, ativar: false })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) abrirConversa(conversaAtiva);
}).catch(function() {});
};
// ===== MENSAGENS =====
async function carregarMensagens() {
if (!conversaAtiva || carregandoMsgs) return;
carregandoMsgs = true;
try {
var res = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/messages?page=1&limit=100', {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (data.success) {
// Notificação desktop: detecta novas mensagens de cliente
var novasDoCliente = data.data.filter(function(m) {
return m.remetente === 'C' && m.id > ultimaMsgIdGlobal && m.privada !== 'S';
});
if (novasDoCliente.length > 0 && Chatc2Notify) {
var ultima = novasDoCliente[novasDoCliente.length - 1];
Chatc2Notify.show('Nova mensagem', (ultima.mensagem || 'Mídia recebida').substring(0, 100));
}
// Atualiza ID máximo global
data.data.forEach(function(m) { if (m.id > ultimaMsgIdGlobal) ultimaMsgIdGlobal = m.id; });
renderMensagens(data.data);
}
} catch(e) { if (Chatc2Toast) Chatc2Toast.error('Erro ao carregar mensagens'); }
carregandoMsgs = false;
}
function renderMensagens(msgs) {
mensagens = msgs;
var area = document.getElementById('messagesArea');
if (msgs.length === 0) {
area.innerHTML = '<div class="empty-state"><div class="icon">💬</div><p>Nenhuma mensagem ainda</p></div>';
return;
}
area.innerHTML = msgs.map(function(m) {
var classe = m.remetente === 'U' ? 'usuario' : m.remetente === 'S' ? 'sistema' : 'cliente';
if (m.privada === 'S') classe += ' privada';
var conteudo = esc(m.mensagem || '');
var mediaTok = '&token=' + encodeURIComponent(token);
// IMAGEM
if ((m.tipo === 'image' || m.tipo === 'imageMessage') && m.midiaId) {
conteudo = '<img src="/api/' + alias + '/media/' + m.midiaId + '?_=' + Date.now() + mediaTok + '" onclick="window.open(this.src)" style="max-width:250px;border-radius:8px;cursor:pointer">';
}
// AUDIO
else if ((m.tipo === 'audio' || m.tipo === 'ptt' || m.tipo === 'audioMessage') && m.midiaId) {
conteudo = '<audio controls style="max-width:250px;height:44px" src="/api/' + alias + '/media/' + m.midiaId + '?_=' + Date.now() + mediaTok + '" preload="auto" onerror="this.nextElementSibling.style.display=\'block\';this.controls=false;this.style.opacity=0.5"></audio><span style="display:none;font-size:11px;color:#ef4444;margin-top:2px">⚠️ Áudio indisponível</span>';
// Transcricao
if (m.transcricao && m.transcricao.length > 0) {
conteudo += '<div style="font-size:11px;color:#6b7280;margin-top:4px;padding:4px 8px;background:#f9fafb;border-radius:4px;max-width:250px">📝 ' + esc(m.transcricao) + '</div>';
}
} else if (m.tipo === 'audio' && !m.midiaId && m.mensagem) {
var audioMsg = m.mensagem;
if (audioMsg.startsWith('data:') || audioMsg.length > 100) {
conteudo = '<audio controls style="max-width:250px;height:44px" src="' + esc(audioMsg) + '" preload="auto"></audio>';
}
}
// VIDEO
else if ((m.tipo === 'video' || m.tipo === 'videoMessage') && m.midiaId) {
conteudo = '<video controls style="max-width:250px;border-radius:8px" src="/api/' + alias + '/media/' + m.midiaId + '?_=' + Date.now() + mediaTok + '"></video>';
}
// DOCUMENTO
else if ((m.tipo === 'document' || m.tipo === 'documentMessage') && m.midiaId) {
conteudo = '<a href="/api/' + alias + '/media/' + m.midiaId + '?token=' + encodeURIComponent(token) + '" target="_blank" style="display:inline-flex;align-items:center;gap:6px;padding:8px 12px;background:#f3f4f6;border-radius:8px;text-decoration:none;color:#374151;font-size:13px">📎 <span>' + esc(m.mensagem || 'Documento') + '</span></a>';
}
if (m.remetente === 'U' && m.privada === 'S') {
conteudo = '🔒 ' + conteudo;
}
var hora = m.dtEnvio ? new Date(m.dtEnvio).toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'}) : '';
return '<div class="msg ' + classe + '">' + conteudo + '<div class="hora">' + hora + '</div></div>';
}).join('');
area.scrollTop = area.scrollHeight;
}
// ===== ENVIAR MENSAGEM =====
window.enviarMensagem = async function() {
var input = document.getElementById('msgInput');
var texto = input.value.trim();
if (!texto || !conversaAtiva) return;
var privada = document.getElementById('privadaToggle').classList.contains('ativa') ? 'S' : 'N';
// Mostrar mensagem temporária "enviando"
var tempId = 'temp-' + Date.now();
var tempMsg = criarMsgTemp(texto, privada === 'S', tempId);
document.getElementById('messagesArea').appendChild(tempMsg);
scrollToBottom();
input.value = '';
input.style.height = 'auto';
try {
var res = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ mensagem: texto, tipo: 'text', privada: privada })
});
var data = await res.json();
if (data.success) {
// Remove temporária e recarrega mensagens
var el = document.getElementById(tempId);
if (el) el.remove();
carregarMensagens();
carregarConversas();
} else {
// Mostrar estado de erro na mensagem temporária
var el = document.getElementById(tempId);
if (el) {
el.classList.remove('enviando');
el.classList.add('erro');
el.querySelector('.msg-erro-acoes').style.display = 'flex';
}
}
} catch(e) {
var el = document.getElementById(tempId);
if (el) {
el.classList.remove('enviando');
el.classList.add('erro');
el.querySelector('.msg-erro-acoes').style.display = 'flex';
}
}
};
// ===== CRIAR MENSAGEM TEMPORÁRIA =====
window.criarMsgTemp = function(texto, privada, id) {
var div = document.createElement('div');
div.id = id;
div.className = 'msg usuario enviando';
if (privada) div.className += ' privada';
div.innerHTML = esc(texto).replace(/\n/g, '<br>') +
'<span class="hora">agora ⌛</span>' +
'<div class="msg-erro-acoes" style="display:none">⚠️ Falha ao enviar. <button class="btn-reenviar" onclick="reenviarMensagem(\'' + id + '\',\'' + esc(texto).replace(/'/g,"\\'") + '\',' + (privada ? 'true' : 'false') + ')">Reenviar</button></div>';
return div;
};
window.reenviarMensagem = function(oldId, texto, privada) {
var el = document.getElementById(oldId);
if (el) el.remove();
var input = document.getElementById('msgInput');
input.value = texto;
if (privada && !document.getElementById('privadaToggle').classList.contains('ativa')) {
togglePrivada();
} else if (!privada && document.getElementById('privadaToggle').classList.contains('ativa')) {
togglePrivada();
}
enviarMensagem();
};
window.scrollToBottom = function() {
var area = document.getElementById('messagesArea');
if (area) area.scrollTop = area.scrollHeight;
};
// ===== HABILITAR/DESABILITAR BOTÃO ENVIAR =====
window.toggleBtnEnviar = function() {
var input = document.getElementById('msgInput');
var btn = document.getElementById('btnEnviar');
if (input && btn) {
btn.disabled = !input.value.trim();
}
};
// ===== ENVIAR MÍDIA =====
window.enviarMidia = async function(input) {
if (!input.files || !input.files[0] || !conversaAtiva) return;
var file = input.files[0];
var reader = new FileReader();
reader.onload = async function(e) {
var base64 = e.target.result.split(',')[1];
var tipo = file.type.startsWith('image') ? 'image' : file.type.startsWith('audio') ? 'audio' : 'document';
try {
await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ mensagem: '', tipo: tipo, midiaBase64: base64, nomeArquivo: file.name })
});
carregarMensagens();
} catch(e) {}
};
reader.readAsDataURL(file);
input.value = '';
};
// ===== GRAVACAO DE AUDIO =====
var mediaRecorder = null;
var audioChunks = [];
var gravando = false;
var audioGravadoBlob = null;
var audioGravadoB64 = null;
var audioGravadoMime = 'audio/webm';
window.toggleGravacao = async function() {
var btn = document.getElementById("btnAudio");
if (gravando) {
if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
btn.classList.remove("recording");
btn.innerHTML = "🎤 Gravar";
gravando = false;
return;
}
// Se ja tem audio pendente, cancela
cancelarAudioGravado();
try {
var stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Tenta formato Ogg (compatível com WhatsApp), fallback para webm
var mimeType = "audio/webm";
if (MediaRecorder.isTypeSupported("audio/ogg;codecs=opus")) {
mimeType = "audio/ogg;codecs=opus";
} else if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) {
mimeType = "audio/webm;codecs=opus";
}
mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
audioChunks = [];
mediaRecorder.ondataavailable = function(e) {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = function() {
stream.getTracks().forEach(function(t) { t.stop(); });
if (audioChunks.length === 0 || !conversaAtiva) return;
audioGravadoBlob = new Blob(audioChunks, { type: mimeType });
audioGravadoMime = mimeType; // guarda o MIME real para usar no envio
// Mostra preview para ouvir antes de enviar
var url = URL.createObjectURL(audioGravadoBlob);
document.getElementById("audioPreviewPlayer").src = url;
document.getElementById("audioPreview").style.display = "flex";
document.getElementById("msgInput").style.display = "none";
document.querySelector(".btn-enviar").style.display = "none";
document.getElementById("btnAudio").style.display = "none";
document.getElementById("btnPlus").style.display = "none";
document.getElementById("privadaToggle").style.display = "none";
// Converte para base64
var fr = new FileReader();
fr.onload = function(ev) {
audioGravadoB64 = ev.target.result.split(",")[1];
};
fr.readAsDataURL(audioGravadoBlob);
};
mediaRecorder.start();
btn.classList.add("recording");
btn.innerHTML = "⏹️ Parar";
gravando = true;
} catch(err) {
alert("Erro ao acessar microfone: " + err.message);
}
};
window.enviarAudioGravado = async function() {
if (!audioGravadoB64 || !conversaAtiva) return;
try {
var ext = (audioGravadoMime || 'audio/webm').includes('ogg') ? 'ogg' : 'webm';
var nomeAudio = 'audio.' + ext;
var resp = await fetch("/api/" + alias + "/conversations/" + conversaAtiva + "/messages", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
body: JSON.stringify({ mensagem: "", tipo: "audio", midiaBase64: audioGravadoB64, nomeArquivo: nomeAudio })
});
var data = await resp.json();
if (!data.success) {
alert("Erro ao enviar áudio: " + (data.error || "desconhecido"));
return;
}
cancelarAudioGravado();
carregarMensagens();
} catch(e) {
alert("Erro ao enviar áudio: " + e.message);
}
};
window.cancelarAudioGravado = function() {
audioGravadoBlob = null;
audioGravadoB64 = null;
document.getElementById("audioPreview").style.display = "none";
document.getElementById("audioPreviewPlayer").src = "";
document.getElementById("msgInput").style.display = "";
document.querySelector(".btn-enviar").style.display = "";
document.getElementById("btnAudio").style.display = "";
document.getElementById("btnPlus").style.display = "";
document.getElementById("privadaToggle").style.display = "";
};
// ===== DRAG AND DROP =====
var dropArea = document.body;
dropArea.addEventListener('dragover', function(e) { e.preventDefault(); document.getElementById('dragOverlay').classList.add('show'); });
dropArea.addEventListener('dragleave', function(e) { document.getElementById('dragOverlay').classList.remove('show'); });
dropArea.addEventListener('drop', function(e) {
e.preventDefault();
document.getElementById('dragOverlay').classList.remove('show');
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
document.getElementById('fileInput').files = e.dataTransfer.files;
enviarMidia(document.getElementById('fileInput'));
}
});
// ===== FINALIZAR =====
window.toggleAgendamento = function() {
var show = document.getElementById('chkAgendar').checked;
document.getElementById('agendamentoCampos').style.display = show ? 'block' : 'none';
if (show) {
if (!document.getElementById('agData').value) {
document.getElementById('agData').value = new Date().toISOString().split('T')[0];
}
// Preenche anotação com o texto da resolução (se existir)
var txtResolucao = document.getElementById('campoResolucao');
var nota = document.getElementById('agNota');
if (txtResolucao && nota && !nota.value) {
nota.value = (txtResolucao.value || '').trim();
}
}
};
window.finalizarConversa = async function() {
if (!conversaAtiva) return;
// Coleta motivo/resolução (se visíveis) e valida obrigatoriedade
var motivoId = null, resolucao = '';
if (cfgResolucao.motivoVisualizar === 'S') {
var sel = document.getElementById('selectMotivo');
motivoId = sel && sel.value ? parseInt(sel.value, 10) : null;
if (cfgResolucao.motivoObrigatorio === 'S' && !motivoId) {
alert('Selecione o motivo do atendimento para finalizar.'); return;
}
}
if (cfgResolucao.resolucaoVisualizar === 'S') {
var txt = document.getElementById('campoResolucao');
resolucao = txt ? (txt.value || '').trim() : '';
if (cfgResolucao.resolucaoObrigatorio === 'S' && !resolucao) {
alert('Preencha a resolução do atendimento para finalizar.'); return;
}
}
// Confirmação já foi feita pelo modal — segue direto
try {
var res = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/finalize', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ motivoId: motivoId, resolucao: resolucao, skipCsat: document.getElementById('chkAgendar') && document.getElementById('chkAgendar').checked })
});
var data = await res.json();
if (data.success) {
// Agenda retorno se checkbox marcado
if (document.getElementById('chkAgendar') && document.getElementById('chkAgendar').checked) {
var agData = document.getElementById('agData').value;
var agHora = document.getElementById('agHora').value || null;
var agNota = document.getElementById('agNota').value.trim() || null;
if (agData) {
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/schedule', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ data: agData, hora: agHora, nota: agNota })
}).catch(function(){});
}
}
document.querySelector('.btn-finalizar').textContent = '✅ Finalizado';
document.querySelector('.btn-finalizar').disabled = true;
document.querySelector('.btn-finalizar').style.opacity = '0.6';
carregarConversas();
// Fecha a conversa da tela (volta ao estado vazio)
conversaAtiva = null;
document.getElementById('chatHeader').style.display = 'none';
document.getElementById('chatInput').style.display = 'none';
document.getElementById('messagesArea').innerHTML = '<div class="empty-state"><div class="icon">💬</div><p>Selecione uma conversa</p></div>';
document.getElementById('clienteInfoContainer').innerHTML = '';
document.getElementById('clienteFoto').innerHTML = '?';
} else {
alert(data.error || 'Não foi possível finalizar a conversa.');
}
} catch(e) {}
};
// ===== PRIVADA TOGGLE =====
// ===== NAVEGAÇÃO MOBILE (painel único) =====
window.voltarLista = function() {
document.body.classList.remove('chat-aberto', 'info-aberto');
};
window.toggleInfo = function() {
document.body.classList.toggle('info-aberto');
};
// ===== ENVIAR BOLETO =====
var boletoClienteAtual = null;
window.abrirModalBoleto = async function(clienteId) {
if (!conversaAtiva || !clienteId) return;
boletoClienteAtual = clienteId;
var modal = document.getElementById('modalBoleto');
var lista = document.getElementById('boletoLista');
modal.classList.add('show');
lista.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:20px">⏳ Carregando títulos...</div>';
try {
var r = await (await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/boletos?clienteId=' + clienteId, {
headers: { 'Authorization': 'Bearer ' + token }
})).json();
if (!r.success) { lista.innerHTML = '<div style="color:#ef4444;padding:16px">' + esc(r.error || 'Erro ao carregar títulos.') + '</div>'; return; }
var carnes = (r.data || []).filter(function(c){ return c.temLinhaDigitavel || c.temPix; });
if (carnes.length === 0) {
lista.innerHTML = '<div style="color:#9ca3af;padding:16px;text-align:center">Nenhum título em aberto com boleto/PIX disponível.</div>';
return;
}
lista.innerHTML = carnes.map(function(c){
var valor = (c.valor != null) ? ('R$ ' + Number(c.valor).toFixed(2).replace('.', ',')) : '-';
var venc = c.vencimento ? c.vencimento.split('-').reverse().join('/') : '-';
var parc = c.parcela ? (' • Parcela ' + c.parcela + (c.totalParcelas ? '/' + c.totalParcelas : '')) : '';
var tags = (c.temPix ? '<span style="font-size:10px;background:#dcfce7;color:#166534;padding:1px 6px;border-radius:8px;margin-left:6px">PIX</span>' : '') +
(c.temLinhaDigitavel ? '<span style="font-size:10px;background:#dbeafe;color:#1e40af;padding:1px 6px;border-radius:8px;margin-left:6px">Boleto</span>' : '');
return '<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;padding:10px;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:8px">' +
'<div><div style="font-weight:600;color:#111827">' + valor + tags + '</div>' +
'<div style="font-size:12px;color:#6b7280">Vencimento: ' + venc + parc + '</div></div>' +
'<button onclick="enviarBoleto(' + c.id + ', this)" style="padding:6px 12px;background:#4f46e5;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Enviar</button>' +
'</div>';
}).join('');
} catch(e) {
lista.innerHTML = '<div style="color:#ef4444;padding:16px">Erro de conexão.</div>';
}
};
window.fecharModalBoleto = function() {
document.getElementById('modalBoleto').classList.remove('show');
};
window.enviarBoleto = async function(carneId, btn) {
if (!conversaAtiva || !boletoClienteAtual) return;
if (btn) { btn.disabled = true; btn.textContent = 'Enviando...'; }
try {
var r = await (await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/send-boleto', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ clienteId: boletoClienteAtual, carneId: carneId })
})).json();
if (r.success) {
fecharModalBoleto();
carregarMensagens();
} else if (btn) {
btn.disabled = false; btn.textContent = 'Enviar';
alert(r.error || 'Não foi possível enviar o boleto.');
}
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = 'Enviar'; }
}
};
window.togglePrivada = function() {
var btn = document.getElementById('privadaToggle');
btn.classList.toggle('ativa');
var isPriv = btn.classList.contains('ativa');
document.getElementById('privadaLabel').textContent = isPriv ? 'Privada' : 'Normal';
btn.querySelector('.priv-icon').textContent = isPriv ? '🔓' : '🔒';
};
// ===== FINALIZAR COM CONFIRMAÇÃO =====
window.confirmarFinalizar = function() {
document.getElementById('modalConfirmarFinalizar').classList.add('show');
};
// ===== MENU DE AÇÕES =====
window.toggleActionMenu = function() {
document.getElementById('actionMenu').classList.toggle('show');
};
// ===== ACCORDION DO PAINEL DIREITO =====
window.toggleAccordion = function(btn, bodyId) {
btn.classList.toggle('open');
document.getElementById(bodyId).classList.toggle('open');
};
// ===== ATALHOS DE TECLADO =====
document.addEventListener('keydown', function(e) {
// Ctrl+J / Ctrl+K: navegar entre conversas
if ((e.ctrlKey || e.metaKey) && (e.key === 'j' || e.key === 'k')) {
e.preventDefault();
var items = document.querySelectorAll('.conv-item');
if (!items.length) return;
var idx = -1;
for (var i = 0; i < items.length; i++) {
if (items[i].classList.contains('active')) { idx = i; break; }
}
if (e.key === 'j') idx = Math.min(idx + 1, items.length - 1);
else idx = Math.max(idx - 1, 0);
if (idx >= 0 && items[idx]) items[idx].click();
}
// Ctrl+F: focar busca de conversas
if ((e.ctrlKey || e.metaKey) && e.key === 'f' && document.activeElement !== document.getElementById('msgInput')) {
e.preventDefault();
var s = document.getElementById('searchConv');
if (s) { s.focus(); s.select(); }
}
// Esc: fechar modais e menus
if (e.key === 'Escape') {
document.getElementById('actionMenu').classList.remove('show');
document.querySelectorAll('.modal-overlay.show').forEach(function(m) {
m.classList.remove('show');
});
}
});
// Fechar menu de ações ao clicar fora
document.addEventListener('click', function(e) {
var menu = document.getElementById('actionMenu');
var btn = document.getElementById('btnPlus');
if (menu && btn && !menu.contains(e.target) && !btn.contains(e.target)) {
menu.classList.remove('show');
}
});
// ===== ASSOCIAR CLIENTE =====
window.mostrarModalAssociar = function() {
document.getElementById('modalAssociar').classList.add('show');
document.getElementById('associarBusca').value = '';
document.getElementById('associarResultados').innerHTML = '';
};
window.fecharModal = function(id) { document.getElementById(id).classList.remove('show'); };
window.mudarTipoAssoc = function() {
var tipo = document.querySelector('input[name="tipoAssoc"]:checked').value;
document.getElementById('assocLabel').textContent = tipo === 'cliente' ? 'Digite o nome, CPF ou matrícula do cliente:' : 'Digite o nome do dependente ou matrícula do titular:';
document.getElementById('associarBusca').placeholder = tipo === 'cliente' ? 'Buscar cliente...' : 'Buscar dependente...';
document.getElementById('associarResultados').innerHTML = '';
};
window.buscarClienteAssociar = async function(event) {
if (event.key !== 'Enter') return;
var q = document.getElementById('associarBusca').value.trim();
var tipo = document.querySelector('input[name="tipoAssoc"]:checked').value;
if (!q) return;
var div = document.getElementById('associarResultados');
div.innerHTML = '<div style="padding:12px;color:#9ca3af;font-size:13px">Buscando...</div>';
try {
if (tipo === 'dependente') {
var res = await fetch('/api/' + alias + '/dependents/search?q=' + encodeURIComponent(q) + '&empresaId=' + empresaId, {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<div style="padding:12px;color:#9ca3af;font-size:13px">Nenhum dependente encontrado</div>'; return;
}
div.innerHTML = data.data.map(function(d) {
var info = d.titularMatricula ? 'Matrícula: ' + d.titularMatricula : '';
info += d.titularNome ? (info ? ' - ' : '') + d.titularNome : '';
return '<div style="padding:8px 12px;border-bottom:1px solid #f3f4f6;cursor:pointer" onclick="associarDependente(' + d.id + ',\'' + d.nome.replace(/'/g,"\\'") + '\',\'' + (d.telefone||'').replace(/'/g,"\\'") + '\')">' +
'<strong>' + d.nome + '</strong><br><span style="font-size:11px;color:#6b7280">' + info + '</span></div>';
}).join('');
} else {
var res = await fetch('/api/' + alias + '/clients/search?q=' + encodeURIComponent(q) + '&empresaId=' + empresaId, {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success) { div.innerHTML = '<div style="padding:12px;color:#9ca3af;font-size:13px">Nenhum resultado</div>'; return; }
div.innerHTML = data.data.map(function(c) {
return '<div style="padding:8px 12px;border-bottom:1px solid #f3f4f6;cursor:pointer" onclick="associarCliente(' + c.id + ',\'' + c.nome.replace(/'/g,"\\'") + '\')">' +
'<strong>' + c.nome + '</strong> - ' + (c.cpf || c.matricula || '') + '</div>';
}).join('') || '<div style="padding:12px;color:#9ca3af;font-size:13px">Nenhum resultado</div>';
}
} catch(e) { div.innerHTML = '<div style="padding:12px;color:#ef4444;font-size:13px">Erro: ' + e.message + '</div>'; }
};
window.associarCliente = async function(clienteId, nome) {
try {
var res = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/link-client', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ clienteId: clienteId })
});
var data = await res.json();
if (data.success) {
alert('Cliente ' + nome + ' associado com sucesso!');
fecharModal('modalAssociar');
// Recarrega a conversa para mostrar os dados atualizados
conversaAtiva = parseInt(conversaAtiva);
window.abrirConversa(conversaAtiva);
} else {
alert('Erro: ' + (data.error || 'desconhecido'));
}
} catch(e) {
alert('Erro de conexão: ' + e.message);
}
};
// ===== ASSOCIAR DEPENDENTE =====
window.associarDependente = async function(depId, nome, telefone) {
if (!confirm('Associar dependente "' + nome + '" a esta conversa?')) return;
try {
// Busca o cliente titular pelo ID do dependente
var res = await fetch('/api/' + alias + '/dependents/search?q=' + encodeURIComponent(nome.split(' ')[0]) + '&empresaId=' + empresaId, {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success || !data.data || data.data.length === 0) {
alert('Erro ao buscar dados do dependente'); return;
}
var dep = data.data.find(function(d) { return d.id === depId; }) || data.data[0];
if (!dep.clienteId) { alert('Dependente sem cliente titular vinculado'); return; }
var res2 = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/link-client', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ clienteId: dep.clienteId })
});
var data2 = await res2.json();
if (data2.success) {
alert('Dependente ' + nome + ' associado!');
fecharModal('modalAssociar');
conversaAtiva = parseInt(conversaAtiva);
window.abrirConversa(conversaAtiva);
} else {
alert('Erro: ' + (data2.error || 'desconhecido'));
}
} catch(e) { alert('Erro: ' + e.message); }
};
// ===== IR PÁGINA CLIENTE =====
window.irPaginaCliente = function() {
var conv = window.conversaAtual;
if (!conv) return;
// Se for dependente, abre página do titular
if (conv.dependente && conv.dependente.titularId) {
window.open('/app/' + alias + '/company/' + empresaId + '/client/' + conv.dependente.titularId, '_blank');
return;
}
// Se for cliente, abre página do cliente
if (conv.cliente) {
window.open('/app/' + alias + '/company/' + empresaId + '/client/' + conv.cliente.id, '_blank');
}
};
// ===== SWITCH TAB =====
var filtroAtual = 'mine';
window.mudarFiltro = function(filtro, el) {
document.querySelectorAll('.nav-tabs a').forEach(function(a) { a.classList.remove('active'); });
el.classList.add('active');
filtroAtual = filtro;
// Mostra/esconde busca e checkbox de finalizadas (não fazem sentido para agendadas)
var searchBox = document.querySelector('.search-box');
if (filtro === 'scheduled') {
if (searchBox) searchBox.style.display = 'none';
} else {
if (searchBox) searchBox.style.display = '';
}
carregarConversas();
};
// ===== AGENDADAS =====
async function carregarAgendadas() {
var dataSel = document.getElementById('agendadasData');
var data = dataSel ? dataSel.value : new Date().toISOString().split('T')[0];
var list = document.getElementById('conversationsList');
list.innerHTML = '<div style="padding:24px;text-align:center;color:rgba(255,255,255,0.4);font-size:13px">Carregando agendamentos...</div>';
try {
var res = await fetch('/api/' + alias + '/schedules?empresaId=' + empresaId + '&data=' + data, {
headers: { 'Authorization': 'Bearer ' + token }
});
var d = await res.json();
if (!d.success) return;
var dataFmt = data.split('-').reverse().join('/');
var html = '<div style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:8px">' +
'<span style="font-size:13px;color:rgba(255,255,255,0.7)">📅</span>' +
'<input type="date" id="agendadasData" value="' + data + '" onchange="carregarAgendadas()" style="flex:1;padding:6px 8px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;font-size:12px;background:rgba(255,255,255,0.06);color:#fff;outline:none">' +
'<span style="font-size:12px;color:rgba(255,255,255,0.5)">' + (d.count || 0) + ' agend.</span></div>';
if (!d.data || d.data.length === 0) {
html += '<div style="padding:24px;text-align:center;color:rgba(255,255,255,0.35);font-size:13px">Nenhum agendamento para ' + dataFmt + '</div>';
} else {
d.data.forEach(function(a) {
var nome = a.nomeContato || a.numero || '-';
var cor = avatarColor(nome);
var ini = avatarInitials(nome);
var hora = a.hora ? a.hora.substring(0,5) : '';
html += '<div class="conv-item" style="cursor:pointer" onclick="reabrirAgendamento(' + a.id + ',' + a.conversaId + ')">' +
'<div class="conv-avatar" style="background:' + cor + '">' + ini + '</div>' +
'<div class="conv-body">' +
'<div class="conv-nome">' + nome + (hora ? ' <span style="font-size:10px;opacity:0.6">🕐 ' + hora + '</span>' : '') + '</div>' +
'<div class="conv-ultima" style="font-size:11px">' + (a.nota || 'Sem anotação') + '</div>' +
'</div>' +
'</div>';
});
}
list.innerHTML = html;
} catch(e) {}
}
window.reabrirAgendamento = async function(agId, convId) {
try {
var res = await fetch('/api/' + alias + '/schedules/' + agId + '/reopen', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
});
var d = await res.json();
if (d.success) {
// Mostra modal com o texto sugerido
document.getElementById('retornoTextoSugerido').value = d.textoSugerido || '';
document.getElementById('retornoConversaId').value = d.conversaId;
document.getElementById('modalRetorno').classList.add('show');
}
} catch(e) {}
};
window.confirmarRetorno = function() {
var convId = document.getElementById('retornoConversaId').value;
var texto = document.getElementById('retornoTextoSugerido').value.trim();
fecharModal('modalRetorno');
if (!convId) return;
// Abre a conversa e preenche o input
mudarFiltro('mine', document.querySelector('.nav-tabs a'));
carregarConversas();
setTimeout(function() {
abrirConversa(parseInt(convId));
// Aguarda o chat carregar e preenche o input
setTimeout(function() {
var input = document.getElementById('msgInput');
if (input && texto) {
input.value = texto;
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px';
input.focus();
}
}, 500);
}, 300);
};
// Mostra
// ===== SELETOR DE EMPRESA (usuário com acesso a mais de uma) =====
async function carregarEmpresas() {
try {
var r = await (await fetch('/api/' + alias + '/empresas', { headers: { 'Authorization': 'Bearer ' + token } })).json();
if (!r.success || !r.data || r.data.length <= 1) return; // só mostra se tiver mais de uma
var sel = document.getElementById('empresaSwitcher');
sel.innerHTML = r.data.map(function(e) {
var nome = e.nomeFantasia || e.nome || ('Empresa ' + e.id);
return '<option value="' + e.id + '"' + (String(e.id) === String(empresaId) ? ' selected' : '') + '>' + esc(nome) + '</option>';
}).join('');
document.getElementById('empresaSwitcherWrap').style.display = 'block';
} catch(e) {}
}
window.trocarEmpresa = function(novaEmpresaId) {
if (!novaEmpresaId || String(novaEmpresaId) === String(empresaId)) return;
// Recarrega a tela na empresa escolhida (estado limpo: conversas, config, etc.)
window.location.href = '/app/' + alias + '/company/' + novaEmpresaId + '/conversation/0';
};
// ===== INICIAR =====
carregarEmpresas();
carregarConversas();
carregarConfigResolucao().then(function() {
if (conversaId) abrirConversa(parseInt(conversaId));
});
// ===== NOVA CONVERSA (número não cadastrado) =====
window.mostrarModalNovaConversa = function() {
document.getElementById('modalNovaConversa').style.display = 'flex';
document.getElementById('novoNumero').value = '';
document.getElementById('novoNome').value = '';
document.getElementById('novoMensagem').value = '';
document.getElementById('novoNumero').focus();
}
window.fecharModalNovaConversa = function() {
document.getElementById('modalNovaConversa').style.display = 'none';
}
window.criarNovaConversa = async function() {
const numero = document.getElementById('novoNumero').value.replace(/\D/g, '');
const nome = document.getElementById('novoNome').value.trim() || numero;
const mensagem = document.getElementById('novoMensagem').value.trim() || '👋 Olá! Em que posso ajudar?';
if (!numero || numero.length < 10) {
if (Chatc2Toast) Chatc2Toast.warn('Informe um número de WhatsApp válido.');
return;
}
const btn = document.getElementById('btnCriarConversa');
btn.disabled = true;
btn.textContent = 'Criando...';
try {
const res = await fetch('/api/' + alias + '/conversations/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({
empresaId: empresaId,
numero: numero,
nomeContato: nome,
mensagem: mensagem,
instanciaId: null
})
});
const data = await res.json();
if (data.success) {
fecharModalNovaConversa();
await carregarConversas();
abrirConversa(data.data.id);
if (Chatc2Toast) Chatc2Toast.success('Conversa iniciada com ' + nome);
} else {
if (Chatc2Toast) Chatc2Toast.error(data.error || 'Erro ao criar conversa');
}
} catch(e) {
if (Chatc2Toast) Chatc2Toast.error('Erro de conexão ao criar conversa');
}
btn.disabled = false;
btn.textContent = 'Iniciar conversa';
}
// ===== LOGOUT =====
window.logout = function() {
['chatc2_token','chatc2_alias','chatc2_user'].forEach(function(k) { localStorage.removeItem(k); });
window.location.href = '/app/' + alias + '/login';
};
// Auto-refresh a cada 5 segundos
let ultimaMsgIdGlobal = 0;
setInterval(function() {
carregarConversas();
if (conversaAtiva) carregarMensagens();
}, 5000);
// Notificação desktop: pede permissão na primeira interação
document.addEventListener('click', function pedirPermissao() {
document.removeEventListener('click', pedirPermissao);
if (Chatc2Notify && Chatc2Notify.isSupported()) {
Chatc2Notify.requestPermission();
}
}, { once: true });
// Presença: marca o atendente como "online" para o dashboard
function pingPresenca() {
fetch('/api/' + alias + '/dashboard/ping', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
}).catch(function(){});
}
pingPresenca();
setInterval(pingPresenca, 45000);
// Auto-resize textarea
document.getElementById('msgInput').addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
})();
</script>
<style>
/* Dark mode para o modal Nova Conversa */
body.dark-mode #modalNovaConversa > div {
background: #1e1e2e !important;
box-shadow: 0 20px 60px rgba(0,0,0,0.5) !important;
}
body.dark-mode #modalNovaConversa h3 {
color: #e0e0e0 !important;
}
body.dark-mode #modalNovaConversa p {
color: #9ca3af !important;
}
body.dark-mode #modalNovaConversa label {
color: #d1d5db !important;
}
body.dark-mode #modalNovaConversa input,
body.dark-mode #modalNovaConversa textarea {
background: #2a2a3e !important;
border-color: #3f3f5a !important;
color: #e0e0e0 !important;
}
body.dark-mode #modalNovaConversa input:focus,
body.dark-mode #modalNovaConversa textarea:focus {
border-color: #818cf8 !important;
background: #2a2a3e !important;
box-shadow: 0 0 0 3px rgba(129,140,248,0.15) !important;
}
body.dark-mode #modalNovaConversa input::placeholder,
body.dark-mode #modalNovaConversa textarea::placeholder {
color: #6b7280 !important;
}
body.dark-mode #modalNovaConversa button {
color: #d1d5db !important;
}
/* Botão Cancelar no dark mode */
body.dark-mode #btnCancelarConversa {
background: #2a2a3e !important;
border-color: #3f3f5a !important;
color: #d1d5db !important;
}
</style>
<!-- Modal: Nova Conversa -->
<div id="modalNovaConversa" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center" onclick="if(event.target===this) fecharModalNovaConversa()">
<div style="background:#fff;border-radius:16px;padding:32px 28px 24px;width:100%;max-width:420px;box-shadow:0 20px 60px rgba(0,0,0,0.18)" onclick="event.stopPropagation()">
<h3 style="margin:0 0 6px;font-size:18px;font-weight:700;color:#111827">Nova conversa</h3>
<p style="margin:0 0 18px;font-size:13px;color:#6b7280">Inicie um atendimento para um número que ainda não está no sistema.</p>
<div class="form-group" style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#374151;margin-bottom:5px">Número (WhatsApp) *</label>
<input type="tel" id="novoNumero" placeholder="(11) 99999-9999" style="width:100%;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;outline:none;background:#f9fafb;transition:all .15s" onfocus="this.style.borderColor='#667eea';this.style.background='#fff'" onblur="this.style.borderColor='#e5e7eb';this.style.background='#f9fafb'">
</div>
<div class="form-group" style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#374151;margin-bottom:5px">Nome do contato</label>
<input type="text" id="novoNome" placeholder="Nome (opcional)" style="width:100%;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;outline:none;background:#f9fafb;transition:all .15s" onfocus="this.style.borderColor='#667eea';this.style.background='#fff'" onblur="this.style.borderColor='#e5e7eb';this.style.background='#f9fafb'">
</div>
<div class="form-group" style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#374151;margin-bottom:5px">Mensagem inicial</label>
<textarea id="novoMensagem" rows="3" placeholder="👋 Olá! Em que posso ajudar?" style="width:100%;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;outline:none;background:#f9fafb;resize:vertical;font-family:inherit;transition:all .15s" onfocus="this.style.borderColor='#667eea';this.style.background='#fff'" onblur="this.style.borderColor='#e5e7eb';this.style.background='#f9fafb'"></textarea>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px">
<button id="btnCancelarConversa" onclick="fecharModalNovaConversa()" style="padding:10px 20px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:#374151;font-size:14px;font-weight:600;cursor:pointer">Cancelar</button>
<button id="btnCriarConversa" onclick="criarNovaConversa()" style="padding:10px 20px;border:none;border-radius:8px;background:#667eea;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s" onmouseover="this.style.background='#5a67d8'" onmouseout="this.style.background='#667eea'">Iniciar conversa</button>
</div>
</div>
</div>
<script src="/js/dark-mode.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/toast.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
<!-- Bottom Tab Bar (mobile) -->
<script src="/js/tabbar.js"></script>
</body>
</html>