adição de agendamento de conversa
This commit is contained in:
@@ -92,6 +92,27 @@ const MIGRACOES = [
|
||||
'ALTER TABLE CHATC2_EQUIPES ADD EQU_ETIQUETA_ID INTEGER',
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MIGRAÇÃO 23: Agendamento de conversas
|
||||
// ----------------------------------------------------------
|
||||
{
|
||||
id: 23,
|
||||
descricao: 'Criar tabela CHATC2_CONVERSAS_AGENDAMENTOS (retorno agendado)',
|
||||
sql: [
|
||||
`CREATE TABLE CHATC2_CONVERSAS_AGENDAMENTOS (
|
||||
CAG_CODIGO_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
CAG_CONVERSA_ID INTEGER NOT NULL,
|
||||
CAG_EMPRESA_ID INTEGER,
|
||||
CAG_DATA DATE NOT NULL,
|
||||
CAG_HORA TIME,
|
||||
CAG_NOTA VARCHAR(300),
|
||||
CAG_STATUS CHAR(1) DEFAULT 'P',
|
||||
CAG_USUARIO_ID INTEGER,
|
||||
CAG_DT_CRIACAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -1569,6 +1569,146 @@ class ChatController {
|
||||
return texto; // fallback: retorna sem substituir
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AGENDAMENTOS ====================
|
||||
|
||||
/**
|
||||
* Agenda retorno para uma conversa.
|
||||
* POST /api/:alias/conversations/:id/schedule
|
||||
*/
|
||||
static async scheduleConversation(req, res) {
|
||||
try {
|
||||
const { alias, id } = req.params;
|
||||
const { data, hora, nota } = req.body;
|
||||
|
||||
if (!data) return res.status(400).json({ success: false, error: 'Data é obrigatória.' });
|
||||
|
||||
const chk = await ChatController.checarConversaEmpresa(alias, id, req);
|
||||
if (chk.status) return res.status(chk.status).json({ success: false, error: 'Conversa não encontrada ou sem permissão.' });
|
||||
|
||||
const newId = await db.nextId(alias, 'GEN_CHATC2_AGENDAMENTOS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_AGENDAMENTOS (CAG_CODIGO_ID, CAG_CONVERSA_ID, CAG_EMPRESA_ID, CAG_DATA, CAG_HORA, CAG_NOTA, CAG_USUARIO_ID)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, [newId, id, chk.empresaId, data, hora || null, nota || null, req.user?.id]);
|
||||
|
||||
res.json({ success: true, data: { id: newId } });
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista agendamentos de uma data (ou hoje).
|
||||
* GET /api/:alias/schedules?data=2026-12-25&empresaId=
|
||||
*/
|
||||
static async listSchedules(req, res) {
|
||||
try {
|
||||
const { alias } = req.params;
|
||||
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
|
||||
const data = req.query.data || new Date().toISOString().split('T')[0];
|
||||
|
||||
if (!req.user?.empresas?.some(e => Number(e) === Number(empresaId))) {
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
}
|
||||
|
||||
const result = await db.query(alias, `
|
||||
SELECT a.*, c.CON_NUMERO, c.CON_NOME_CONTATO, c.CON_CLIENTE_ID
|
||||
FROM CHATC2_CONVERSAS_AGENDAMENTOS a
|
||||
JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = a.CAG_CONVERSA_ID
|
||||
WHERE a.CAG_EMPRESA_ID = ? AND a.CAG_DATA = ? AND a.CAG_STATUS = 'P'
|
||||
ORDER BY a.CAG_HORA NULLS LAST, a.CAG_DT_CRIACAO
|
||||
`, [empresaId, data]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.map(r => ({
|
||||
id: r.CAG_CODIGO_ID,
|
||||
conversaId: r.CAG_CONVERSA_ID,
|
||||
data: r.CAG_DATA,
|
||||
hora: r.CAG_HORA,
|
||||
nota: (r.CAG_NOTA || '').trim(),
|
||||
numero: (r.CON_NUMERO || '').trim(),
|
||||
nomeContato: (r.CON_NOME_CONTATO || '').trim(),
|
||||
clienteId: r.CON_CLIENTE_ID,
|
||||
status: (r.CAG_STATUS || '').trim(),
|
||||
})),
|
||||
count: result.length,
|
||||
});
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria nova conversa a partir de um agendamento.
|
||||
* POST /api/:alias/schedules/:id/reopen
|
||||
* Retorna a nova conversa e o texto sugerido — NÃO envia automaticamente.
|
||||
*/
|
||||
static async reopenSchedule(req, res) {
|
||||
try {
|
||||
const { alias, id } = req.params;
|
||||
|
||||
const ag = await db.query(alias,
|
||||
`SELECT a.*, c.CON_NUMERO, c.CON_NOME_CONTATO, c.CON_CLIENTE_ID, c.CON_EMPRESA_ID, c.CON_INSTANCIA_ID
|
||||
FROM CHATC2_CONVERSAS_AGENDAMENTOS a
|
||||
JOIN CHATC2_CONVERSAS c ON c.CON_CODIGO_ID = a.CAG_CONVERSA_ID
|
||||
WHERE a.CAG_CODIGO_ID = ?`, [id]);
|
||||
if (ag.length === 0) return res.status(404).json({ success: false, error: 'Agendamento não encontrado.' });
|
||||
|
||||
const a = ag[0];
|
||||
const numeroLimpo = (a.CON_NUMERO || '').replace(/\D/g, '').substring(0, 15);
|
||||
const nomeContato = (a.CON_NOME_CONTATO || a.CON_NUMERO || '').trim();
|
||||
const nota = (a.CAG_NOTA || '').trim();
|
||||
let clienteId = a.CON_CLIENTE_ID || null;
|
||||
|
||||
// Se não tem cliente vinculado, busca pelo número
|
||||
if (!clienteId) {
|
||||
const variantes = [numeroLimpo];
|
||||
if (numeroLimpo.startsWith('55')) variantes.push(numeroLimpo.substring(2));
|
||||
variantes.push(numeroLimpo.slice(-8));
|
||||
|
||||
for (const termo of variantes) {
|
||||
const cli = await db.query(alias, `
|
||||
SELECT CLI_CODIGO_ID, CLI_NOME FROM CLIENTES
|
||||
WHERE CLI_EMPRESA_ID = ?
|
||||
AND (REPLACE(REPLACE(REPLACE(COALESCE(CLI_CELULAR,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
||||
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE1,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
||||
OR REPLACE(REPLACE(REPLACE(COALESCE(CLI_FONE2,''),'-',''),'(',''),')','') LIKE '%' || ? || '%')
|
||||
FETCH FIRST 1 ROWS ONLY
|
||||
`, [a.CAG_EMPRESA_ID, termo, termo, termo]);
|
||||
if (cli.length > 0) { clienteId = cli[0].CLI_CODIGO_ID; break; }
|
||||
}
|
||||
|
||||
if (!clienteId) {
|
||||
for (const termo of variantes) {
|
||||
const dep = await db.query(alias, `
|
||||
SELECT DEPC_CLIENTE_ID FROM DEPENDENTES_CLI
|
||||
WHERE DEPC_EMPRESA_ID = ? AND DEPC_SITUACAO = 'A'
|
||||
AND REPLACE(REPLACE(REPLACE(COALESCE(DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'
|
||||
FETCH FIRST 1 ROWS ONLY
|
||||
`, [a.CAG_EMPRESA_ID, termo]);
|
||||
if (dep.length > 0) { clienteId = dep[0].DEPC_CLIENTE_ID; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cria NOVA conversa
|
||||
const newId = await db.nextId(alias, 'GEN_CHATC2_CONVERSAS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
|
||||
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, [newId, a.CAG_EMPRESA_ID, a.CON_INSTANCIA_ID, numeroLimpo, nomeContato, clienteId]);
|
||||
|
||||
// Monta texto sugerido (placeholders serão processados no frontend ao enviar)
|
||||
const textoSugerido = nota
|
||||
? '📅 *Retorno agendado*\n' + nota + '\n\nOlá [CLIENTE]! Conforme combinado, estou retornando o contato. Em que posso ajudar?'
|
||||
: '👋 Olá [CLIENTE]! Conforme combinado, estou retornando o contato. Em que posso ajudar?';
|
||||
|
||||
// Marca agendamento como retomado
|
||||
await db.execute(alias,
|
||||
"UPDATE CHATC2_CONVERSAS_AGENDAMENTOS SET CAG_STATUS = 'R' WHERE CAG_CODIGO_ID = ?", [id]);
|
||||
|
||||
res.json({ success: true, conversaId: newId, textoSugerido: textoSugerido });
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: normaliza número para formato WhatsApp (DDI + 9º dígito)
|
||||
|
||||
@@ -97,6 +97,7 @@ const GENERATORS = [
|
||||
'GEN_CHATC2_MENUS_EMPRESA',
|
||||
'GEN_CHATC2_CSAT_AVALIACOES',
|
||||
'GEN_CHATC2_MOTIVOS_ATENDIMENTO',
|
||||
'GEN_CHATC2_AGENDAMENTOS',
|
||||
];
|
||||
|
||||
async function nextId(alias, generatorName) {
|
||||
|
||||
+162
-2
@@ -825,9 +825,31 @@ body.dark-mode .msg.enviando { opacity: 0.4; }
|
||||
|
||||
<!-- Modal confirmar finalização -->
|
||||
<div class="modal-overlay" id="modalConfirmarFinalizar">
|
||||
<div class="modal">
|
||||
<div class="modal" style="max-width:420px">
|
||||
<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>
|
||||
<p style="font-size:13px;color:#6b7280;margin-bottom:12px">Tem certeza que deseja finalizar este atendimento?</p>
|
||||
|
||||
<!-- Agendamento -->
|
||||
<div id="agendamentoBox" style="margin-bottom:14px;padding:14px;background:var(--surface-2);border-radius:8px;border:1px solid var(--border)">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--text-secondary);cursor:pointer;margin-bottom:0">
|
||||
<input type="checkbox" id="chkAgendar" onchange="toggleAgendamento()" style="accent-color:var(--primary);width:16px;height:16px"> 📅 Agendar retorno
|
||||
</label>
|
||||
<div id="agendamentoCampos" style="display:none;margin-top:12px">
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||||
<div style="flex:1">
|
||||
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Data</label>
|
||||
<input type="date" id="agData" style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;outline:none;background:var(--surface);color:var(--text-primary)">
|
||||
</div>
|
||||
<div style="width:90px">
|
||||
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Hora</label>
|
||||
<input type="time" id="agHora" style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;outline:none;background:var(--surface);color:var(--text-primary)">
|
||||
</div>
|
||||
</div>
|
||||
<label style="font-size:11px;font-weight:600;color:var(--text-muted);display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em">Anotação</label>
|
||||
<textarea id="agNota" rows="2" placeholder="Ex: Confirmar pagamento do boleto..." style="width:100%;padding:8px 10px;border:2px solid var(--border);border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;outline:none;background:var(--surface);color:var(--text-primary)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button onclick="fecharModal('modalConfirmarFinalizar')">Cancelar</button>
|
||||
<button class="btn-primary" onclick="fecharModal('modalConfirmarFinalizar');finalizarConversa()">Finalizar</button>
|
||||
@@ -835,6 +857,23 @@ body.dark-mode .msg.enviando { opacity: 0.4; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal retorno agendado -->
|
||||
<div class="modal-overlay" id="modalRetorno">
|
||||
<div class="modal" style="max-width:460px">
|
||||
<h3>📅 Retorno agendado</h3>
|
||||
<p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">Conversa criada. Revise o texto antes de enviar:</p>
|
||||
<input type="hidden" id="retornoConversaId">
|
||||
<div class="form-group">
|
||||
<textarea id="retornoTextoSugerido" rows="4" style="width:100%;padding:10px 14px;border:2px solid var(--border);border-radius:8px;font-size:14px;font-family:inherit;resize:vertical;outline:none;background:var(--surface-2);color:var(--text-primary)"></textarea>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-faint);margin-bottom:16px">Use [CLIENTE], [CONTATO], [EMPRESAF] para dados automáticos.</p>
|
||||
<div class="btn-group">
|
||||
<button onclick="document.getElementById('retornoConversaId').value='';fecharModal('modalRetorno')">Cancelar</button>
|
||||
<button class="btn-primary" onclick="confirmarRetorno()">✅ Abrir conversa</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal associar cliente -->
|
||||
<div class="modal-overlay" id="modalAssociar">
|
||||
<div class="modal">
|
||||
@@ -881,6 +920,7 @@ body.dark-mode .msg.enviando { opacity: 0.4; }
|
||||
<a href="#" onclick="mudarFiltro('unassigned',this)">Sem atend. (0)</a>
|
||||
<a href="#" id="tabTodasConvs" onclick="mudarFiltro('all',this)" class="admin-only-nav">Todas</a>
|
||||
<a href="#" onclick="mudarFiltro('equipe',this)" id="tabEquipeConvs">Equipe (0)</a>
|
||||
<a href="#" onclick="mudarFiltro('scheduled',this)" id="tabAgendadas">📅 Agendadas (0)</a>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchConv" placeholder="🔍 Buscar conversas..." onkeyup="carregarConversas()">
|
||||
@@ -1080,6 +1120,11 @@ document.querySelectorAll('.sidebar-left .nav-tabs a').forEach(function(a) {
|
||||
|
||||
// ===== CARREGAR CONVERSAS =====
|
||||
async function carregarConversas() {
|
||||
// Aba Agendadas: carrega da API de agendamentos
|
||||
if (filtroAtual === 'scheduled') {
|
||||
return carregarAgendadas();
|
||||
}
|
||||
|
||||
const busca = document.getElementById('searchConv').value;
|
||||
var statusFilter = document.getElementById('chkFinalizadas').checked ? 'A,E,F' : 'A,E';
|
||||
try {
|
||||
@@ -1818,6 +1863,22 @@ dropArea.addEventListener('drop', function(e) {
|
||||
});
|
||||
|
||||
// ===== FINALIZAR =====
|
||||
window.toggleAgendamento = function() {
|
||||
var show = document.getElementById('chkAgendar').checked;
|
||||
document.getElementById('agendamentoCampos').style.display = show ? 'block' : 'none';
|
||||
if (show) {
|
||||
if (!document.getElementById('agData').value) {
|
||||
document.getElementById('agData').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
// Preenche anotação com o texto da resolução (se existir)
|
||||
var txtResolucao = document.getElementById('campoResolucao');
|
||||
var nota = document.getElementById('agNota');
|
||||
if (txtResolucao && nota && !nota.value) {
|
||||
nota.value = (txtResolucao.value || '').trim();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.finalizarConversa = async function() {
|
||||
if (!conversaAtiva) return;
|
||||
|
||||
@@ -1847,6 +1908,19 @@ window.finalizarConversa = async function() {
|
||||
});
|
||||
var data = await res.json();
|
||||
if (data.success) {
|
||||
// Agenda retorno se checkbox marcado
|
||||
if (document.getElementById('chkAgendar') && document.getElementById('chkAgendar').checked) {
|
||||
var agData = document.getElementById('agData').value;
|
||||
var agHora = document.getElementById('agHora').value || null;
|
||||
var agNota = document.getElementById('agNota').value.trim() || null;
|
||||
if (agData) {
|
||||
fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/schedule', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: agData, hora: agHora, nota: agNota })
|
||||
}).catch(function(){});
|
||||
}
|
||||
}
|
||||
document.querySelector('.btn-finalizar').textContent = '✅ Finalizado';
|
||||
document.querySelector('.btn-finalizar').disabled = true;
|
||||
document.querySelector('.btn-finalizar').style.opacity = '0.6';
|
||||
@@ -2116,9 +2190,95 @@ window.mudarFiltro = function(filtro, el) {
|
||||
document.querySelectorAll('.nav-tabs a').forEach(function(a) { a.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
filtroAtual = filtro;
|
||||
// Mostra/esconde busca e checkbox de finalizadas (não fazem sentido para agendadas)
|
||||
var searchBox = document.querySelector('.search-box');
|
||||
if (filtro === 'scheduled') {
|
||||
if (searchBox) searchBox.style.display = 'none';
|
||||
} else {
|
||||
if (searchBox) searchBox.style.display = '';
|
||||
}
|
||||
carregarConversas();
|
||||
};
|
||||
|
||||
// ===== AGENDADAS =====
|
||||
async function carregarAgendadas() {
|
||||
var dataSel = document.getElementById('agendadasData');
|
||||
var data = dataSel ? dataSel.value : new Date().toISOString().split('T')[0];
|
||||
var list = document.getElementById('conversationsList');
|
||||
list.innerHTML = '<div style="padding:24px;text-align:center;color:rgba(255,255,255,0.4);font-size:13px">Carregando agendamentos...</div>';
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/' + alias + '/schedules?empresaId=' + empresaId + '&data=' + data, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
var d = await res.json();
|
||||
if (!d.success) return;
|
||||
|
||||
var dataFmt = data.split('-').reverse().join('/');
|
||||
var html = '<div style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:8px">' +
|
||||
'<span style="font-size:13px;color:rgba(255,255,255,0.7)">📅</span>' +
|
||||
'<input type="date" id="agendadasData" value="' + data + '" onchange="carregarAgendadas()" style="flex:1;padding:6px 8px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;font-size:12px;background:rgba(255,255,255,0.06);color:#fff;outline:none">' +
|
||||
'<span style="font-size:12px;color:rgba(255,255,255,0.5)">' + (d.count || 0) + ' agend.</span></div>';
|
||||
|
||||
if (!d.data || d.data.length === 0) {
|
||||
html += '<div style="padding:24px;text-align:center;color:rgba(255,255,255,0.35);font-size:13px">Nenhum agendamento para ' + dataFmt + '</div>';
|
||||
} else {
|
||||
d.data.forEach(function(a) {
|
||||
var nome = a.nomeContato || a.numero || '-';
|
||||
var cor = avatarColor(nome);
|
||||
var ini = avatarInitials(nome);
|
||||
var hora = a.hora ? a.hora.substring(0,5) : '';
|
||||
html += '<div class="conv-item" style="cursor:pointer" onclick="reabrirAgendamento(' + a.id + ',' + a.conversaId + ')">' +
|
||||
'<div class="conv-avatar" style="background:' + cor + '">' + ini + '</div>' +
|
||||
'<div class="conv-body">' +
|
||||
'<div class="conv-nome">' + nome + (hora ? ' <span style="font-size:10px;opacity:0.6">🕐 ' + hora + '</span>' : '') + '</div>' +
|
||||
'<div class="conv-ultima" style="font-size:11px">' + (a.nota || 'Sem anotação') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
list.innerHTML = html;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
window.reabrirAgendamento = async function(agId, convId) {
|
||||
try {
|
||||
var res = await fetch('/api/' + alias + '/schedules/' + agId + '/reopen', {
|
||||
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
var d = await res.json();
|
||||
if (d.success) {
|
||||
// Mostra modal com o texto sugerido
|
||||
document.getElementById('retornoTextoSugerido').value = d.textoSugerido || '';
|
||||
document.getElementById('retornoConversaId').value = d.conversaId;
|
||||
document.getElementById('modalRetorno').classList.add('show');
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
window.confirmarRetorno = function() {
|
||||
var convId = document.getElementById('retornoConversaId').value;
|
||||
var texto = document.getElementById('retornoTextoSugerido').value.trim();
|
||||
fecharModal('modalRetorno');
|
||||
if (!convId) return;
|
||||
// Abre a conversa e preenche o input
|
||||
mudarFiltro('mine', document.querySelector('.nav-tabs a'));
|
||||
carregarConversas();
|
||||
setTimeout(function() {
|
||||
abrirConversa(parseInt(convId));
|
||||
// Aguarda o chat carregar e preenche o input
|
||||
setTimeout(function() {
|
||||
var input = document.getElementById('msgInput');
|
||||
if (input && texto) {
|
||||
input.value = texto;
|
||||
input.style.height = 'auto';
|
||||
input.style.height = input.scrollHeight + 'px';
|
||||
input.focus();
|
||||
}
|
||||
}, 500);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Mostra
|
||||
|
||||
// ===== SELETOR DE EMPRESA (usuário com acesso a mais de uma) =====
|
||||
|
||||
@@ -1041,6 +1041,15 @@ body.dark-mode .tabbar {
|
||||
border-color: #0f3460;
|
||||
box-shadow: 0 -2px 12px rgba(0,0,0,0.3);
|
||||
}
|
||||
/* Agendamento box no dark mode */
|
||||
body.dark-mode #agendamentoBox {
|
||||
background: #1a1a2e !important;
|
||||
border-color: #0f3460 !important;
|
||||
}
|
||||
body.dark-mode #agendamentoBox input[type="date"],
|
||||
body.dark-mode #agendamentoBox input[type="time"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
/* Botoes do painel direito no dark mode */
|
||||
body.dark-mode [style*="background:#eef2ff"] {
|
||||
background: #1e1b4b !important;
|
||||
|
||||
@@ -19,6 +19,9 @@ router.post('/:alias/conversations/:id/labels', authenticateToken, ChatControlle
|
||||
router.post('/:alias/conversations/:id/link-client', authenticateToken, ChatController.linkClient);
|
||||
router.get('/:alias/conversations/:id/boletos', authenticateToken, ChatController.getBoletos);
|
||||
router.post('/:alias/conversations/:id/send-boleto', authenticateToken, ChatController.sendBoleto);
|
||||
router.post('/:alias/conversations/:id/schedule', authenticateToken, ChatController.scheduleConversation);
|
||||
router.get('/:alias/schedules', authenticateToken, ChatController.listSchedules);
|
||||
router.post('/:alias/schedules/:id/reopen', authenticateToken, ChatController.reopenSchedule);
|
||||
router.get('/:alias/search', authenticateToken, ChatController.search);
|
||||
router.get('/:alias/media/:mediaId', ChatController.getMedia);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user