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',
|
'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
|
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)
|
// Helper: normaliza número para formato WhatsApp (DDI + 9º dígito)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const GENERATORS = [
|
|||||||
'GEN_CHATC2_MENUS_EMPRESA',
|
'GEN_CHATC2_MENUS_EMPRESA',
|
||||||
'GEN_CHATC2_CSAT_AVALIACOES',
|
'GEN_CHATC2_CSAT_AVALIACOES',
|
||||||
'GEN_CHATC2_MOTIVOS_ATENDIMENTO',
|
'GEN_CHATC2_MOTIVOS_ATENDIMENTO',
|
||||||
|
'GEN_CHATC2_AGENDAMENTOS',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function nextId(alias, generatorName) {
|
async function nextId(alias, generatorName) {
|
||||||
|
|||||||
+162
-2
@@ -825,9 +825,31 @@ body.dark-mode .msg.enviando { opacity: 0.4; }
|
|||||||
|
|
||||||
<!-- Modal confirmar finalização -->
|
<!-- Modal confirmar finalização -->
|
||||||
<div class="modal-overlay" id="modalConfirmarFinalizar">
|
<div class="modal-overlay" id="modalConfirmarFinalizar">
|
||||||
<div class="modal">
|
<div class="modal" style="max-width:420px">
|
||||||
<h3>⚠️ Finalizar conversa</h3>
|
<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">
|
<div class="btn-group">
|
||||||
<button onclick="fecharModal('modalConfirmarFinalizar')">Cancelar</button>
|
<button onclick="fecharModal('modalConfirmarFinalizar')">Cancelar</button>
|
||||||
<button class="btn-primary" onclick="fecharModal('modalConfirmarFinalizar');finalizarConversa()">Finalizar</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>
|
||||||
</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 -->
|
<!-- Modal associar cliente -->
|
||||||
<div class="modal-overlay" id="modalAssociar">
|
<div class="modal-overlay" id="modalAssociar">
|
||||||
<div class="modal">
|
<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="#" onclick="mudarFiltro('unassigned',this)">Sem atend. (0)</a>
|
||||||
<a href="#" id="tabTodasConvs" onclick="mudarFiltro('all',this)" class="admin-only-nav">Todas</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('equipe',this)" id="tabEquipeConvs">Equipe (0)</a>
|
||||||
|
<a href="#" onclick="mudarFiltro('scheduled',this)" id="tabAgendadas">📅 Agendadas (0)</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input type="text" id="searchConv" placeholder="🔍 Buscar conversas..." onkeyup="carregarConversas()">
|
<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 =====
|
// ===== CARREGAR CONVERSAS =====
|
||||||
async function carregarConversas() {
|
async function carregarConversas() {
|
||||||
|
// Aba Agendadas: carrega da API de agendamentos
|
||||||
|
if (filtroAtual === 'scheduled') {
|
||||||
|
return carregarAgendadas();
|
||||||
|
}
|
||||||
|
|
||||||
const busca = document.getElementById('searchConv').value;
|
const busca = document.getElementById('searchConv').value;
|
||||||
var statusFilter = document.getElementById('chkFinalizadas').checked ? 'A,E,F' : 'A,E';
|
var statusFilter = document.getElementById('chkFinalizadas').checked ? 'A,E,F' : 'A,E';
|
||||||
try {
|
try {
|
||||||
@@ -1818,6 +1863,22 @@ dropArea.addEventListener('drop', function(e) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== FINALIZAR =====
|
// ===== 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() {
|
window.finalizarConversa = async function() {
|
||||||
if (!conversaAtiva) return;
|
if (!conversaAtiva) return;
|
||||||
|
|
||||||
@@ -1847,6 +1908,19 @@ window.finalizarConversa = async function() {
|
|||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
if (data.success) {
|
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').textContent = '✅ Finalizado';
|
||||||
document.querySelector('.btn-finalizar').disabled = true;
|
document.querySelector('.btn-finalizar').disabled = true;
|
||||||
document.querySelector('.btn-finalizar').style.opacity = '0.6';
|
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'); });
|
document.querySelectorAll('.nav-tabs a').forEach(function(a) { a.classList.remove('active'); });
|
||||||
el.classList.add('active');
|
el.classList.add('active');
|
||||||
filtroAtual = filtro;
|
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();
|
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
|
// Mostra
|
||||||
|
|
||||||
// ===== SELETOR DE EMPRESA (usuário com acesso a mais de uma) =====
|
// ===== SELETOR DE EMPRESA (usuário com acesso a mais de uma) =====
|
||||||
|
|||||||
@@ -1041,6 +1041,15 @@ body.dark-mode .tabbar {
|
|||||||
border-color: #0f3460;
|
border-color: #0f3460;
|
||||||
box-shadow: 0 -2px 12px rgba(0,0,0,0.3);
|
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 */
|
/* Botoes do painel direito no dark mode */
|
||||||
body.dark-mode [style*="background:#eef2ff"] {
|
body.dark-mode [style*="background:#eef2ff"] {
|
||||||
background: #1e1b4b !important;
|
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.post('/:alias/conversations/:id/link-client', authenticateToken, ChatController.linkClient);
|
||||||
router.get('/:alias/conversations/:id/boletos', authenticateToken, ChatController.getBoletos);
|
router.get('/:alias/conversations/:id/boletos', authenticateToken, ChatController.getBoletos);
|
||||||
router.post('/:alias/conversations/:id/send-boleto', authenticateToken, ChatController.sendBoleto);
|
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/search', authenticateToken, ChatController.search);
|
||||||
router.get('/:alias/media/:mediaId', ChatController.getMedia);
|
router.get('/:alias/media/:mediaId', ChatController.getMedia);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user