feat: comprehensive design system overhaul

Applied impeccable design critique, audit, harden, layout, distill, and polish across all pages.

DESIGN SYSTEM:
- Created PRODUCT.md (5 design principles, WCAG 2.1 AA target)
- Created DESIGN.md (34 color tokens, 6 radius scale, full component spec)
- Created .impeccable/design.json (sidecar with tonal ramps, shadows, motion)

ACCESSIBILITY:
- 33 aria-labels across 7 pages (was ~13)
- 63 <label> elements (was ~30)
- 6 <main> landmarks + role=navigation on all sidebars
- Esc to close modals on 4 pages
- Keyboard shortcuts (Ctrl+J/K, Ctrl+F) on chat

THEMING:
- 216 hardcoded colors replaced with var() references
- 449 !important removed from dark-mode.css
- Dark mode script unified (was duplicated 9x inline)
- All modals converted to .modal-overlay + .modal design system classes

CHAT:
- Error states for messages (.msg.erro, .msg.enviando)
- Connection status indicator
- Confirmation modal before finalizar
- Input bar reorganized (3 visible actions, + menu)
- Right panel accordions
- Send button disabled when empty
- Touch targets 44px on mobile

CLIENT DETAIL:
- Tabs/badges/sub-tabs use CSS classes instead of inline styles
- 'Iniciar Conversa' button now primary action
- alert() replaced with inline feedback
- Modal converted to design system classes

CLIENT LIST:
- Modal moved inside <body> (was HTML-invalid)
- alert() replaced with inline feedback
- Modal uses .show pattern

SETTINGS:
- 4 modals converted to design system classes
- 14 alert() calls replaced with inline feedback
- fecharModal() uses classList instead of style.display

ROUTES:
- border-radius normalized, hardcoded colors fixed

Score progression:
  Chat:        23 → 32/40
  Client List: 32 → 35/40
  Client Detail: 26 → 30/40
  Settings:     29 → 35/40
  Routes:       37 → 38/40
  Audit (project): 14/20 → 17/20
This commit is contained in:
2026-06-23 16:58:12 +00:00
parent cf720c37a1
commit 12d419de7c
113 changed files with 52423 additions and 951 deletions
+24 -20
View File
@@ -6,30 +6,28 @@
<title>Conversas - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background:#f3f4f6; display:flex; min-height:100vh; }
body { background:var(--surface-3); display:flex; min-height:100vh; }
.sidebar-nav .admin-only { display:none; }
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
.container { flex:1; padding:24px; overflow-y:auto; }
.filters { display:flex; gap:10px; margin-bottom:20px; flex-wrap:wrap; align-items:center; }
.filters select, .filters input { padding:9px 14px; border:2px solid #e5e7eb; border-radius:8px; font-size:13px; outline:none; background:#fff; color:#374151; transition:border-color .15s; }
.filters select:focus, .filters input:focus { border-color:#667eea; }
.filters button { padding:9px 16px; background:#667eea; color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; transition:background .15s; }
.filters button:hover { background:#5a67d8; }
.filters select, .filters input { padding:9px 14px; border:2px solid var(--border); border-radius:8px; font-size:13px; outline:none; background:#fff; color:var(--text-secondary); transition:border-color .15s; }
.filters select:focus, .filters input:focus { border-color:var(--primary); }
.filters button { padding:9px 16px; background:var(--primary); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; transition:background .15s; }
.filters button:hover { background:var(--primary-dark); }
.status-badge { display:inline-block; padding:3px 10px; border-radius:10px; font-size:11px; font-weight:600; }
.status-E { background:#fef3c7; color:#92400e; }
.status-A { background:#d1fae5; color:#065f46; }
.status-F { background:#f3f4f6; color:#6b7280; }
.atender-btn { padding:5px 12px; border:none; border-radius:6px; background:#667eea; color:#fff; font-size:12px; font-weight:600; cursor:pointer; text-decoration:none; transition:background .15s; display:inline-block; }
.atender-btn:hover { background:#5a67d8; }
.loading { text-align:center; padding:40px; color:#9ca3af; }
.status-E { background:var(--warning-bg); color:var(--warning-text); }
.status-A { background:var(--success-bg); color:var(--success-text); }
.status-F { background:var(--surface-3); color:var(--text-muted); }
.atender-btn { padding:5px 12px; border:none; border-radius:6px; background:var(--primary); color:#fff; font-size:12px; font-weight:600; cursor:pointer; text-decoration:none; transition:background .15s; display:inline-block; }
.atender-btn:hover { background:var(--primary-dark); }
.loading { text-align:center; padding:40px; color:var(--text-faint); }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand"><div class="logo">C2</div><div><h2>Chatc2</h2><span id="sidebarAlias">-</span></div></div>
<nav class="sidebar-nav">
<div class="nav-label">Principal</div>
@@ -41,16 +39,17 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" id="navRoutes" class="admin-only"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a></div>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a></div>
</aside>
<div class="main">
<main class="main" id="main-content">
<div class="topbar">
<span class="topbar-title">💬 Todas as Conversas</span>
<span id="totalInfo" style="font-size:13px;color:#6b7280"></span>
</div>
<div class="container">
<div class="filters">
<label for="filterStatus" class="sr-only">Status</label>
<select id="filterStatus" onchange="carregar()">
<option value="A,E">Em aberto</option>
<option value="E">Em espera</option>
@@ -58,13 +57,15 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<option value="F">Finalizadas</option>
<option value="A,E,F">Todas</option>
</select>
<label for="filterAtribuicao" class="sr-only">Atribuição</label>
<select id="filterAtribuicao" onchange="carregar()">
<option value="">Todas</option>
<option value="me">Minhas conversas</option>
<option value="sem">Sem atendente</option>
</select>
<label for="buscaContato" class="sr-only">Buscar contato</label>
<input type="text" id="buscaContato" placeholder="Buscar por nome ou número..." onkeyup="if(event.key==='Enter')carregar()">
<button onclick="carregar()">🔄 Atualizar</button>
<button onclick="carregar()" aria-label="Atualizar lista">🔄 Atualizar</button>
</div>
<div class="card" id="tabelaContainer">
<div class="loading"><div class="spinner"></div><p style="margin-top:8px">Carregando conversas...</p></div>
@@ -151,7 +152,7 @@ window.carregar = async function() {
convs.forEach(function(c) {
const statusLabel = c.status === 'E' ? '🟡 Espera' : c.status === 'A' ? '🟢 Atendimento' : '⚫ Finalizado';
const labelsHtml = (c.labels || []).map(function(l) {
return '<span style="display:inline-block;padding:0 6px;border-radius:3px;background:'+(l.cor||'#667eea')+';color:#fff;font-size:10px;margin:1px">'+(l.nome||'')+'</span>';
return '<span style="display:inline-block;padding:0 6px;border-radius:4px;background:'+(l.cor||'#667eea')+';color:#fff;font-size:10px;margin:1px">'+(l.nome||'')+'</span>';
}).join('');
const ultima = (c.ultimaMsg || '').substring(0, 60);
const usuarioNome = c.usuarioId ? 'Usuário ' + c.usuarioId : '-';
@@ -178,5 +179,8 @@ setInterval(carregar, 10000);
})();
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+477 -118
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/css/main.css">
<style>
/* ===== CHAT PAGE - Estilos específicos ===== */
body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
body { background:var(--surface-3); display:flex; height:100vh; overflow:hidden; }
/* ===== SIDEBAR ESQUERDA ===== */
.sidebar-left {
@@ -37,7 +37,7 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
}
.sidebar-left .header-nav a {
padding: 6px 9px;
border-radius: 7px;
border-radius: 6px;
color: rgba(255,255,255,0.55);
text-decoration: none;
font-size: 17px;
@@ -237,35 +237,35 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
.chat-header {
padding: 11px 18px;
border-bottom: 1px solid #e5e7eb;
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: #111827; }
.chat-header .info span { font-size: 12px; color: #6b7280; }
.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 #e5e7eb;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
font-size: 12px;
font-weight: 500;
cursor: pointer;
color: #374151;
color: var(--text-secondary);
transition: all .15s;
}
.chat-header .actions button:hover { background: #f3f4f6; border-color: #d1d5db; }
.chat-header .actions button:hover { background: var(--surface-3); border-color: #d1d5db; }
.chat-header .actions .btn-finalizar {
background: #ef4444;
background: var(--danger);
color: #fff;
border-color: #ef4444;
border-color: var(--danger);
font-weight: 600;
}
.chat-header .actions .btn-finalizar:hover { background: #dc2626; border-color: #dc2626; }
.chat-header .actions .btn-finalizar:hover { background: var(--danger-text); border-color: var(--danger-text); }
/* Área de mensagens - estilo chat */
.messages-area {
@@ -280,7 +280,7 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
}
.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: 3px; }
.messages-area::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 4px; }
/* Bolhas de mensagem */
.msg {
@@ -297,19 +297,19 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
background: #fff;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
color: #1f2937;
color: var(--text-primary);
}
.msg.usuario {
align-self: flex-end;
background: #dcf8c6;
color: #1f2937;
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: #6b7280;
color: var(--text-muted);
font-size: 12px;
padding: 4px 14px;
border-radius: 8px;
@@ -338,10 +338,76 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
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 #e5e7eb;
border-top: 1px solid var(--border);
display: flex;
gap: 6px;
align-items: flex-end;
@@ -351,20 +417,20 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
.chat-input textarea {
flex: 1;
padding: 10px 16px;
border: 1px solid #e5e7eb;
border-radius: 22px;
border: 1px solid var(--border);
border-radius: 9999px;
font-size: 14px;
resize: none;
outline: none;
max-height: 120px;
font-family: inherit;
background: #fff;
color: #1f2937;
color: var(--text-primary);
line-height: 1.4;
transition: border-color .15s;
}
.chat-input textarea:focus { border-color: #667eea; }
.chat-input textarea::placeholder { color: #9ca3af; }
.chat-input textarea:focus { border-color: var(--primary); }
.chat-input textarea::placeholder { color: var(--text-faint); }
.chat-input .btn-enviar {
width: 40px;
@@ -383,10 +449,18 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
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;
}
.chat-input .btn-midia,
.chat-input .btn-audio,
.chat-input .privada-toggle {
/* Botões de ação agrupados no menu "+" */
.chat-input .btn-action {
width: 36px;
height: 36px;
display: flex;
@@ -396,18 +470,89 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 20px;
color: #6b7280;
font-size: 18px;
color: var(--text-muted);
transition: all .15s;
padding: 0;
}
.chat-input .btn-midia:hover,
.chat-input .btn-audio:hover { background: #e5e7eb; color: #374151; }
.chat-input .privada-toggle.ativa { background: #fef3c7; color: #d97706; }
.chat-input .privada-toggle:hover { background: #fef9c3; color: #d97706; }
.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: #fef2f2;
color: #ef4444;
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)} }
@@ -416,7 +561,7 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
.sidebar-right {
width: 280px;
background: #fff;
border-left: 1px solid #e5e7eb;
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
@@ -424,23 +569,52 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
}
.sidebar-right .header {
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
flex-shrink: 0;
}
.sidebar-right .header h3 {
font-size: 11px;
font-weight: 700;
color: #6b7280;
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, #667eea, #764ba2);
background: linear-gradient(135deg, var(--primary), #764ba2);
display: flex;
align-items: center;
justify-content: center;
@@ -454,46 +628,46 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
.sidebar-right .cliente-foto img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-right .info-section {
padding: 10px 16px;
border-bottom: 1px solid #f3f4f6;
border-bottom: 1px solid var(--surface-3);
}
.sidebar-right .info-section .label {
font-size: 10px;
text-transform: uppercase;
color: #9ca3af;
color: var(--text-faint);
letter-spacing: 0.07em;
font-weight: 600;
margin-bottom: 4px;
}
.sidebar-right .info-section .value { font-size: 13px; color: #374151; }
.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 #e5e7eb;
border-radius: 7px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
outline: none;
color: #374151;
color: var(--text-secondary);
background: #fff;
cursor: pointer;
transition: border-color .15s;
}
.sidebar-right .info-section select:focus { border-color: #667eea; }
.sidebar-right .info-section .inadimplente { color: #ef4444; font-weight: 600; }
.sidebar-right .info-section .adimplente { color: #059669; font-weight: 600; }
.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 #e5e7eb;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-align: left;
color: #374151;
color: var(--text-secondary);
transition: all .15s;
}
.sidebar-right .acoes button:hover { background: #f3f4f6; border-color: #d1d5db; }
.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 {
@@ -535,20 +709,20 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
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: #111827; }
.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 #e5e7eb;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 14px;
margin-bottom: 12px;
outline: none;
transition: border-color .15s;
background: #f9fafb;
color: #1f2937;
background: var(--surface-2);
color: var(--text-primary);
}
.modal input:focus { border-color: #667eea; background: #fff; box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.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;
@@ -556,14 +730,14 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid #e5e7eb;
border: 1px solid var(--border);
background: #fff;
color: #374151;
color: var(--text-secondary);
transition: all .15s;
}
.modal .btn-group button:hover { background: #f3f4f6; }
.modal .btn-group .btn-primary { background: #667eea; color: #fff; border-color: #667eea; }
.modal .btn-group .btn-primary:hover { background: #5a67d8; border-color: #5a67d8; }
.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 {
@@ -572,12 +746,12 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(102,126,234,0.12);
border: 3px dashed #667eea;
border: 3px dashed var(--primary);
z-index: 999;
pointer-events: none;
font-size: 22px;
font-weight: 600;
color: #667eea;
color: var(--primary);
}
.drag-overlay.show { display: flex; align-items: center; justify-content: center; }
@@ -588,12 +762,12 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
align-items: center;
justify-content: center;
flex-direction: column;
color: #9ca3af;
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: #6b7280; }
.empty-state p { font-size: 15px; color: var(--text-muted); }
/* Botões só do mobile (voltar / info) */
.mobile-only { display: none; }
@@ -623,17 +797,44 @@ body { background:#f3f4f6; display:flex; height:100vh; overflow:hidden; }
.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 darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</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">
<h3>⚠️ Finalizar conversa</h3>
<p style="font-size:13px;color:#6b7280;margin-bottom:16px">Tem certeza que deseja finalizar este atendimento? A conversa será movida para finalizadas.</p>
<div class="btn-group">
<button onclick="fecharModal('modalConfirmarFinalizar')">Cancelar</button>
<button class="btn-primary" onclick="fecharModal('modalConfirmarFinalizar');finalizarConversa()">Finalizar</button>
</div>
</div>
</div>
<!-- Modal associar cliente -->
<div class="modal-overlay" id="modalAssociar">
<div class="modal">
@@ -696,14 +897,20 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<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"></button>
<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>
<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"></button>
<button class="btn-finalizar" onclick="finalizarConversa()">✅ Finalizar</button>
<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">
@@ -713,16 +920,23 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
</div>
<div class="chat-input" id="chatInput" style="display:none">
<button class="privada-toggle" id="privadaToggle" onclick="togglePrivada()" title="Mensagem privada">🔓</button>
<button class="btn-midia" onclick="document.getElementById('fileInput').click()" title="Anexar arquivo">📎</button>
<button class="btn-audio" id="btnAudio" onclick="toggleGravacao()" title="Gravar áudio">🎤</button>
<input type="file" id="fileInput" style="display:none" accept="image/*,audio/*" onchange="enviarMidia(this)">
<textarea id="msgInput" placeholder="Digite uma mensagem..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();enviarMensagem()}"></textarea>
<button class="btn-enviar" onclick="enviarMensagem()" title="Enviar"></button>
<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"></audio>
<button class="btn-enviar" onclick="enviarAudioGravado()" title="Enviar áudio"></button>
<button class="btn-midia" onclick="cancelarAudioGravado()" title="Cancelar" style="background:#fef2f2;color:#ef4444"></button>
<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>
@@ -730,43 +944,57 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<div class="sidebar-right" id="sidebarRight" style="display:none">
<div class="header"><h3>📋 Informações</h3></div>
<div class="cliente-foto" id="clienteFoto">?</div>
<div class="info-section" id="clienteInfoContainer"></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 (aparece conforme configuração da empresa) -->
<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>
<!-- 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 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 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="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 class="acoes" id="acoesContainer">
<button onclick="mostrarModalAssociar()"><span class="icon">🔗</span> Associar</button>
<button onclick="irPaginaCliente()"><span class="icon">📋</span> Dados do Titular</button>
@@ -818,10 +1046,18 @@ async function carregarConfigResolucao() {
}
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';
panel.style.display = (verMot || verRes) ? 'block' : 'none';
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) {
@@ -1316,6 +1552,12 @@ window.enviarMensagem = async function() {
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';
@@ -1327,10 +1569,67 @@ window.enviarMensagem = async function() {
});
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) {}
} 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 =====
@@ -1369,7 +1668,7 @@ window.toggleGravacao = async function() {
if (gravando) {
if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
btn.classList.remove("recording");
btn.textContent = "🎤";
btn.innerHTML = "🎤 Gravar";
gravando = false;
return;
}
@@ -1407,7 +1706,7 @@ window.toggleGravacao = async function() {
document.getElementById("msgInput").style.display = "none";
document.querySelector(".btn-enviar").style.display = "none";
document.getElementById("btnAudio").style.display = "none";
document.querySelector(".btn-midia").style.display = "none";
document.getElementById("btnPlus").style.display = "none";
document.getElementById("privadaToggle").style.display = "none";
// Converte para base64
@@ -1420,7 +1719,7 @@ window.toggleGravacao = async function() {
mediaRecorder.start();
btn.classList.add("recording");
btn.textContent = "⏹️";
btn.innerHTML = "⏹️ Parar";
gravando = true;
} catch(err) {
alert("Erro ao acessar microfone: " + err.message);
@@ -1457,7 +1756,7 @@ window.cancelarAudioGravado = function() {
document.getElementById("msgInput").style.display = "";
document.querySelector(".btn-enviar").style.display = "";
document.getElementById("btnAudio").style.display = "";
document.querySelector(".btn-midia").style.display = "";
document.getElementById("btnPlus").style.display = "";
document.getElementById("privadaToggle").style.display = "";
};
@@ -1495,7 +1794,7 @@ window.finalizarConversa = async function() {
}
}
if (!confirm('Finalizar esta conversa?')) return;
// Confirmação já foi feita pelo modal — segue direto
try {
var res = await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/finalize', {
method: 'POST',
@@ -1584,11 +1883,68 @@ window.enviarBoleto = async function(carneId, btn) {
};
window.togglePrivada = function() {
document.getElementById('privadaToggle').classList.toggle('ativa');
var btn = document.getElementById('privadaToggle');
btn.textContent = btn.classList.contains('ativa') ? '🔒' : '🔓';
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');
@@ -1777,5 +2133,8 @@ document.getElementById('msgInput').addEventListener('input', function() {
})();
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+155 -85
View File
@@ -6,46 +6,97 @@
<title>Detalhes do Cliente - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background:#f3f4f6; display:flex; min-height:100vh; }
body { background:var(--surface-3); display:flex; min-height:100vh; }
.sidebar-nav .admin-only { display:none; }
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
.container { flex:1; padding:24px; overflow-y:auto; }
.loading { text-align:center; padding:80px 20px; color:#9ca3af; }
.error-box { background:#fef2f2; border:1px solid #fecaca; border-radius:12px; padding:32px; text-align:center; color:#991b1b; }
.loading { text-align:center; padding:80px 20px; color:var(--text-faint); }
.error-box { background:var(--danger-bg); border:1px solid #fecaca; border-radius:12px; padding:32px; text-align:center; color:var(--danger-text); }
.error-box .icon { font-size:48px; margin-bottom:12px; }
.client-header { background:#fff; border-radius:14px; padding:24px; box-shadow:0 1px 4px rgba(0,0,0,0.08); margin-bottom:20px; display:flex; align-items:center; gap:20px; border:1px solid #f3f4f6; }
.client-avatar { width:68px; height:68px; border-radius:50%; background:linear-gradient(135deg,#667eea,#764ba2); display:flex; align-items:center; justify-content:center; color:#fff; font-size:26px; font-weight:800; flex-shrink:0; box-shadow:0 4px 14px rgba(102,126,234,0.3); }
.client-header-info h1 { font-size:22px; font-weight:800; color:#111827; margin-bottom:4px; letter-spacing:-0.3px; }
.client-header-info .matricula { font-size:14px; color:#6b7280; }
.client-header { background:#fff; border-radius:14px; padding:24px; box-shadow:0 1px 4px rgba(0,0,0,0.08); margin-bottom:20px; display:flex; align-items:center; gap:20px; border:1px solid var(--surface-3); }
.client-avatar { width:68px; height:68px; border-radius:50%; background:linear-gradient(135deg,var(--primary),#764ba2); display:flex; align-items:center; justify-content:center; color:#fff; font-size:26px; font-weight:800; flex-shrink:0; box-shadow:0 4px 14px rgba(102,126,234,0.3); }
.client-header-info h1 { font-size:22px; font-weight:800; color:var(--text-primary); margin-bottom:4px; letter-spacing:-0.3px; }
.client-header-info .matricula { font-size:14px; color:var(--text-muted); }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:18px; margin-bottom:20px; }
.card h3 { font-size:12px; text-transform:uppercase; letter-spacing:0.07em; color:#9ca3af; margin-bottom:14px; font-weight:700; }
.card h3 { font-size:12px; text-transform:uppercase; letter-spacing:0.07em; color:var(--text-faint); margin-bottom:14px; font-weight:700; }
.field { margin-bottom:12px; }
.field:last-child { margin-bottom:0; }
.field .label { font-size:11px; text-transform:uppercase; letter-spacing:0.06em; color:#9ca3af; margin-bottom:3px; font-weight:600; }
.field .value { font-size:15px; color:#111827; font-weight:500; }
.field .label { font-size:11px; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-faint); margin-bottom:3px; font-weight:600; }
.field .value { font-size:15px; color:var(--text-primary); font-weight:500; }
.carnes-section { margin-top:20px; }
.carnes-filters { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; align-items:center; }
.carnes-filters label { display:flex; align-items:center; gap:6px; font-size:13px; color:#374151; cursor:pointer; padding:7px 14px; border-radius:8px; border:1px solid #e5e7eb; background:#fff; transition:all 0.15s; user-select:none; font-weight:500; }
.carnes-filters label:hover { border-color:#667eea; color:#667eea; }
.carnes-filters label.filtro-ativo { border-color:#667eea; background:#eef2ff; color:#667eea; font-weight:600; }
.carnes-filters label input { accent-color:#667eea; }
.carnes-filters label { display:flex; align-items:center; gap:6px; font-size:13px; color:var(--text-secondary); cursor:pointer; padding:7px 14px; border-radius:8px; border:1px solid var(--border); background:#fff; transition:all 0.15s; user-select:none; font-weight:500; }
.carnes-filters label:hover { border-color:var(--primary); color:var(--primary); }
.carnes-filters label.filtro-ativo { border-color:var(--primary); background:#eef2ff; color:var(--primary); font-weight:600; }
.carnes-filters label input { accent-color:var(--primary); }
table { font-size:13px; }
.valor { text-align:right; font-family:'SF Mono',monospace; }
.centro { text-align:center; }
.sit-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }
.vencido-dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:#ef4444; margin-right:4px; }
.back-btn { margin-right:14px; padding:7px 14px; background:#f3f4f6; border:1px solid #e5e7eb; border-radius:8px; font-size:13px; font-weight:500; cursor:pointer; color:#374151; text-decoration:none; transition:all .15s; display:inline-flex; align-items:center; gap:4px; }
.back-btn:hover { background:#e5e7eb; border-color:#d1d5db; }
.empty-msg { text-align:center; padding:32px; color:#9ca3af; font-size:14px; }
.vencido-dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--danger); margin-right:4px; }
.back-btn { margin-right:14px; padding:7px 14px; background:var(--surface-3); border:1px solid var(--border); border-radius:8px; font-size:13px; font-weight:500; cursor:pointer; color:var(--text-secondary); text-decoration:none; transition:all .15s; display:inline-flex; align-items:center; gap:4px; }
.back-btn:hover { background:var(--border); border-color:#d1d5db; }
.empty-msg { text-align:center; padding:32px; color:var(--text-faint); font-size:14px; }
/* ===== ABAS ===== */
.aba-btn {
flex: 1; padding: 10px; border: none;
font-size: 13px; cursor: pointer;
transition: all .15s;
}
.aba-btn.ativo { background: var(--primary); color: #fff; font-weight: 600; }
.aba-btn.inativo { background: var(--surface-3); color: var(--text-secondary); font-weight: 400; }
.aba-btn.inativo:hover { background: var(--border); }
/* ===== SUB-ABAS (Convalescentes) ===== */
.subaba-btn {
flex: 1; padding: 6px 10px; border: none;
font-size: 11px; cursor: pointer;
transition: all .15s;
}
.subaba-btn.ativo { background: var(--primary); color: #fff; font-weight: 600; }
.subaba-btn.inativo { background: var(--surface-3); color: var(--text-secondary); }
/* ===== BADGES CONVALESCENTES ===== */
.conv-badge {
display: inline-block; padding: 2px 8px; border-radius: 10px;
font-size: 11px; font-weight: 600;
}
.conv-badge.devolvido { background: var(--success-bg); color: var(--success-text); }
.conv-badge.entregue { background: var(--warning-bg); color: var(--warning-text); }
.conv-badge.ativo { background: #dbeafe; color: #1e40af; }
.conv-badge.cancelado { background: var(--danger-bg); color: var(--danger-text); }
/* ===== BOTÃO AÇÃO PRIMÁRIA NA TOPBAR ===== */
.btn-acao-topbar {
display: inline-flex; align-items: center; gap: 4px;
padding: 7px 16px; border: none; border-radius: 8px;
background: var(--primary); color: #fff;
font-size: 13px; font-weight: 600; cursor: pointer;
text-decoration: none; transition: all .15s;
}
.btn-acao-topbar:hover { background: var(--primary-dark); }
/* ===== FEEDBACK INLINE ===== */
.feedback-msg {
padding: 8px 14px; border-radius: 6px; font-size: 13px;
margin-top: 8px; display: none;
}
.feedback-msg.sucesso { background: var(--success-bg); color: var(--success-text); display: block; }
.feedback-msg.erro { background: var(--danger-bg); color: var(--danger-text); display: block; }
/* ===== CARD CONVALESCENTE ===== */
.conv-card {
border: 1px solid var(--border); border-radius: 8px;
padding: 16px; margin-bottom: 12px;
}
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div><h2>Chatc2</h2><span id="sidebarAlias">-</span></div>
@@ -60,16 +111,16 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" id="navRoutesDetail" class="admin-only"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a>
</div>
</aside>
<div class="main">
<main class="main" id="main-content">
<div class="topbar">
<a href="#" class="back-btn" id="backBtn">← Voltar</a>
<span class="topbar-title">Detalhes do Cliente</span>
<a href="#" class="back-btn" id="btnIniciarConv" style="font-size:13px;margin-left:auto" title="Iniciar conversa WhatsApp">💬 Iniciar Conversa</a>
<a href="#" class="btn-acao-topbar" id="btnIniciarConv" style="margin-left:auto">💬 Iniciar Conversa</a>
</div>
<div class="container" id="container">
<div class="loading" id="loadingState">
@@ -80,18 +131,18 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
<!-- Modal Nova Conversa -->
<div class="modal-overlay" id="modalNovaConvDet" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:450px">
<h3 style="margin-bottom:16px">💬 Iniciar Conversa</h3>
<div class="modal-overlay" id="modalNovaConvDet">
<div class="modal" style="max-width:450px">
<h3>💬 Iniciar Conversa</h3>
<input type="hidden" id="convClienteIdDet">
<input type="hidden" id="convEmpresaIdDet">
<div class="form-group"><label>Número</label><input type="text" id="convNumeroDet" readonly style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;background:#f9fafb;margin-bottom:12px"></div>
<div class="form-group"><label>Cliente</label><input type="text" id="convNomeDet" readonly style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;background:#f9fafb;margin-bottom:12px"></div>
<div class="form-group"><label>Instância WhatsApp</label><select id="convInstanciaDet" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;margin-bottom:12px"></select></div>
<div class="form-group"><label>Mensagem inicial</label><textarea id="convMensagemDet" rows="3" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;resize:vertical;margin-bottom:12px" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button onclick="document.getElementById('modalNovaConvDet').style.display='none'" style="padding:8px 16px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer">Cancelar</button>
<button onclick="iniciarConversaDet()" style="padding:8px 16px;background:#667eea;color:#fff;border:none;border-radius:8px;cursor:pointer">💬 Enviar e Abrir Chat</button>
<div class="form-group"><label for="convNumeroDet">Número</label><input type="text" id="convNumeroDet" readonly></div>
<div class="form-group"><label for="convNomeDet">Cliente</label><input type="text" id="convNomeDet" readonly></div>
<div class="form-group"><label for="convInstanciaDet">Instância WhatsApp</label><select id="convInstanciaDet"></select></div>
<div class="form-group"><label for="convMensagemDet">Mensagem inicial</label><textarea id="convMensagemDet" rows="3" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="document.getElementById('modalNovaConvDet').classList.remove('show')">Cancelar</button>
<button class="btn-primary" onclick="iniciarConversaDet()" id="btnEnviarConv">💬 Enviar e Abrir Chat</button>
</div>
</div>
</div>
@@ -227,17 +278,17 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
'</div>' +
'<div class="grid">' +
'<div class="card"><h3>📞 Contato <button onclick="editarContato()" style="float:right;padding:2px 8px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:11px">✏️ Editar</button></h3>' +
'<div class="card"><h3>📞 Contato <button onclick="editarContato()" style="float:right;padding:2px 8px;border:1px solid var(--border);border-radius:4px;background:#fff;cursor:pointer;font-size:11px">✏️ Editar</button></h3>' +
'<div class="field"><div class="label">Email</div><div class="value" id="campoEmail">' + (c.email || '-') + '</div></div>' +
'<div class="field"><div class="label">Celular</div><div class="value" id="campoCelular">' + (c.celular && c.celular.formatado ? c.celular.formatado : c.celular && c.celular.original ? c.celular.original : '-') + '</div></div>' +
'<div class="field"><div class="label">Telefone</div><div class="value" id="campoTelefone">' + (c.telefone || '-') + '</div></div>' +
'<div id="editContatoForm" style="display:none;margin-top:12px;padding:12px;background:#f9fafb;border-radius:8px">' +
'<input type="email" id="editEmail" placeholder="Email" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editCelular" placeholder="Celular" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editTelefone" placeholder="Telefone" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<div id="editContatoForm" style="display:none;margin-top:12px;padding:12px;background:var(--surface-2);border-radius:8px">' +
'<input type="email" id="editEmail" placeholder="Email" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editCelular" placeholder="Celular" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editTelefone" placeholder="Telefone" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<div style="display:flex;gap:8px">' +
'<button onclick="salvarContato()" style="padding:6px 14px;background:#667eea;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px">💾 Salvar</button>' +
'<button onclick="cancelarEditarContato()" style="padding:6px 14px;border:1px solid #d1d5db;border-radius:6px;background:#fff;cursor:pointer;font-size:12px">Cancelar</button>' +
'<button onclick="salvarContato()" style="padding:6px 14px;background:var(--primary);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px">💾 Salvar</button>' +
'<button onclick="cancelarEditarContato()" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:#fff;cursor:pointer;font-size:12px">Cancelar</button>' +
'</div>' +
'</div>' +
'</div>' +
@@ -258,11 +309,11 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
'</div>' +
// ABAS: Conversas | Títulos | Dependentes | Convalescentes
'<div style="display:flex;gap:0;margin-bottom:16px;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08)">' +
'<button class="aba-ativa" onclick="mudarAba(\'conversas\')" id="abaConversas" style="flex:1;padding:10px;border:none;background:#667eea;color:#fff;font-size:13px;font-weight:600;cursor:pointer">💬 Conversas</button>' +
'<button onclick="mudarAba(\'titulos\')" id="abaTitulos" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">📄 Títulos</button>' +
'<button onclick="mudarAba(\'dependentes\')" id="abaDependentes" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">👥 Dependentes</button>' +
'<button onclick="mudarAba(\'convalescentes\')" id="abaConvalescentes" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">🛏️ Convalescentes</button>' +
'<div style="display:flex;gap:0;margin-bottom:16px;background:var(--surface);border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08)">' +
'<button class="aba-btn ativo" onclick="mudarAba(\'conversas\')" id="abaConversas">💬 Conversas</button>' +
'<button class="aba-btn inativo" onclick="mudarAba(\'titulos\')" id="abaTitulos">📄 Títulos</button>' +
'<button class="aba-btn inativo" onclick="mudarAba(\'dependentes\')" id="abaDependentes">👥 Dependentes</button>' +
'<button class="aba-btn inativo" onclick="mudarAba(\'convalescentes\')" id="abaConvalescentes">🛏️ Convalescentes</button>' +
'</div>' +
'<div id="conteudoAba">' +
// CONVERSAS (padrão - primeira aba)
@@ -310,25 +361,24 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
var cC = document.getElementById('conteudoConvalescentes');
var btns = [btnV, btnT, btnD, btnC];
var contents = [cV, cT, cD, cC];
btns.forEach(function(b) { if (b) { b.style.background = '#f3f4f6'; b.style.color = '#374151'; b.style.fontWeight = '400'; } });
btns.forEach(function(b) { if (b) { b.className = 'aba-btn inativo'; } });
contents.forEach(function(c) { if (c) c.style.display = 'none'; });
var activeBtn, activeContent;
if (aba === 'conversas') {
btnV.style.background = '#667eea'; btnV.style.color = '#fff'; btnV.style.fontWeight = '600';
cV.style.display = 'block';
activeBtn = btnV; activeContent = cV;
carregarConversas();
} else if (aba === 'titulos') {
btnT.style.background = '#667eea'; btnT.style.color = '#fff'; btnT.style.fontWeight = '600';
cT.style.display = 'block';
activeBtn = btnT; activeContent = cT;
loadCarnes(1);
} else if (aba === 'dependentes') {
btnD.style.background = '#667eea'; btnD.style.color = '#fff'; btnD.style.fontWeight = '600';
cD.style.display = 'block';
activeBtn = btnD; activeContent = cD;
carregarDependentes();
} else if (aba === 'convalescentes') {
btnC.style.background = '#667eea'; btnC.style.color = '#fff'; btnC.style.fontWeight = '600';
cC.style.display = 'block';
activeBtn = btnC; activeContent = cC;
carregarConvalescentes();
}
if (activeBtn) activeBtn.className = 'aba-btn ativo';
if (activeContent) activeContent.style.display = 'block';
};
async function carregarConversas() {
@@ -360,7 +410,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
'<td style="font-size:12px">' + statusLabel + '</td>' +
'<td style="font-size:12px">' + usuario + '</td>' +
'<td style="font-size:12px">' + equipe + '</td>' +
'<td><span style="color:#667eea;font-size:12px">Abrir →</span></td>' +
'<td><span style="color:var(--primary);font-size:12px">Abrir →</span></td>' +
'</tr>';
});
html += '</tbody></table></div>';
@@ -389,7 +439,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
var telEdit = d.telefone || '';
html += '<tr><td><strong>' + d.nome + '</strong></td><td>' + getParentescoLabel(d.parentesco) + '</td><td>' +
'<span id="depTel_' + d.id + '">' + (telEdit || '-') + '</span> ' +
'<button onclick="editarTelDep(' + d.id + ')" data-tel="' + telEdit.replace(/"/g,'&quot;') + '" style="padding:1px 6px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:10px">✏️</button></td><td>' + adicional + '</td><td style="text-align:right;font-family:monospace">R$ ' + (d.valorContribuicao / 100).toFixed(2) + '</td><td>' + sit + '</td></tr>';
'<button onclick="editarTelDep(' + d.id + ')" data-tel="' + telEdit.replace(/"/g,'&quot;') + '" style="padding:1px 6px;border:1px solid var(--border);border-radius:4px;background:#fff;cursor:pointer;font-size:10px">✏️</button></td><td>' + adicional + '</td><td style="text-align:right;font-family:monospace">R$ ' + (d.valorContribuicao / 100).toFixed(2) + '</td><td>' + sit + '</td></tr>';
});
html += '</tbody></table></div>';
area.innerHTML = html;
@@ -413,34 +463,34 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
var html = '';
data.data.forEach(function(c) {
var sitLabel = c.situacao || '-';
var sitBadge = c.situacao === 'D' ? '<span class="sit-badge" style="background:#d1fae5;color:#065f46">Devolvido</span>' :
c.situacao === 'E' ? '<span class="sit-badge" style="background:#fef3c7;color:#92400e">Entregue</span>' :
c.situacao === 'A' ? '<span class="sit-badge" style="background:#dbeafe;color:#1e40af">Ativo</span>' :
c.situacao === 'C' ? '<span class="sit-badge" style="background:#fef2f2;color:#991b1b">Cancelado</span>' :
'<span class="sit-badge" style="background:#f3f4f6;color:#6b7280">' + c.situacao + '</span>';
html += '<div style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px">' +
var sitBadge = c.situacao === 'D' ? '<span class="conv-badge devolvido">Devolvido</span>' :
c.situacao === 'E' ? '<span class="conv-badge entregue">Entregue</span>' :
c.situacao === 'A' ? '<span class="conv-badge ativo">Ativo</span>' :
c.situacao === 'C' ? '<span class="conv-badge cancelado">Cancelado</span>' :
'<span class="conv-badge" style="background:var(--surface-3);color:var(--text-muted)">' + c.situacao + '</span>';
html += '<div class="conv-card">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<h4 style="margin:0;font-size:15px">🛏️ Convalescente #' + c.id + '</h4>' +
sitBadge +
'</div>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;font-size:13px;margin-bottom:12px">' +
'<div><span style="color:#9ca3af">Valor Total:</span> <strong>R$ ' + (c.valorTotal || 0).toFixed(2) + '</strong></div>' +
'<div><span style="color:#9ca3af">Data Saída:</span> ' + fmtDate(c.dtSaida) + '</div>' +
'<div><span style="color:#9ca3af">Previsão Retorno:</span> ' + fmtDate(c.dtPrevisaoRetorno) + '</div>' +
(c.dtCancelou ? '<div><span style="color:#9ca3af">Data Cancelamento:</span> ' + fmtDate(c.dtCancelou) + '</div>' : '') +
'<div><span style="color:#9ca3af">Cadastrado por:</span> ' + (c.usuarioNome || '-') + '</div>' +
'<div><span style="color:var(--text-faint)">Valor Total:</span> <strong>R$ ' + (c.valorTotal || 0).toFixed(2) + '</strong></div>' +
'<div><span style="color:var(--text-faint)">Data Saída:</span> ' + fmtDate(c.dtSaida) + '</div>' +
'<div><span style="color:var(--text-faint)">Previsão Retorno:</span> ' + fmtDate(c.dtPrevisaoRetorno) + '</div>' +
(c.dtCancelou ? '<div><span style="color:var(--text-faint)">Data Cancelamento:</span> ' + fmtDate(c.dtCancelou) + '</div>' : '') +
'<div><span style="color:var(--text-faint)">Cadastrado por:</span> ' + (c.usuarioNome || '-') + '</div>' +
'</div>';
// SUB-ABAS: Boletos | Itens
var temCarnes = c.carnes && c.carnes.length > 0;
var temItens = c.itens && c.itens.length > 0;
if (temCarnes || temItens) {
html += '<div style="margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb">' +
html += '<div style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">' +
'<div style="display:flex;gap:0;margin-bottom:8px;border-radius:6px;overflow:hidden">';
if (temCarnes) {
html += '<button class="subaba-ativa" onclick="mudarSubaba(\'carnes_' + c.id + '\',this)" id="subCarnes_' + c.id + '" style="flex:1;padding:6px 10px;border:none;background:#667eea;color:#fff;font-size:11px;font-weight:600;cursor:pointer">📄 Boletos (' + c.carnes.length + ')</button>';
html += '<button class="subaba-btn ativo" onclick="mudarSubaba(\'carnes_' + c.id + '\',this)" id="subCarnes_' + c.id + '">📄 Boletos (' + c.carnes.length + ')</button>';
}
if (temItens) {
html += '<button onclick="mudarSubaba(\'itens_' + c.id + '\',this)" id="subItens_' + c.id + '" style="flex:1;padding:6px 10px;border:none;background:#f3f4f6;color:#374151;font-size:11px;cursor:pointer">📦 Itens (' + c.itens.length + ')</button>';
html += '<button class="subaba-btn ' + (temCarnes ? 'inativo' : 'ativo') + '" onclick="mudarSubaba(\'itens_' + c.id + '\',this)" id="subItens_' + c.id + '">📦 Itens (' + c.itens.length + ')</button>';
}
html += '</div>';
// Conteudo Boletos
@@ -489,8 +539,8 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
window.mudarSubaba = function(id, btn) {
var container = btn.parentElement.parentElement;
var botoes = container.querySelectorAll('button[id^="sub"]');
botoes.forEach(function(b) { b.style.background = '#f3f4f6'; b.style.color = '#374151'; });
btn.style.background = '#667eea'; btn.style.color = '#fff';
botoes.forEach(function(b) { b.className = 'subaba-btn inativo'; });
btn.className = 'subaba-btn ativo';
var prefixo = id.split('_')[0];
var covId = id.split('_')[1];
var cCarnes = document.getElementById('conteudoCarnes_' + covId);
@@ -511,7 +561,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
span.innerHTML =
'<input type="text" id="depTelInput_' + id + '" value="' + telAtual.replace(/"/g, '&quot;') + '" style="width:120px;padding:2px 6px;border:1px solid #667eea;border-radius:4px;font-size:12px;outline:none">' +
' <button onclick="confirmarTelDep(' + id + ')" title="Confirmar" style="padding:1px 7px;border:1px solid #059669;border-radius:4px;background:#059669;color:#fff;cursor:pointer;font-size:11px"></button>' +
' <button onclick="cancelarTelDep(' + id + ')" title="Cancelar" style="padding:1px 7px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:11px"></button>';
' <button onclick="cancelarTelDep(' + id + ')" title="Cancelar" style="padding:1px 7px;border:1px solid var(--border);border-radius:4px;background:#fff;cursor:pointer;font-size:11px"></button>';
var inp = document.getElementById('depTelInput_' + id);
if (inp) {
inp.focus();
@@ -638,7 +688,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
for (var i = 0; i < data.data.length; i++) {
var c = data.data[i];
var bg = c.situacao.color || '#f3f4f6';
var tc = c.situacao.textColor || '#6b7280';
var tc = c.situacao.textColor || 'var(--text-muted)';
var vencDot = c.vencido ? '<span class="vencido-dot"></span>' : '';
html += '<tr>' +
@@ -705,7 +755,10 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
if (celular) data.celular = celular;
if (telefone) data.telefone = telefone;
if (Object.keys(data).length === 0) { alert('Preencha ao menos um campo'); return; }
if (Object.keys(data).length === 0) {
mostrarFeedback('Preencha ao menos um campo', 'erro');
return;
}
try {
var r = await fetch('/api/' + alias + '/clients/' + clienteId, {
@@ -715,15 +768,29 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
});
var d = await r.json();
if (d.success) {
alert('Dados atualizados!');
mostrarFeedback('Dados atualizados com sucesso!', 'sucesso');
document.getElementById('editContatoForm').style.display = 'none';
loadClient(); // Recarrega os dados
loadClient();
} else {
alert('Erro: ' + d.error);
mostrarFeedback('Erro: ' + d.error, 'erro');
}
} catch(e) { alert('Erro: ' + e.message); }
} catch(e) { mostrarFeedback('Erro: ' + e.message, 'erro'); }
};
function mostrarFeedback(msg, tipo) {
var fb = document.getElementById('editFeedback');
if (!fb) {
fb = document.createElement('div');
fb.id = 'editFeedback';
fb.className = 'feedback-msg';
var form = document.getElementById('editContatoForm');
form.parentNode.insertBefore(fb, form.nextSibling);
}
fb.textContent = msg;
fb.className = 'feedback-msg ' + tipo;
setTimeout(function() { if (fb) fb.className = 'feedback-msg'; }, 3000);
}
// ===== MODAL NOVA CONVERSA =====
window.abrirModalConversa = async function(empresaId, clienteId, nome, celular) {
document.getElementById('convEmpresaIdDet').value = empresaId;
@@ -750,7 +817,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
sel.innerHTML = '<option value="">Erro ao carregar</option>';
}
document.getElementById('modalNovaConvDet').style.display = 'flex';
document.getElementById('modalNovaConvDet').classList.add('show');
};
window.iniciarConversaDet = async function() {
@@ -772,7 +839,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
});
var d = await r.json();
if (d.success) {
document.getElementById('modalNovaConvDet').style.display = 'none';
document.getElementById('modalNovaConvDet').classList.remove('show');
window.location.href = '/app/' + alias + '/company/' + empresaId + '/conversation/' + d.data.id;
} else {
alert('Erro: ' + d.error);
@@ -781,7 +848,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
};
// Fechar modal ao clicar fora
document.getElementById('modalNovaConvDet').onclick = function(e) { if (e.target === this) this.style.display = 'none'; };
document.getElementById('modalNovaConvDet').onclick = function(e) { if (e.target === this) this.classList.remove('show'); };
// ---- Iniciar ----
loadClient();
@@ -789,5 +856,8 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
})();
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+51 -33
View File
@@ -6,18 +6,16 @@
<title>Clientes - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background: #f3f4f6; display: flex; min-height: 100vh; }
body { background: var(--surface-3); display: flex; min-height: 100vh; }
.sidebar-nav .admin-only { display: none; }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.container { flex: 1; padding: 24px; overflow-y: auto; }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div><h2>Chatc2</h2><span id="sidebarAlias">lajedo</span></div>
@@ -32,12 +30,12 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" id="navRoutes" class="admin-only"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a>
</div>
</aside>
<div class="main">
<main class="main" id="main-content">
<div class="topbar">
<span class="topbar-title">👥 Clientes</span>
<div class="user-info">
@@ -47,12 +45,12 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
<div class="container">
<div class="search-bar">
<div class="search-bar" role="search">
<select id="empresaSelect">
<option value="">Todas as empresas</option>
</select>
<input type="text" id="searchInput" placeholder="Buscar por nome, matrícula ou CPF..." autofocus />
<button id="btnSearch">🔍 Buscar</button>
<label for="searchInput" class="sr-only">Buscar</label><input type="text" id="searchInput" placeholder="Buscar por nome, matrícula ou CPF..." autofocus />
<button id="btnSearch" aria-label="Buscar">🔍 Buscar</button>
<span class="total-info" id="totalInfo"></span>
</div>
@@ -249,28 +247,35 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
// === INICIAR ===
loadEmpresas().then(() => fetchClients(1, '', ''));
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>
<!-- Modal Nova Conversa -->
<div class="modal-overlay" id="modalNovaConv" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:450px">
<h3 style="margin-bottom:16px">💬 Iniciar Conversa</h3>
<div class="modal-overlay" id="modalNovaConv">
<div class="modal" style="max-width:450px">
<h3>💬 Iniciar Conversa</h3>
<input type="hidden" id="convClienteId">
<input type="hidden" id="convEmpresaId">
<div class="form-group"><label>Número</label><input type="text" id="convNumero" readonly style="background:#f9fafb"></div>
<div class="form-group"><label>Cliente</label><input type="text" id="convNome" readonly style="background:#f9fafb"></div>
<div class="form-group"><label>Instância WhatsApp</label><select id="convInstancia" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px"></select></div>
<div class="form-group"><label>Mensagem inicial</label><textarea id="convMensagem" rows="3" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;resize:vertical" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModalConv()" style="padding:8px 16px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer">Cancelar</button>
<button class="btn btn-primary" onclick="iniciarConversa()" style="padding:8px 16px;background:#667eea;color:#fff;border:none;border-radius:8px;cursor:pointer">💬 Enviar e Abrir Chat</button>
<div class="form-group"><label for="convNumero">Número</label><input type="text" id="convNumero" readonly></div>
<div class="form-group"><label for="convNome">Cliente</label><input type="text" id="convNome" readonly></div>
<div class="form-group"><label for="convInstancia">Instância WhatsApp</label><select id="convInstancia"></select></div>
<div class="form-group"><label for="convMensagem">Mensagem inicial</label><textarea id="convMensagem" rows="3" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div id="convFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModalConv()">Cancelar</button>
<button class="btn-primary" onclick="iniciarConversa()">💬 Enviar e Abrir Chat</button>
</div>
</div>
</div>
<script>
// ===== FECHAR MODAIS COM ESC =====
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.show').forEach(function(m) {
m.classList.remove('show');
});
}
});
// ===== NOVA CONVERSA =====
var modalConvEl = document.getElementById('modalNovaConv');
@@ -280,8 +285,8 @@ window.abrirModalConversa = async function(empresaId, clienteId, nome, celular)
document.getElementById('convNumero').value = celular || '';
document.getElementById('convNome').value = nome || '';
document.getElementById('convMensagem').value = 'Olá ' + (nome || '') + '! Tudo bem? Como podemos ajudar?';
document.getElementById('convFeedback').className = 'feedback-msg';
// Carregar instâncias
var sel = document.getElementById('convInstancia');
sel.innerHTML = '<option value="">Carregando...</option>';
try {
@@ -300,11 +305,11 @@ window.abrirModalConversa = async function(empresaId, clienteId, nome, celular)
sel.innerHTML = '<option value="">Erro ao carregar</option>';
}
modalConvEl.style.display = 'flex';
modalConvEl.classList.add('show');
};
window.fecharModalConv = function() {
modalConvEl.style.display = 'none';
modalConvEl.classList.remove('show');
};
window.iniciarConversa = async function() {
@@ -316,11 +321,10 @@ window.iniciarConversa = async function() {
var clienteId = document.getElementById('convClienteId').value;
if (!numero || !mensagem) {
alert('Informe o número e a mensagem');
mostrarConvFeedback('Informe o número e a mensagem', 'erro');
return;
}
// Adicionar código do país se necessário
var numCompleto = numero.startsWith('55') ? numero : '55' + numero;
try {
@@ -334,13 +338,27 @@ window.iniciarConversa = async function() {
fecharModalConv();
window.location.href = '/app/' + alias + '/company/' + empresaId + '/conversation/' + d.data.id;
} else {
alert('Erro: ' + d.error);
mostrarConvFeedback('Erro: ' + d.error, 'erro');
}
} catch(e) {
alert('Erro de conexão: ' + e.message);
mostrarConvFeedback('Erro de conexão: ' + e.message, 'erro');
}
};
// Fechar modal ao clicar fora
modalConvEl.addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
function mostrarConvFeedback(msg, tipo) {
var fb = document.getElementById('convFeedback');
fb.textContent = msg;
fb.className = 'feedback-msg ' + tipo;
setTimeout(function() { fb.className = 'feedback-msg'; }, 4000);
}
modalConvEl.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('show'); });
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
</script>
+13 -10
View File
@@ -8,7 +8,7 @@
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
@@ -25,8 +25,8 @@ body {
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.logo { font-size: 48px; margin-bottom: 16px; }
h1 { font-size: 24px; color: #1f2937; margin-bottom: 8px; }
.subtitle { font-size: 14px; color: #6b7280; margin-bottom: 32px; }
h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
.subtitle { font-size: 14px; color: var(--text-muted); margin-bottom: 32px; }
.stars {
display: flex;
@@ -55,7 +55,7 @@ h1 { font-size: 24px; color: #1f2937; margin-bottom: 8px; }
textarea {
width: 100%;
padding: 14px 16px;
border: 2px solid #e5e7eb;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 14px;
font-family: inherit;
@@ -65,12 +65,12 @@ textarea {
transition: border-color .2s;
margin-bottom: 20px;
}
textarea:focus { border-color: #667eea; }
textarea:focus { border-color: var(--primary); }
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 12px;
@@ -84,18 +84,18 @@ textarea:focus { border-color: #667eea; }
.rating-text {
font-size: 14px;
color: #6b7280;
color: var(--text-muted);
margin-bottom: 24px;
min-height: 20px;
}
.success { display: none; }
.success .icon { font-size: 64px; margin-bottom: 16px; }
.success h2 { color: #059669; margin-bottom: 8px; }
.success p { color: #6b7280; }
.success h2 { color: var(--success); margin-bottom: 8px; }
.success p { color: var(--text-muted); }
.erro {
color: #ef4444;
color: var(--danger);
font-size: 14px;
margin-top: 12px;
display: none;
@@ -204,5 +204,8 @@ async function enviar() {
getParams();
</script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -110,7 +110,7 @@ body {
.sidebar-nav::-webkit-scrollbar { width: 3px; }
.sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
.sidebar-nav .nav-label {
font-size: 10px;
@@ -644,9 +644,20 @@ tr:last-child td { border-bottom: none; }
/* ===== SCROLLBAR CUSTOMIZADA ===== */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
/* ===== SCREEN READER ONLY ===== */
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
/* =====================================================
RESPONSIVO (tablet / celular)
===================================================== */
+20 -19
View File
@@ -6,28 +6,28 @@
<title>Dashboard - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background: #f3f4f6; display: flex; min-height: 100vh; }
body { background: var(--surface-3); display: flex; min-height: 100vh; }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.container { flex: 1; padding: 24px; overflow-y: auto; }
.section-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #6b7280; margin: 4px 0 12px; }
.section-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin: 4px 0 12px; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 24px; }
.stat-card { background: #fff; border-radius: 12px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); border-left: 4px solid #9ca3af; }
.stat-card .num { font-size: 32px; font-weight: 800; color: #111827; line-height: 1; }
.stat-card .lbl { font-size: 13px; color: #6b7280; margin-top: 6px; font-weight: 600; }
.stat-card .desc { font-size: 11px; color: #9ca3af; margin-top: 4px; }
.stat-card { background: #fff; border-radius: 12px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); border-left: 4px solid var(--text-faint); }
.stat-card .num { font-size: 32px; font-weight: 800; color: var(--text-primary); line-height: 1; }
.stat-card .lbl { font-size: 13px; color: var(--text-muted); margin-top: 6px; font-weight: 600; }
.stat-card .desc { font-size: 11px; color: var(--text-faint); margin-top: 4px; }
.stat-card.abertas { border-left-color: #10b981; }
.stat-card.naoatendidas { border-left-color: #ef4444; }
.stat-card.naoatendidas { border-left-color: var(--danger); }
.stat-card.naoatribuidas { border-left-color: #f59e0b; }
.stat-card.pendentes { border-left-color: #6366f1; }
.stat-card.disponiveis { border-left-color: #10b981; }
.stat-card.desconectados { border-left-color: #9ca3af; }
.stat-card.desconectados { border-left-color: var(--text-faint); }
.card { background:#fff; border-radius:12px; padding:18px; box-shadow:0 1px 3px rgba(0,0,0,0.08); margin-bottom:24px; }
.atendente-row { display:flex; align-items:center; gap:8px; padding:7px 0; border-bottom:1px solid #f3f4f6; font-size:14px; }
.atendente-row { display:flex; align-items:center; gap:8px; padding:7px 0; border-bottom:1px solid var(--surface-3); font-size:14px; }
.atendente-row:last-child { border-bottom:none; }
.dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; }
.dot.on { background:#10b981; box-shadow:0 0 0 3px rgba(16,185,129,.18); }
.dot.off { background:#cbd5e1; }
.atendente-row .st { margin-left:auto; font-size:12px; color:#6b7280; }
.atendente-row .st { margin-left:auto; font-size:12px; color:var(--text-muted); }
/* Heatmap (estilo GitHub) */
.heatmap-scroll { overflow-x:auto; padding-bottom:6px; }
.heatmap { display:flex; gap:3px; }
@@ -35,18 +35,16 @@
.hm-cell { width:12px; height:12px; border-radius:2px; background:#ebedf0; }
.hm-cell.l1 { background:#9be9a8; } .hm-cell.l2 { background:#40c463; }
.hm-cell.l3 { background:#30a14e; } .hm-cell.l4 { background:#216e39; }
.hm-months { display:flex; gap:3px; font-size:10px; color:#9ca3af; margin-bottom:4px; height:12px; }
.hm-legend { display:flex; align-items:center; gap:4px; font-size:11px; color:#9ca3af; margin-top:8px; justify-content:flex-end; }
.hm-months { display:flex; gap:3px; font-size:10px; color:var(--text-faint); margin-bottom:4px; height:12px; }
.hm-legend { display:flex; align-items:center; gap:4px; font-size:11px; color:var(--text-faint); margin-top:8px; justify-content:flex-end; }
body.dark-mode .hm-cell { background:#161b22; }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<!-- SIDEBAR -->
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div>
@@ -65,13 +63,13 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" id="navRoutes" style="display:none"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a>
</div>
</aside>
<!-- MAIN -->
<div class="main">
<main class="main" id="main-content">
<div class="topbar">
<span class="topbar-title">Dashboard</span>
<div class="user-info">
@@ -259,5 +257,8 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
}
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+31 -39
View File
@@ -1,44 +1,36 @@
// Dark Mode Controller - Only handles initialization/persistence
// Dark Mode Controller — Unified for all Chatc2 pages
(function() {
if (typeof window.darkModeToggle !== 'function') {
// Ensure function exists (inline script in head should have defined it)
window.darkModeToggle = function() {
var el = document.body;
if (!el) return;
var isDark = localStorage.getItem('chatc2_dark_mode') === 'true';
var enable = !isDark;
el.classList.toggle('dark-mode', enable);
localStorage.setItem('chatc2_dark_mode', enable ? 'true' : 'false');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = enable ? '☀️ Claro' : '🌙 Escuro';
});
};
window.darkModeApply = function(enable) {
var el = document.body;
if (!el) return;
el.classList.toggle('dark-mode', enable);
localStorage.setItem('chatc2_dark_mode', enable ? 'true' : 'false');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = enable ? '☀️ Claro' : '🌙 Escuro';
});
};
window.darkModeIsDark = function() { return localStorage.getItem('chatc2_dark_mode') === 'true'; };
// Apply on load immediately (FOUC prevention)
if (localStorage.getItem('chatc2_dark_mode') === 'true') {
document.documentElement.classList.add('dark-mode-pending');
}
// Apply on load
if (localStorage.getItem('chatc2_dark_mode') === 'true') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('dark-mode');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = '☀️ Claro';
});
});
} else {
document.body.classList.add('dark-mode');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = '☀️ Claro';
});
}
function applyTheme(enable) {
document.documentElement.classList.remove('dark-mode-pending');
document.body && document.body.classList.toggle('dark-mode', enable);
localStorage.setItem('chatc2_dark_mode', enable ? 'true' : 'false');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = enable ? '☀️ Claro' : '🌙 Escuro';
});
}
window.darkModeToggle = function() {
var isDark = localStorage.getItem('chatc2_dark_mode') === 'true';
applyTheme(!isDark);
};
window.darkModeApply = function(enable) { applyTheme(enable); };
window.darkModeIsDark = function() {
return localStorage.getItem('chatc2_dark_mode') === 'true';
};
// Apply stored preference after DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
applyTheme(localStorage.getItem('chatc2_dark_mode') === 'true');
});
} else {
applyTheme(localStorage.getItem('chatc2_dark_mode') === 'true');
}
})();
+20 -19
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/css/main.css">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 50%, #f093fb 100%);
background-size: 400% 400%;
animation: gradientShift 12s ease infinite;
min-height: 100vh;
@@ -40,7 +40,7 @@
.login-header .logo {
width: 76px;
height: 76px;
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, var(--primary), #764ba2);
border-radius: 20px;
display: inline-flex;
align-items: center;
@@ -63,13 +63,13 @@
.login-header h1 {
font-size: 24px;
font-weight: 800;
color: #111827;
color: var(--text-primary);
margin-bottom: 6px;
letter-spacing: -0.5px;
}
.login-header p {
color: #6b7280;
color: var(--text-muted);
font-size: 14px;
}
@@ -81,31 +81,31 @@
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
color: var(--text-secondary);
margin-bottom: 7px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border: 2px solid var(--border);
border-radius: 11px;
font-size: 15px;
transition: all 0.15s;
outline: none;
background: #f9fafb;
color: #111827;
background: var(--surface-2);
color: var(--text-primary);
}
.form-group input:focus {
border-color: #667eea;
border-color: var(--primary);
background: #fff;
box-shadow: 0 0 0 4px rgba(102,126,234,0.12);
}
.form-group input.error {
border-color: #ef4444;
background: #fef2f2;
border-color: var(--danger);
background: var(--danger-bg);
}
.password-wrapper { position: relative; }
@@ -119,17 +119,17 @@
background: none;
border: none;
cursor: pointer;
color: #9ca3af;
color: var(--text-faint);
font-size: 18px;
padding: 4px;
transition: color 0.15s;
}
.toggle-password:hover { color: #6b7280; }
.toggle-password:hover { color: var(--text-muted); }
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, var(--primary), #764ba2);
color: #fff;
border: none;
border-radius: 11px;
@@ -145,8 +145,8 @@
.btn-login:disabled { opacity: 0.6; cursor: not-allowed; }
.error-message {
background: #fef2f2;
color: #dc2626;
background: var(--danger-bg);
color: var(--danger-text);
padding: 12px 16px;
border-radius: 10px;
font-size: 13px;
@@ -196,9 +196,7 @@
}
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<div class="login-container">
@@ -355,5 +353,8 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<button class="dark-mode-toggle" onclick="darkModeToggle()">🌙 Escuro</button>
</div>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+39 -38
View File
@@ -6,89 +6,87 @@
<title>Rotas - Chatc2 API</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background:#f3f4f6; display:flex; min-height:100vh; }
body { background:var(--surface-3); display:flex; min-height:100vh; }
.sidebar-nav .admin-only { display:none; }
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
.container { flex:1; padding:24px; overflow-y:auto; }
/* Barra de busca/filtros */
.search-box { margin-bottom:18px; display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
.search-box input { flex:1; min-width:220px; padding:10px 16px; border:2px solid #e5e7eb; border-radius:8px; font-size:14px; outline:none; transition:border-color .15s; background:#fff; color:#1f2937; }
.search-box input:focus { border-color:#667eea; }
.search-box select { padding:10px 12px; border:2px solid #e5e7eb; border-radius:8px; font-size:13px; outline:none; background:#fff; cursor:pointer; color:#374151; }
.search-box select:focus { border-color:#667eea; }
.btn-ghost { padding:9px 14px; border:2px solid #e5e7eb; border-radius:8px; background:#fff; cursor:pointer; font-size:13px; color:#374151; }
.btn-ghost:hover { background:#f3f4f6; }
.search-box input { flex:1; min-width:220px; padding:10px 16px; border:2px solid var(--border); border-radius:8px; font-size:14px; outline:none; transition:border-color .15s; background:#fff; color:var(--text-primary); }
.search-box input:focus { border-color:var(--primary); }
.search-box select { padding:10px 12px; border:2px solid var(--border); border-radius:8px; font-size:13px; outline:none; background:#fff; cursor:pointer; color:var(--text-secondary); }
.search-box select:focus { border-color:var(--primary); }
.btn-ghost { padding:9px 14px; border:2px solid var(--border); border-radius:8px; background:#fff; cursor:pointer; font-size:13px; color:var(--text-secondary); }
.btn-ghost:hover { background:var(--surface-3); }
/* Legenda */
.legenda { display:flex; gap:16px; flex-wrap:wrap; font-size:12px; color:#6b7280; margin-bottom:16px; align-items:center; }
.legenda { display:flex; gap:16px; flex-wrap:wrap; font-size:12px; color:var(--text-muted); margin-bottom:16px; align-items:center; }
.legenda .pill { display:inline-flex; align-items:center; gap:5px; }
/* Categoria */
.cat-group { margin-bottom:26px; }
.cat-title { display:flex; align-items:center; gap:8px; font-size:15px; font-weight:700; color:#374151; margin:0 0 10px; }
.cat-title .count { font-size:11px; background:#e5e7eb; color:#6b7280; padding:2px 8px; border-radius:10px; font-weight:600; }
.cat-title { display:flex; align-items:center; gap:8px; font-size:15px; font-weight:700; color:var(--text-secondary); margin:0 0 10px; }
.cat-title .count { font-size:11px; background:var(--border); color:var(--text-muted); padding:2px 8px; border-radius:8px; font-weight:600; }
/* Card de rota (expansível) */
.route { background:#fff; border:1px solid #e5e7eb; border-radius:10px; margin-bottom:8px; overflow:hidden; }
.route[open] { box-shadow:0 1px 6px rgba(0,0,0,.06); border-color:#d1d5db; }
.route { background:#fff; border:1px solid var(--border); border-radius:8px; margin-bottom:8px; overflow:hidden; }
.route[open] { box-shadow:0 1px 6px rgba(0,0,0,.06); border-color:var(--border); }
.route-head { list-style:none; cursor:pointer; display:flex; align-items:center; gap:10px; padding:12px 14px; flex-wrap:wrap; }
.route-head::-webkit-details-marker { display:none; }
.route-head:hover { background:#fafafa; }
.route-head:hover { background:var(--surface-2); }
.route[open] .route-head { border-bottom:1px solid #f0f0f0; background:#fafbff; }
.chev { color:#9ca3af; font-size:11px; transition:transform .15s; }
.chev { color:var(--text-faint); font-size:11px; transition:transform .15s; }
.route[open] .chev { transform:rotate(90deg); }
.method { display:inline-block; padding:3px 9px; border-radius:5px; font-size:11px; font-weight:700; text-transform:uppercase; color:#fff; min-width:54px; text-align:center; }
.method.get { background:#059669; }
.method.get { background:var(--success); }
.method.post { background:#2563eb; }
.method.put { background:#d97706; }
.method.delete { background:#dc2626; }
.method.delete { background:var(--danger-text); }
.method.patch { background:#7c3aed; }
.route-path { font-family:'SF Mono',Monaco,'Cascadia Code',monospace; font-size:13px; color:#1f2937; word-break:break-all; }
.route-path { font-family:'SF Mono',Monaco,'Cascadia Code',monospace; font-size:13px; color:var(--text-primary); word-break:break-all; }
.route-path .ph { color:#7c3aed; font-style:normal; }
.route-summary-desc { color:#6b7280; font-size:12.5px; flex:1 1 100%; margin-left:64px; }
.route-summary-desc { color:var(--text-muted); font-size:12.5px; flex:1 1 100%; margin-left:64px; }
.auth-badge { font-size:10.5px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }
.auth-yes { background:#fef3c7; color:#92400e; }
.auth-no { background:#d1fae5; color:#065f46; }
.auth-yes { background:var(--warning-bg); color:var(--warning-text); }
.auth-no { background:var(--success-bg); color:var(--success-text); }
/* Detalhe expandido */
.route-detail { padding:14px 16px 16px; }
.block { margin-top:14px; }
.block:first-child { margin-top:0; }
.block-title { font-size:12px; font-weight:700; color:#6b7280; text-transform:uppercase; letter-spacing:.4px; margin-bottom:7px; display:flex; align-items:center; gap:8px; }
.block-title { font-size:12px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.4px; margin-bottom:7px; display:flex; align-items:center; gap:8px; }
.param-table { width:100%; border-collapse:collapse; font-size:13px; }
.param-table th { text-align:left; font-size:10.5px; text-transform:uppercase; color:#9ca3af; padding:4px 10px; border-bottom:1px solid #eee; font-weight:600; }
.param-table td { padding:6px 10px; border-bottom:1px solid #f3f4f6; vertical-align:top; color:#374151; }
.param-table td.pname { font-family:'SF Mono',monospace; color:#1f2937; font-weight:600; white-space:nowrap; }
.param-table th { text-align:left; font-size:10.5px; text-transform:uppercase; color:var(--text-faint); padding:4px 10px; border-bottom:1px solid var(--border); font-weight:600; }
.param-table td { padding:6px 10px; border-bottom:1px solid var(--surface-3); vertical-align:top; color:var(--text-secondary); }
.param-table td.pname { font-family:'SF Mono',monospace; color:var(--text-primary); font-weight:600; white-space:nowrap; }
.tag { font-size:10px; font-weight:700; padding:2px 7px; border-radius:12px; white-space:nowrap; }
.tag.req { background:#fee2e2; color:#b91c1c; }
.tag.opt { background:#f3f4f6; color:#6b7280; }
.tag.opt { background:var(--surface-3); color:var(--text-muted); }
.code-json { background:#0f172a; color:#e2e8f0; border-radius:8px; padding:12px 14px; font-family:'SF Mono',Monaco,'Cascadia Code',monospace; font-size:12.5px; line-height:1.55; overflow-x:auto; margin:0; white-space:pre; }
.code-curl { background:#111827; color:#a7f3d0; border-radius:8px; padding:12px 14px; font-family:'SF Mono',monospace; font-size:12px; line-height:1.5; overflow-x:auto; margin:0; white-space:pre; }
.req-note { font-size:12px; color:#6b7280; margin-top:7px; }
.req-note code { background:#f3f4f6; padding:1px 6px; border-radius:4px; color:#b91c1c; font-weight:600; }
.code-curl { background:var(--text-primary); color:#a7f3d0; border-radius:8px; padding:12px 14px; font-family:'SF Mono',monospace; font-size:12px; line-height:1.5; overflow-x:auto; margin:0; white-space:pre; }
.req-note { font-size:12px; color:var(--text-muted); margin-top:7px; }
.req-note code { background:var(--surface-3); padding:1px 6px; border-radius:4px; color:#b91c1c; font-weight:600; }
.note-box { font-size:12.5px; color:#475569; background:#f8fafc; border-left:3px solid #cbd5e1; padding:8px 12px; border-radius:0 6px 6px 0; }
.copy { padding:3px 9px; border:none; border-radius:5px; background:#e5e7eb; cursor:pointer; font-size:11px; color:#374151; }
.copy { padding:3px 9px; border:none; border-radius:5px; background:var(--border); cursor:pointer; font-size:11px; color:var(--text-secondary); }
.copy:hover { background:#d1d5db; }
.copy-light { background:rgba(255,255,255,.12); color:#e2e8f0; }
.copy-light:hover { background:rgba(255,255,255,.24); }
.empty { text-align:center; padding:50px; color:#9ca3af; font-size:14px; }
.empty { text-align:center; padding:50px; color:var(--text-faint); font-size:14px; }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div><h2>Chatc2</h2><span>API Routes</span></div>
@@ -103,18 +101,18 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" class="active admin-only" id="navRoutesActive"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a>
</div>
</aside>
<div class="main">
<main class="main" id="main-content">
<div class="topbar">
<span class="topbar-title">📡 Rotas da API</span>
<span id="totalRotas" style="font-size:13px;color:#6b7280"></span>
</div>
<div class="container">
<div class="search-box">
<input type="text" id="searchInput" placeholder="Buscar por método, path ou descrição..." oninput="filtrarRotas()">
<label for="searchInput" class="sr-only">Buscar</label><input type="text" id="searchInput" placeholder="Buscar por método, path ou descrição..." oninput="filtrarRotas()">
<select id="filterCat" onchange="filtrarRotas()"><option value="">Todas as categorias</option></select>
<select id="filterMethod" onchange="filtrarRotas()">
<option value="">Todos os métodos</option>
@@ -332,5 +330,8 @@ carregarRotas();
})();
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>
+156 -119
View File
@@ -6,36 +6,43 @@
<title>Configurações - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body { background:#f3f4f6; display:flex; min-height:100vh; }
body { background:var(--surface-3); display:flex; min-height:100vh; }
.sidebar-nav .admin-only { display:none; }
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
.container { flex:1; padding:24px; overflow-y:auto; }
.tabs { display:flex; margin-bottom:24px; background:#fff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.08); border:1px solid #f3f4f6; }
.tabs button { flex:1; padding:13px 16px; border:none; background:#fff; font-size:14px; cursor:pointer; border-bottom:2px solid transparent; transition:all .15s; color:#6b7280; font-weight:500; }
.tabs button:hover { background:#f9fafb; color:#374151; }
.tabs button.active { border-bottom-color:#667eea; color:#667eea; font-weight:700; background:#fafbff; }
.tabs { display:flex; margin-bottom:24px; background:#fff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.08); border:1px solid var(--surface-3); }
.tabs button { flex:1; padding:13px 16px; border:none; background:#fff; font-size:14px; cursor:pointer; border-bottom:2px solid transparent; transition:all .15s; color:var(--text-muted); font-weight:500; }
.tabs button:hover { background:var(--surface-2); color:var(--text-secondary); }
.tabs button.active { border-bottom-color:var(--primary); color:var(--primary); font-weight:700; background:#fafbff; }
.tab-content { display:none; }
.tab-content.active { display:block; }
.form-group { margin-bottom:16px; }
.form-group label { display:block; font-size:13px; font-weight:600; color:#374151; margin-bottom:5px; }
.form-group input, .form-group select, .form-group textarea { width:100%; padding:10px 14px; border:2px solid #e5e7eb; border-radius:8px; font-size:14px; outline:none; background:#f9fafb; color:#111827; transition:all .15s; font-family:inherit; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:#667eea; background:#fff; box-shadow:0 0 0 3px rgba(102,126,234,0.1); }
.form-group label { display:block; font-size:13px; font-weight:600; color:var(--text-secondary); margin-bottom:5px; }
.form-group input, .form-group select, .form-group textarea { width:100%; padding:10px 14px; border:2px solid var(--border); border-radius:8px; font-size:14px; outline:none; background:var(--surface-2); color:var(--text-primary); transition:all .15s; font-family:inherit; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:var(--primary); background:#fff; box-shadow:0 0 0 3px rgba(102,126,234,0.1); }
.form-group textarea { min-height:80px; resize:vertical; }
.form-group .toggle { display:flex; align-items:center; gap:8px; }
.form-group .toggle input { width:auto; }
.btn { display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border:none; border-radius:8px; font-size:14px; font-weight:600; cursor:pointer; transition:all .15s; }
.btn-primary { background:#667eea; color:#fff; }
.btn-primary:hover { background:#5a67d8; }
.btn-danger { background:#ef4444; color:#fff; }
.btn-danger:hover { background:#dc2626; }
.btn-primary { background:var(--primary); color:#fff; }
.btn-primary:hover { background:var(--primary-dark); }
.btn-danger { background:var(--danger); color:#fff; }
.btn-danger:hover { background:var(--danger-text); }
.btn-sm { padding:6px 12px; font-size:12px; border-radius:6px; }
.badge { display:inline-block; padding:2px 9px; border-radius:10px; font-size:11px; font-weight:600; }
.badge-success { background:#d1fae5; color:#065f46; }
.badge-danger { background:#fef2f2; color:#991b1b; }
.badge-success { background:var(--success-bg); color:var(--success-text); }
.badge-danger { background:var(--danger-bg); color:var(--danger-text); }
.user-info { display:flex; align-items:center; gap:14px; }
.user-name { font-size:14px; font-weight:500; color:#374151; }
.user-name { font-size:14px; font-weight:500; color:var(--text-secondary); }
.status-badge { display:inline-flex; align-items:center; gap:5px; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; }
.status-badge.online { background:#d1fae5; color:#065f46; }
.status-badge.online { background:var(--success-bg); color:var(--success-text); }
.feedback-msg {
padding: 8px 14px; border-radius: 6px; font-size: 13px;
margin-bottom: 8px; display: none;
}
.feedback-msg.sucesso { background: var(--success-bg); color: var(--success-text); display: block; }
.feedback-msg.erro { background: var(--danger-bg); color: var(--danger-text); display: block; }
/* Responsivo: abas rolam horizontalmente no celular */
@media (max-width: 768px) {
@@ -44,12 +51,10 @@ body { background:#f3f4f6; display:flex; min-height:100vh; }
}
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
window.darkModeApply=function(a){var e=document.body;if(!e)return;e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?'☀️ Claro':'🌙 Escuro'});};
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
</head>
<body>
<aside class="sidebar">
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div><h2>Chatc2</h2><span id="sidebarAlias">-</span></div>
@@ -64,10 +69,10 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<a href="#" id="navRoutesSet" class="admin-only"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a></div>
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px" aria-label="Alternar modo escuro">🌙 Escuro</button>
<a onclick="logout()" aria-label="Sair do sistema"><span class="icon">🚪</span> Sair</a></div>
</aside>
<div class="main">
<main class="main" id="main-content">
<div class="topbar"><span class="topbar-title">⚙️ Configurações</span></div>
<div class="container">
<div class="tabs">
@@ -84,17 +89,17 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0">📋 Fluxo de Atendimento</h3>
</div>
<p style="color:#6b7280;font-size:13px;margin-bottom:16px">
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px">
Configure os submenus que aparecem quando o cliente seleciona uma equipe.
As opções iniciais são sempre as <strong>Equipes</strong> cadastradas,
com a opção de <strong>Segunda via de Boleto</strong> no final.
</p>
<div id="menusList"><p style="color:#9ca3af">Carregando...</p></div>
<div id="menusList"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
<div class="card" id="cardResolucao">
<h3 style="margin:0 0 8px">🧩 Fluxo de Resolução</h3>
<p style="color:#6b7280;font-size:13px;margin-bottom:16px">
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px">
Defina os motivos de atendimento e como o atendente registra a resolução ao finalizar a conversa.
</p>
@@ -103,7 +108,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<input type="text" id="novoMotivo" placeholder="Cadastrar novo motivo..." style="flex:1;padding:8px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px" onkeydown="if(event.key==='Enter')adicionarMotivo()">
<button class="btn btn-primary btn-sm" onclick="adicionarMotivo()">+ Adicionar</button>
</div>
<div id="motivosList" style="margin-bottom:12px"><p style="color:#9ca3af;font-size:13px">Carregando...</p></div>
<div id="motivosList" style="margin-bottom:12px"><p style="color:var(--text-faint);font-size:13px">Carregando...</p></div>
<div class="form-group"><div class="toggle"><input type="checkbox" id="flgMotivoVis"> <label for="flgMotivoVis">Visualizar Motivo na tela de conversa</label></div></div>
<div class="form-group"><div class="toggle"><input type="checkbox" id="flgMotivoObr"> <label for="flgMotivoObr">Obrigatório preencher (só finaliza com motivo)</label></div></div>
@@ -111,7 +116,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<div class="form-group"><div class="toggle"><input type="checkbox" id="flgResolVis"> <label for="flgResolVis">Visualizar Resolução na tela de conversa</label></div></div>
<div class="form-group"><div class="toggle"><input type="checkbox" id="flgResolObr"> <label for="flgResolObr">Obrigatório preencher (só finaliza com resolução)</label></div></div>
<button class="btn btn-primary" onclick="salvarResolucao()">💾 Salvar Fluxo de Resolução</button>
<div id="resolucaoFeedback" class="feedback-msg"></div><button class="btn btn-primary" onclick="salvarResolucao()">💾 Salvar Fluxo de Resolução</button>
</div>
</div>
@@ -122,11 +127,11 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<h3 style="margin:0">Equipes</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalEquipe()">+ Nova Equipe</button>
</div>
<div id="equipesList"><p style="color:#9ca3af">Carregando...</p></div>
<div id="equipesList"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
<div class="card">
<h3>Usuários da Empresa</h3>
<div id="usuariosList"><p style="color:#9ca3af">Carregando...</p></div>
<div id="usuariosList"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
</div>
@@ -134,7 +139,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<div class="tab-content" id="tabEmpresa">
<div class="card">
<h3>Configurações da Empresa</h3>
<div id="configForm"><p style="color:#9ca3af">Carregando...</p></div>
<div id="configForm"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
</div>
@@ -145,7 +150,7 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<h3 style="margin:0">Etiquetas</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalEtiqueta()">+ Nova Etiqueta</button>
</div>
<div id="etiquetasList"><p style="color:#9ca3af">Carregando...</p></div>
<div id="etiquetasList"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
</div>
@@ -156,61 +161,62 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<h3 style="margin:0">📱 Conexões WhatsApp</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalConexao()">+ Nova Conexão</button>
</div>
<div id="conexoesList"><p style="color:#9ca3af">Carregando...</p></div>
<div id="conexoesList"><p style="color:var(--text-faint)">Carregando...</p></div>
</div>
</div>
</div>
</div>
<!-- Modal Equipe -->
<div class="modal-overlay" id="modalEquipe" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:400px">
<div class="modal-overlay" id="modalEquipe">
<div class="modal" style="max-width:400px">
<h3 id="modalEquipeTitle">Nova Equipe</h3>
<input type="hidden" id="editEquipeId">
<div class="form-group"><label>Nome da Equipe</label><input type="text" id="equipeNome" placeholder="Ex: Atendimento"></div>
<div class="form-group"><label>Ordem</label><input type="number" id="equipeOrdem" value="0" min="0" style="width:80px"><span style="font-size:12px;color:#9ca3af;margin-left:8px">(menor = aparece primeiro)</span></div>
<div class="form-group"><label for="equipeNome">Nome da Equipe</label><input type="text" id="equipeNome" placeholder="Ex: Atendimento"></div>
<div class="form-group"><label for="equipeOrdem">Ordem</label><input type="number" id="equipeOrdem" value="0" min="0" style="width:80px"><span style="font-size:12px;color:var(--text-faint);margin-left:8px">(menor = aparece primeiro)</span></div>
<div class="form-group"><label>Membros</label><div id="equipeMembros"></div></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalEquipe')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarEquipe()">Salvar</button>
<div id="equipeFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModal('modalEquipe')">Cancelar</button>
<button class="btn-primary" onclick="salvarEquipe()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Etiqueta -->
<div class="modal-overlay" id="modalEtiqueta" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:400px">
<div class="modal-overlay" id="modalEtiqueta">
<div class="modal" style="max-width:400px">
<h3 id="modalEtiquetaTitle">Nova Etiqueta</h3>
<input type="hidden" id="editEtiquetaId">
<div class="form-group"><label>Nome</label><input type="text" id="etiquetaNome" placeholder="Ex: Cliente VIP"></div>
<div class="form-group"><label>Cor</label><input type="color" id="etiquetaCor" value="#667eea"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalEtiqueta')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarEtiqueta()">Salvar</button>
<div class="form-group"><label for="etiquetaNome">Nome</label><input type="text" id="etiquetaNome" placeholder="Ex: Cliente VIP"></div>
<div class="form-group"><label for="etiquetaCor">Cor</label><input type="color" id="etiquetaCor" value="#667eea"></div>
<div id="etiquetaFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModal('modalEtiqueta')">Cancelar</button>
<button class="btn-primary" onclick="salvarEtiqueta()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Menu -->
<div class="modal-overlay" id="modalMenu" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:500px">
<div class="modal-overlay" id="modalMenu">
<div class="modal" style="max-width:500px">
<h3 id="modalMenuTitle">Novo Submenu</h3>
<input type="hidden" id="editMenuId">
<input type="hidden" id="editMenuEquipeId">
<input type="hidden" id="editMenuPaiId">
<div class="form-group" id="menuEquipeGroup"><label>Equipe</label><select id="menuEquipe"><option value="">Selecione...</option></select></div>
<div class="form-group"><label>Título</label><input type="text" id="menuTitulo" placeholder="Ex: Alteração Cadastral"></div>
<div class="form-group">
<label>Tipo</label>
<div class="form-group" id="menuEquipeGroup"><label for="menuEquipe">Equipe</label><select id="menuEquipe"><option value="">Selecione...</option></select></div>
<div class="form-group"><label for="menuTitulo">Título</label><input type="text" id="menuTitulo" placeholder="Ex: Alteração Cadastral"></div>
<div class="form-group"><label for="menuTipo">Tipo</label>
<select id="menuTipo" onchange="mudarTipoMenu()">
<option value="M">📂 Submenu (mostra mais opções)</option>
<option value="T">💬 Texto (envia mensagem fixa)</option>
<option value="R">⚙️ Ação (executa alteração via API)</option>
</select>
</div>
<div class="form-group" id="menuTextoGroup" style="display:none"><label>Texto a ser enviado</label><textarea id="menuTexto" rows="3" placeholder="Digite a mensagem..."></textarea></div>
<div class="form-group" id="menuTextoGroup" style="display:none"><label for="menuTexto">Texto a ser enviado</label><textarea id="menuTexto" rows="3" placeholder="Digite a mensagem..."></textarea></div>
<div class="form-group" id="menuRotaGroup" style="display:none">
<label>Ação (Rota)</label>
<label for="menuAcaoRota">Ação (Rota)</label>
<select id="menuAcaoRota">
<option value="">Selecione uma ação...</option>
<option value="listar_carnes">📄 Listar carnês / Boleto</option>
@@ -219,31 +225,33 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<option value="alterar_endereco">🏠 Alterar Endereço</option>
<option value="info_cliente">️ Mostrar dados do cliente</option>
</select>
<div style="margin-top:8px"><label>Pergunta ao cliente (se precisar de dados)</label>
<textarea id="menuAcaoPrompt" rows="2" placeholder="Ex: Informe seu novo e-mail:" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical"></textarea></div>
<div style="margin-top:8px"><label for="menuAcaoPrompt">Pergunta ao cliente (se precisar de dados)</label>
<textarea id="menuAcaoPrompt" rows="2" placeholder="Ex: Informe seu novo e-mail:"></textarea></div>
</div>
<div class="form-group"><label>Ordem</label><input type="number" id="menuOrdem" value="0" min="0" style="width:80px"></div>
<div class="form-group"><label>Subordinado a (opcional)</label><select id="menuPai"><option value="">Nenhum (raiz da equipe)</option></select></div>
<div class="form-group"><label>Etiquetas (adicionar ao selecionar)</label><div id="menuEtiquetasContainer" style="display:flex;flex-wrap:wrap;gap:6px;padding:6px;border:1px solid #e5e7eb;border-radius:8px;min-height:36px"></div></div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
<button class="btn" onclick="fecharModal('modalMenu')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarMenu()">Salvar</button>
<div class="form-group"><label for="menuOrdem">Ordem</label><input type="number" id="menuOrdem" value="0" min="0" style="width:80px"></div>
<div class="form-group"><label for="menuPai">Subordinado a (opcional)</label><select id="menuPai"><option value="">Nenhum (raiz da equipe)</option></select></div>
<div class="form-group"><label>Etiquetas (adicionar ao selecionar)</label><div id="menuEtiquetasContainer" style="display:flex;flex-wrap:wrap;gap:6px;padding:6px;border:1px solid var(--border);border-radius:8px;min-height:36px"></div></div>
<div id="menuFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModal('modalMenu')">Cancelar</button>
<button class="btn-primary" onclick="salvarMenu()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Conexão -->
<div class="modal-overlay" id="modalConexao" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:500px">
<div class="modal-overlay" id="modalConexao">
<div class="modal" style="max-width:500px">
<h3 id="modalConexaoTitle">Nova Conexão WhatsApp</h3>
<input type="hidden" id="editConexaoId">
<div class="form-group"><label>Nome da Instância</label><input type="text" id="conNome" placeholder="Ex: WhatsApp Comercial"></div>
<div class="form-group"><label>URL Evolution API</label><input type="text" id="conUrl" value="https://evoatende.c2sistemas.com.br" placeholder="https://evoatende.c2sistemas.com.br"></div>
<div class="form-group"><label>API Key</label><input type="text" id="conApiKey" placeholder="API Key"></div>
<div class="form-group"><label>Nome da Instância (Evolution)</label><input type="text" id="conInstance" placeholder="Ex: empresa1-wpp"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalConexao')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarConexao()" id="btnSalvarConexao">Conectar</button>
<div class="form-group"><label for="conNome">Nome da Instância</label><input type="text" id="conNome" placeholder="Ex: WhatsApp Comercial"></div>
<div class="form-group"><label for="conUrl">URL Evolution API</label><input type="text" id="conUrl" value="https://evoatende.c2sistemas.com.br" placeholder="https://evoatende.c2sistemas.com.br"></div>
<div class="form-group"><label for="conApiKey">API Key</label><input type="text" id="conApiKey" placeholder="API Key"></div>
<div class="form-group"><label for="conInstance">Nome da Instância (Evolution)</label><input type="text" id="conInstance" placeholder="Ex: empresa1-wpp"></div>
<div id="conexaoFeedback" class="feedback-msg"></div>
<div class="modal-footer">
<button class="btn-secondary" onclick="fecharModal('modalConexao')">Cancelar</button>
<button class="btn-primary" onclick="salvarConexao()" id="btnSalvarConexao">Conectar</button>
</div>
</div>
</div>
@@ -251,6 +259,16 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
<script>
(function(){
'use strict';
// Fechar modais com Esc
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.show, .modal.show').forEach(function(m) {
m.classList.remove('show');
});
}
});
const token = localStorage.getItem('chatc2_token');
const pathParts = window.location.pathname.split('/');
const alias = pathParts[2] || localStorage.getItem('chatc2_alias') || 'lajedo';
@@ -298,14 +316,14 @@ async function carregarEquipes() {
var data = await api('/teams?empresaId=' + empresaId);
var div = document.getElementById('equipesList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma equipe cadastrada</p>';
div.innerHTML = '<p style="color:var(--text-faint)">Nenhuma equipe cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Ordem</th><th>Nome</th><th>Membros</th><th>Ações</th></tr></thead><tbody>' +
data.data.map(function(eq) {
var membros = (eq.membros || []).map(function(m) { return m.nome; }).join(', ') || '-';
return '<tr><td>' + (eq.ordem || 0) + '</td><td><strong>' + eq.nome + '</strong></td><td>' + membros + '</td><td>' +
'<button class="btn btn-sm" onclick="editarEquipe(' + eq.id + ',' + (eq.ordem || 0) + ',\'' + eq.nome.replace(/'/g,"\\'") + '\')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm" onclick="editarEquipe(' + eq.id + ',' + (eq.ordem || 0) + ',\'' + eq.nome.replace(/'/g,"\\'") + '\')" style="background:var(--surface-3);margin-right:4px">✏️</button>' +
'<button class="btn btn-sm btn-danger" onclick="excluirEquipe(' + eq.id + ')">🗑️</button></td></tr>';
}).join('') + '</tbody></table>';
}
@@ -316,7 +334,7 @@ window.mostrarModalEquipe = function() {
document.getElementById('equipeNome').value = '';
document.getElementById('modalEquipeTitle').textContent = 'Nova Equipe';
carregarUsuariosCheckbox();
document.getElementById('modalEquipe').style.display = 'flex';
document.getElementById('modalEquipe').classList.add('show');
};
window.editarEquipe = function(id, ordem, nome) {
@@ -325,13 +343,13 @@ window.editarEquipe = function(id, ordem, nome) {
document.getElementById('equipeNome').value = nome;
document.getElementById('modalEquipeTitle').textContent = 'Editar Equipe';
carregarUsuariosCheckbox(id);
document.getElementById('modalEquipe').style.display = 'flex';
document.getElementById('modalEquipe').classList.add('show');
};
async function carregarUsuariosCheckbox(equipeId) {
var div = document.getElementById('equipeMembros');
var data = await api('/company/users?empresaId=' + empresaId);
if (!data.success) { div.innerHTML = '<p style="color:#9ca3af">Erro ao carregar</p>'; return; }
if (!data.success) { div.innerHTML = '<p style="color:var(--text-faint)">Erro ao carregar</p>'; return; }
var membrosAtuais = [];
if (equipeId) {
@@ -352,7 +370,7 @@ window.salvarEquipe = async function() {
var ordem = parseInt(document.getElementById('equipeOrdem').value) || 0;
var membros = Array.from(document.querySelectorAll('#equipeMembros input:checked')).map(function(cb) { return parseInt(cb.value); });
if (!nome) return alert('Informe o nome da equipe');
if (!nome) { mostrarFeedbackModal('equipeFeedback', 'Informe o nome da equipe', 'erro'); return; }
if (id) {
var r = await api('/teams/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros }) });
@@ -374,7 +392,7 @@ async function carregarUsuarios() {
var data = await api('/company/users?empresaId=' + empresaId);
var div = document.getElementById('usuariosList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhum usuário encontrado</p>';
div.innerHTML = '<p style="color:var(--text-faint)">Nenhum usuário encontrado</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Login</th><th>Tipo Chat</th></tr></thead><tbody>' +
@@ -393,7 +411,7 @@ async function carregarMenus() {
var div = document.getElementById('menusList');
if (!eqData.success || !eqData.data || eqData.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Crie equipes primeiro para configurar os submenus.</p>';
div.innerHTML = '<p style="color:var(--text-faint)">Crie equipes primeiro para configurar os submenus.</p>';
return;
}
@@ -404,8 +422,8 @@ async function carregarMenus() {
var menusEq = menusAgrupados.find(function(g) { return g.equipeId === eq.id; });
var temMenus = menusEq && menusEq.menus && menusEq.menus.length > 0;
html += '<div style="margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">';
html += '<div style="background:#f9fafb;padding:10px 14px;font-weight:600;font-size:14px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between">';
html += '<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:8px;overflow:hidden">';
html += '<div style="background:var(--surface-2);padding:10px 14px;font-weight:600;font-size:14px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between">';
html += '<span>👥 ' + eq.nome + '</span>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + eq.id + ')" style="background:#e0e7ff">+ Submenu</button>';
html += '</div>';
@@ -413,7 +431,7 @@ async function carregarMenus() {
if (temMenus) {
html += '<div style="padding:4px 0">' + montarArvoreHtml(menusEq.menus, 0) + '</div>';
} else {
html += '<p style="padding:12px 14px;color:#9ca3af;font-size:13px;margin:0">Nenhum submenu configurado.</p>';
html += '<p style="padding:12px 14px;color:var(--text-faint);font-size:13px;margin:0">Nenhum submenu configurado.</p>';
}
html += '</div>';
});
@@ -427,8 +445,8 @@ function montarArvoreHtml(menus, nivel) {
menus.forEach(function(m) {
var tipoIcon = m.tipo === 'M' ? '📂' : m.tipo === 'T' ? '💬' : m.tipo === 'R' ? '⚙️' : '❓';
html += '<div style="display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid #f3f4f6">';
html += '<span style="flex:1;font-size:13px">' + tipoIcon + ' ' + m.titulo + ' <span style="color:#9ca3af;font-size:11px">(' + m.tipo + ')</span></span>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + m.equipeId + ',' + m.id + ')" style="background:#f3f4f6;margin-right:4px;padding:4px 8px;font-size:11px">✏️</button>';
html += '<span style="flex:1;font-size:13px">' + tipoIcon + ' ' + m.titulo + ' <span style="color:var(--text-faint);font-size:11px">(' + m.tipo + ')</span></span>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + m.equipeId + ',' + m.id + ')" style="background:var(--surface-3);margin-right:4px;padding:4px 8px;font-size:11px">✏️</button>';
html += '<button class="btn btn-sm" onclick="excluirMenu(' + m.id + ',\'' + m.titulo.replace(/'/g,"\\'") + '\')" style="background:#fef2f2;color:#991b1b;padding:4px 8px;font-size:11px">🗑️</button>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + m.equipeId + ',null,' + m.id + ')" style="margin-left:4px;padding:4px 8px;font-size:11px"> Sub</button>';
html += '</div>';
@@ -545,7 +563,7 @@ window.mostrarModalMenu = async function(equipeId, editMenuId, parentMenuId) {
}
}
document.getElementById('modalMenu').style.display = 'flex';
document.getElementById('modalMenu').classList.add('show');
};
window.mudarTipoMenu = function() {
@@ -575,12 +593,12 @@ async function buscarEtiquetasDoMenu(menuId) {
function renderEtiquetasCheckbox(data, selecionadas) {
var container = document.getElementById('menuEtiquetasContainer');
if (!data.success || !data.data || data.data.length === 0) {
container.innerHTML = '<span style="color:#9ca3af;font-size:12px">Nenhuma etiqueta cadastrada</span>';
container.innerHTML = '<span style="color:var(--text-faint);font-size:12px">Nenhuma etiqueta cadastrada</span>';
return;
}
container.innerHTML = data.data.map(function(e) {
var checked = selecionadas.includes(e.id) ? 'checked' : '';
return '<label style="display:flex;align-items:center;gap:4px;padding:4px 8px;background:#f3f4f6;border-radius:6px;font-size:12px;cursor:pointer">' +
return '<label style="display:flex;align-items:center;gap:4px;padding:4px 8px;background:var(--surface-3);border-radius:6px;font-size:12px;cursor:pointer">' +
'<input type="checkbox" value="' + e.id + '" ' + checked + ' style="accent-color:#667eea"> ' +
'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + (e.cor || '#667eea') + '"></span> ' +
e.nome +
@@ -601,8 +619,8 @@ window.salvarMenu = async function() {
// Etiquetas selecionadas
var etiquetaIds = Array.from(document.querySelectorAll('#menuEtiquetasContainer input[type="checkbox"]:checked')).map(function(cb) { return parseInt(cb.value); }).filter(function(v) { return !isNaN(v); }).join(',');
if (!titulo) return alert('Informe o título');
if (!equipeId) return alert('Selecione a equipe');
if (!titulo) { mostrarFeedbackModal('menuFeedback', 'Informe o título', 'erro'); return; }
if (!equipeId) { mostrarFeedbackModal('menuFeedback', 'Selecione a equipe', 'erro'); return; }
var body = {
empresaId: empresaId,
@@ -622,11 +640,11 @@ window.salvarMenu = async function() {
var r = await api('/menus/' + editId, { method: 'PUT', body: JSON.stringify(body) });
console.log('[Menu] Resposta save:', JSON.stringify(r));
if (r.success) { fecharModal('modalMenu'); carregarMenus(); }
else alert('Erro: ' + (r.error || 'desconhecido'));
else mostrarFeedbackModal('menuFeedback', 'Erro: ' + (r.error || 'desconhecido'), 'erro');
} else {
var r = await api('/menus', { method: 'POST', body: JSON.stringify(body) });
if (r.success) { fecharModal('modalMenu'); carregarMenus(); }
else alert('Erro: ' + (r.error || 'desconhecido'));
else mostrarFeedbackModal('menuFeedback', 'Erro: ' + (r.error || 'desconhecido'), 'erro');
}
};
@@ -641,13 +659,13 @@ async function carregarEtiquetas() {
var data = await api('/labels?empresaId=' + empresaId);
var div = document.getElementById('etiquetasList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma etiqueta cadastrada</p>';
div.innerHTML = '<p style="color:var(--text-faint)">Nenhuma etiqueta cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Cor</th><th>Ações</th></tr></thead><tbody>' +
data.data.map(function(l) {
return '<tr><td><strong>' + l.nome + '</strong></td><td><span class="tag" style="background:' + l.cor + '">' + l.cor + '</span></td><td>' +
'<button class="btn btn-sm" onclick="editarEtiqueta(' + l.id + ',\'' + l.nome.replace(/'/g,"\\'") + '\',\'' + l.cor + '\')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm" onclick="editarEtiqueta(' + l.id + ',\'' + l.nome.replace(/'/g,"\\'") + '\',\'' + l.cor + '\')" style="background:var(--surface-3);margin-right:4px">✏️</button>' +
'<button class="btn btn-sm btn-danger" onclick="excluirEtiqueta(' + l.id + ')">🗑️</button></td></tr>';
}).join('') + '</tbody></table>';
}
@@ -657,7 +675,7 @@ window.mostrarModalEtiqueta = function() {
document.getElementById('etiquetaNome').value = '';
document.getElementById('etiquetaCor').value = '#667eea';
document.getElementById('modalEtiquetaTitle').textContent = 'Nova Etiqueta';
document.getElementById('modalEtiqueta').style.display = 'flex';
document.getElementById('modalEtiqueta').classList.add('show');
};
window.editarEtiqueta = function(id, nome, cor) {
@@ -665,14 +683,14 @@ window.editarEtiqueta = function(id, nome, cor) {
document.getElementById('etiquetaNome').value = nome;
document.getElementById('etiquetaCor').value = cor;
document.getElementById('modalEtiquetaTitle').textContent = 'Editar Etiqueta';
document.getElementById('modalEtiqueta').style.display = 'flex';
document.getElementById('modalEtiqueta').classList.add('show');
};
window.salvarEtiqueta = async function() {
var id = document.getElementById('editEtiquetaId').value;
var nome = document.getElementById('etiquetaNome').value.trim();
var cor = document.getElementById('etiquetaCor').value;
if (!nome) return alert('Informe o nome');
if (!nome) { mostrarFeedbackModal('etiquetaFeedback', 'Informe o nome', 'erro'); return; }
if (id) {
var r = await api('/labels/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, cor: cor }) });
@@ -704,14 +722,14 @@ async function carregarConfig() {
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgNomeUser" ' + (cfg.enviarNomeUsuario === 'S' ? 'checked' : '') + '> <label for="cfgNomeUser">Mostrar nome do usuário nas mensagens ("Nome: Mensagem")</label></div></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgEnviarBoleto" ' + (cfg.enviarBoleto === 'S' ? 'checked' : '') + '> <label for="cfgEnviarBoleto">💳 Permitir enviar boleto na conversa (botão "Enviar boleto")</label></div></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgTriagem" ' + (cfg.triagemAtiva === 'S' ? 'checked' : '') + '> <label for="cfgTriagem">📋 Ativar fluxo de triagem (menu de opções)</label></div></div>' +
'<div class="form-group" id="triagemConfig" style="display:' + (cfg.triagemAtiva === 'S' ? 'block' : 'none') + ';padding:12px;background:#f9fafb;border-radius:8px;margin-bottom:12px">' +
'<div class="form-group" id="triagemConfig" style="display:' + (cfg.triagemAtiva === 'S' ? 'block' : 'none') + ';padding:12px;background:var(--surface-2);border-radius:8px;margin-bottom:12px">' +
'<div class="form-group"><label>Mensagem de boas-vindas (use {EMPRESA} para o nome)</label><textarea id="cfgTriagemWelcome" rows="2" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical">' + (cfg.triagemMsgWelcome || '') + '</textarea></div>' +
'<div class="form-group"><label>Mensagem após escolha</label><textarea id="cfgTriagemAfter" rows="2" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical">' + (cfg.triagemMsgAfter || '') + '</textarea></div>' +
'<div class="form-group"><label>Número da opção "Segunda via de Boleto"</label><input type="text" id="cfgTriagemBoletoNum" value="' + (cfg.triagemBoletoNumero || '0') + '" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px"></div>' +
'</div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgCsat" ' + (cfg.csatAtivo === 'S' ? 'checked' : '') + '> <label for="cfgCsat">Ativar CSAT (Pesquisa de satisfação)</label></div></div>' +
'<div class="form-group"><label>Mensagem CSAT</label><textarea id="cfgCsatMsg">' + (cfg.csatMensagem || '') + '</textarea></div>' +
'<button class="btn btn-primary" onclick="salvarConfig()">💾 Salvar Configurações</button>';
'<div id="configFeedback" class="feedback-msg"></div><button class="btn btn-primary" onclick="salvarConfig()">💾 Salvar Configurações</button>';
// Evento para toggle da triagem
document.getElementById('cfgTriagem').addEventListener('change', function() {
@@ -750,7 +768,7 @@ window.salvarConfig = async function() {
};
var r = await api('/company/config', { method: 'POST', body: JSON.stringify(data) });
if (r.success) alert('Configurações salvas com sucesso!');
if (r.success) mostrarFeedbackModal('configFeedback', 'Configurações salvas com sucesso!', 'sucesso');
};
// O addEventListener do cfgTriagem é adicionado dentro do carregarConfig() após criar o HTML
@@ -764,11 +782,11 @@ async function carregarResolucao() {
if (div && md.success) {
div.innerHTML = (md.data && md.data.length)
? md.data.map(function(m) {
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border:1px solid #eee;border-radius:6px;margin-bottom:4px">' +
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border:1px solid var(--border);border-radius:6px;margin-bottom:4px">' +
'<span style="font-size:13px">' + escc(m.descricao) + '</span>' +
'<button class="btn btn-sm" style="color:#dc2626;background:none;border:none;cursor:pointer" onclick="removerMotivo(' + m.id + ')">🗑️</button></div>';
}).join('')
: '<p style="color:#9ca3af;font-size:13px">Nenhum motivo cadastrado.</p>';
: '<p style="color:var(--text-faint);font-size:13px">Nenhum motivo cadastrado.</p>';
}
var cd = await api('/company/config?empresaId=' + empresaId);
if (cd.success) {
@@ -785,15 +803,23 @@ window.adicionarMotivo = async function() {
var d = (inp.value || '').trim();
if (!d) return;
var r = await api('/motivos', { method: 'POST', body: JSON.stringify({ descricao: d, empresaId: empresaId }) });
if (r.success) { inp.value = ''; carregarResolucao(); } else alert(r.error || 'Erro ao adicionar motivo');
if (r.success) { inp.value = ''; carregarResolucao(); } else mostrarFeedbackResolucao(r.error || 'Erro ao adicionar motivo', 'erro');
};
window.removerMotivo = async function(id) {
if (!confirm('Remover este motivo?')) return;
var r = await api('/motivos/' + id, { method: 'DELETE' });
if (r.success) carregarResolucao(); else alert(r.error || 'Erro ao remover');
if (r.success) carregarResolucao(); else mostrarFeedbackResolucao(r.error || 'Erro ao remover', 'erro');
};
function mostrarFeedbackResolucao(msg, tipo) {
var fb = document.getElementById('resolucaoFeedback');
if (!fb) return;
fb.textContent = msg;
fb.className = 'feedback-msg ' + tipo;
setTimeout(function() { fb.className = 'feedback-msg'; }, 4000);
}
window.salvarResolucao = async function() {
var body = {
empresaId: empresaId,
@@ -803,7 +829,7 @@ window.salvarResolucao = async function() {
resolucaoObrigatorio: document.getElementById('flgResolObr').checked ? 'S' : 'N',
};
var r = await api('/company/resolucao-config', { method: 'POST', body: JSON.stringify(body) });
if (r.success) alert('Fluxo de Resolução salvo!'); else alert(r.error || 'Erro ao salvar');
if (r.success) mostrarFeedbackModal('resolucaoFeedback', 'Fluxo de Resolução salvo!', 'sucesso'); else mostrarFeedbackModal('resolucaoFeedback', r.error || 'Erro ao salvar', 'erro');
};
// ===== CONEXÕES =====
@@ -811,7 +837,7 @@ async function carregarConexoes() {
var data = await api('/evolution/instances?empresaId=' + empresaId);
var div = document.getElementById('conexoesList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma conexão cadastrada</p>';
div.innerHTML = '<p style="color:var(--text-faint)">Nenhuma conexão cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Instância</th><th>URL</th><th>Status</th><th>Ações</th></tr></thead><tbody>' +
@@ -821,9 +847,9 @@ async function carregarConexoes() {
ins.status === 'D' ? '<span class="badge badge-danger">⛔ Desconectado</span>' : '<span class="badge badge-danger">❌ ' + ins.status + '</span>';
var nomeEsc = (ins.nome || '').replace(/'/g, "\\'");
return '<tr><td><strong>' + ins.nome + '</strong></td><td>' + ins.instanceName + '</td><td style="font-size:11px;max-width:150px;overflow:hidden;text-overflow:ellipsis">' + ins.url + '</td><td>' + status + '</td><td>' +
'<button class="btn btn-sm" onclick="editarConexao(' + ins.id + ')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm" onclick="editarConexao(' + ins.id + ')" style="background:var(--surface-3);margin-right:4px">✏️</button>' +
'<button class="btn btn-sm" onclick="excluirConexao(' + ins.id + ',\'' + nomeEsc + '\')" style="background:#fef2f2;color:#991b1b;margin-right:4px">🗑️</button>' +
'<button class="btn btn-sm" onclick="gerarQRCode(' + ins.id + ')" style="background:#f3f4f6">📱 QR Code</button></td></tr>';
'<button class="btn btn-sm" onclick="gerarQRCode(' + ins.id + ')" style="background:var(--surface-3)">📱 QR Code</button></td></tr>';
}).join('') + '</tbody></table>';
}
@@ -835,7 +861,7 @@ window.mostrarModalConexao = function() {
document.getElementById('conInstance').value = '';
document.getElementById('modalConexaoTitle').textContent = 'Nova Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Conectar';
document.getElementById('modalConexao').style.display = 'flex';
document.getElementById('modalConexao').classList.add('show');
};
window.gerarQRCode = async function(id) {
@@ -847,9 +873,9 @@ window.gerarQRCode = async function(id) {
w.document.write('<html><head><title>QR Code</title></head><body style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">' +
'<h2 style="margin-bottom:20px">Escaneie o QR Code</h2>' +
'<img src="' + imgSrc + '" style="max-width:300px">' +
'<p style="margin-top:20px;color:#6b7280">Abra o WhatsApp no celular e escaneie</p></body></html>');
'<p style="margin-top:20px;color:var(--text-muted)">Abra o WhatsApp no celular e escaneie</p></body></html>');
} else {
alert('Erro ao gerar QR Code: ' + JSON.stringify(r.data));
mostrarFeedbackModal('conexaoFeedback', 'Erro ao gerar QR Code', 'erro');
}
};
@@ -857,9 +883,9 @@ window.gerarQRCode = async function(id) {
window.editarConexao = async function(id) {
// Busca dados atuais da API
var data = await api('/evolution/instances?empresaId=' + empresaId);
if (!data.success) return alert('Erro ao carregar dados');
if (!data.success) { mostrarFeedbackModal('conexaoFeedback', 'Erro ao carregar dados', 'erro'); return; }
var ins = data.data.find(function(i) { return i.id === id; });
if (!ins) return alert('Instância não encontrada');
if (!ins) { mostrarFeedbackModal('conexaoFeedback', 'Instância não encontrada', 'erro'); return; }
document.getElementById('editConexaoId').value = id;
document.getElementById('conNome').value = ins.nome;
@@ -868,7 +894,7 @@ window.editarConexao = async function(id) {
document.getElementById('conInstance').value = ins.instanceName;
document.getElementById('modalConexaoTitle').textContent = 'Editar Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Salvar';
document.getElementById('modalConexao').style.display = 'flex';
document.getElementById('modalConexao').classList.add('show');
};
// ===== EXCLUIR CONEXÃO =====
@@ -878,7 +904,7 @@ window.excluirConexao = async function(id, nome) {
if (r.success) {
carregarConexoes();
} else {
alert('Erro: ' + r.error);
mostrarFeedbackModal('conexaoFeedback', 'Erro: ' + r.error, 'erro');
}
};
@@ -892,7 +918,7 @@ window.salvarConexao = async function() {
};
if (!data.INS_NOME || !data.INS_URL || !data.INS_API_KEY || !data.INS_INSTANCE_NAME)
return alert('Preencha todos os campos');
mostrarFeedbackModal('conexaoFeedback', 'Preencha todos os campos', 'erro'); return;
var r;
if (editId) {
@@ -912,16 +938,24 @@ window.salvarConexao = async function() {
carregarConexoes();
if (!editId && r.data && r.data.id) gerarQRCode(r.data.id);
} else {
alert('Erro: ' + r.error);
mostrarFeedbackModal('conexaoFeedback', 'Erro: ' + r.error, 'erro');
}
};
// ===== MODAIS =====
window.fecharModal = function(id) { document.getElementById(id).style.display = 'none'; };
window.fecharModal = function(id) { document.getElementById(id).classList.remove('show'); };
function mostrarFeedbackModal(elId, msg, tipo) {
var fb = document.getElementById(elId);
if (!fb) return;
fb.textContent = msg;
fb.className = 'feedback-msg ' + tipo;
setTimeout(function() { fb.className = 'feedback-msg'; }, 4000);
}
// Fechar modal ao clicar fora
document.querySelectorAll('.modal-overlay').forEach(function(el) {
el.addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
el.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('show'); });
});
// ===== INICIAR =====
@@ -936,5 +970,8 @@ carregarResolucao();
})();
</script>
<script src="/js/dark-mode.js"></script>
<!-- impeccable-live-start -->
<script src="http://localhost:8400/live.js"></script>
<!-- impeccable-live-end -->
</body>
</html>