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
+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>