atualizacoes
This commit is contained in:
@@ -80,6 +80,18 @@ const MIGRACOES = [
|
|||||||
'CREATE INDEX IDX_CONV_STATUS ON CHATC2_CONVERSAS (CON_STATUS)',
|
'CREATE INDEX IDX_CONV_STATUS ON CHATC2_CONVERSAS (CON_STATUS)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// MIGRAÇÃO 22: Mensagem e etiqueta por equipe (triagem)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
descricao: 'Adicionar EQU_MENSAGEM e EQU_ETIQUETA_ID em CHATC2_EQUIPES',
|
||||||
|
sql: [
|
||||||
|
'ALTER TABLE CHATC2_EQUIPES ADD EQU_MENSAGEM VARCHAR(500)',
|
||||||
|
'ALTER TABLE CHATC2_EQUIPES ADD EQU_ETIQUETA_ID INTEGER',
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -639,6 +639,9 @@ class ChatController {
|
|||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
|
// Processa placeholders: [CLIENTE], [CONTATO], [EMPRESAR], [EMPRESAF]
|
||||||
|
textoFinal = await ChatController.processarPlaceholders(alias, id, textoFinal);
|
||||||
|
|
||||||
// Insere mensagem no banco
|
// Insere mensagem no banco
|
||||||
await db.execute(alias, `
|
await db.execute(alias, `
|
||||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
|
||||||
@@ -902,10 +905,11 @@ class ChatController {
|
|||||||
|
|
||||||
// Adiciona mensagem na conversa existente
|
// Adiciona mensagem na conversa existente
|
||||||
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
||||||
|
const textoProcessado = await ChatController.processarPlaceholders(alias, conversaId, mensagem);
|
||||||
await db.execute(alias, `
|
await db.execute(alias, `
|
||||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||||
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||||
`, [msgId, conversaId, req.user?.id, mensagem]);
|
`, [msgId, conversaId, req.user?.id, textoProcessado]);
|
||||||
|
|
||||||
await db.execute(alias,
|
await db.execute(alias,
|
||||||
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?',
|
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?',
|
||||||
@@ -915,7 +919,7 @@ class ChatController {
|
|||||||
const resultado = await db.query(alias,
|
const resultado = await db.query(alias,
|
||||||
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]);
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]);
|
||||||
if (resultado.length > 0) {
|
if (resultado.length > 0) {
|
||||||
sendEvolutionMessage(resultado[0], numeroLimpo, mensagem, 'text', null).catch(function(){});
|
sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessado, 'text', null).catch(function(){});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({ success: true, data: { id: conversaId, reutilizada: true } });
|
return res.json({ success: true, data: { id: conversaId, reutilizada: true } });
|
||||||
@@ -941,18 +945,19 @@ class ChatController {
|
|||||||
|
|
||||||
// Insere primeira mensagem
|
// Insere primeira mensagem
|
||||||
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
const msgId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
||||||
|
const textoProcessadoNew = await ChatController.processarPlaceholders(alias, newId, mensagem);
|
||||||
|
|
||||||
await db.execute(alias, `
|
await db.execute(alias, `
|
||||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||||
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||||
`, [msgId, newId, req.user?.id, mensagem]);
|
`, [msgId, newId, req.user?.id, textoProcessadoNew]);
|
||||||
|
|
||||||
// Envia via Evolution API
|
// Envia via Evolution API
|
||||||
try {
|
try {
|
||||||
const resultado = await db.query(alias,
|
const resultado = await db.query(alias,
|
||||||
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instId]);
|
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instId]);
|
||||||
if (resultado.length > 0) {
|
if (resultado.length > 0) {
|
||||||
await sendEvolutionMessage(resultado[0], numeroLimpo, mensagem, 'text', null);
|
await sendEvolutionMessage(resultado[0], numeroLimpo, textoProcessadoNew, 'text', null);
|
||||||
}
|
}
|
||||||
} catch (evoErr) {
|
} catch (evoErr) {
|
||||||
console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message);
|
console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message);
|
||||||
@@ -1428,6 +1433,26 @@ class ChatController {
|
|||||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recebe avaliação CSAT via formulário web
|
||||||
|
* POST /api/:alias/csat/avaliar
|
||||||
|
*/
|
||||||
|
static async csatCheck(req, res) {
|
||||||
|
try {
|
||||||
|
const { alias } = req.params;
|
||||||
|
const conversaId = parseInt(req.query.conversa);
|
||||||
|
if (!conversaId) return res.status(400).json({ success: false, error: 'conversa obrigatório.' });
|
||||||
|
|
||||||
|
const existe = await db.query(alias,
|
||||||
|
'SELECT CSA_NOTA, CSA_COMENTARIO FROM CHATC2_CSAT_AVALIACOES WHERE CSA_CONVERSA_ID = ?',
|
||||||
|
[conversaId]);
|
||||||
|
|
||||||
|
res.json({ success: true, avaliado: existe.length > 0 });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recebe avaliação CSAT via formulário web
|
* Recebe avaliação CSAT via formulário web
|
||||||
* POST /api/:alias/csat/avaliar
|
* POST /api/:alias/csat/avaliar
|
||||||
@@ -1470,6 +1495,67 @@ class ChatController {
|
|||||||
res.status(500).json({ success: false, error: err.message });
|
res.status(500).json({ success: false, error: err.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substitui placeholders no texto da mensagem.
|
||||||
|
* [CLIENTE] → nome do cliente cadastrado (ou [CONTATO] se não tiver)
|
||||||
|
* [CONTATO] → nome do contato WhatsApp
|
||||||
|
* [EMPRESAR] → razão social da empresa
|
||||||
|
* [EMPRESAF] → nome fantasia da empresa
|
||||||
|
*/
|
||||||
|
static async processarPlaceholders(alias, conversaId, texto) {
|
||||||
|
if (!texto || (!texto.includes('[') && !texto.includes(']'))) return texto;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conv = await db.query(alias,
|
||||||
|
'SELECT CON_CLIENTE_ID, CON_NOME_CONTATO, CON_EMPRESA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?',
|
||||||
|
[conversaId]);
|
||||||
|
if (conv.length === 0) return texto;
|
||||||
|
|
||||||
|
const contato = (conv[0].CON_NOME_CONTATO || '').trim();
|
||||||
|
const empresaId = conv[0].CON_EMPRESA_ID;
|
||||||
|
|
||||||
|
// Busca nome do cliente
|
||||||
|
let clienteNome = '';
|
||||||
|
if (conv[0].CON_CLIENTE_ID) {
|
||||||
|
const cli = await db.query(alias,
|
||||||
|
'SELECT CLI_NOME, CLI_NOME_FANTASIA FROM CLIENTES WHERE CLI_CODIGO_ID = ?',
|
||||||
|
[conv[0].CON_CLIENTE_ID]);
|
||||||
|
if (cli.length > 0) {
|
||||||
|
clienteNome = (cli[0].CLI_NOME_FANTASIA || cli[0].CLI_NOME || '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca dados da empresa
|
||||||
|
let empresaR = '', empresaF = '';
|
||||||
|
if (empresaId) {
|
||||||
|
const emp = await db.query(alias,
|
||||||
|
'SELECT EMP_NOME, EMP_NOME_FANTASIA FROM EMPRESAS WHERE EMP_CODIGO_ID = ?',
|
||||||
|
[empresaId]);
|
||||||
|
if (emp.length > 0) {
|
||||||
|
empresaR = (emp[0].EMP_NOME || '').trim();
|
||||||
|
empresaF = (emp[0].EMP_NOME_FANTASIA || emp[0].EMP_NOME || '').trim();
|
||||||
|
} else {
|
||||||
|
console.log('[Placeholder] Empresa nao encontrada para ID:', empresaId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[Placeholder] Conversa sem CON_EMPRESA_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitui: [CLIENTE] usa nome do cadastro, senão cai para [CONTATO]
|
||||||
|
let result = texto;
|
||||||
|
result = result.replace(/\[CLIENTE\]/gi, clienteNome || contato || '');
|
||||||
|
result = result.replace(/\[CONTATO\]/gi, contato || '');
|
||||||
|
result = result.replace(/\[EMPRESAR\]/gi, empresaR || empresaF || '');
|
||||||
|
result = result.replace(/\[EMPRESAF\]/gi, empresaF || empresaR || '');
|
||||||
|
|
||||||
|
console.log('[Placeholder] conversa:', conversaId, '| contato:', contato, '| cliente:', clienteNome, '| empresaR:', empresaR, '| empresaF:', empresaF, '| antes:', texto.substring(0, 80), '| depois:', result.substring(0, 80));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
return texto; // fallback: retorna sem substituir
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: normaliza número para formato WhatsApp (DDI + 9º dígito)
|
// Helper: normaliza número para formato WhatsApp (DDI + 9º dígito)
|
||||||
|
|||||||
@@ -1009,6 +1009,40 @@ class ClientController {
|
|||||||
res.status(500).json({ success: false, error: err.message });
|
res.status(500).json({ success: false, error: err.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Atualiza a data de agendamento de cobrança de um título.
|
||||||
|
* PUT /api/:alias/clients/:clienteId/carne/:carneId/agendamento
|
||||||
|
*/
|
||||||
|
static async atualizarAgendamento(req, res) {
|
||||||
|
try {
|
||||||
|
const { alias, clienteId, carneId } = req.params;
|
||||||
|
const { dataAgendamento } = req.body;
|
||||||
|
|
||||||
|
// Verifica autenticação básica
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(403).json({ success: false, error: 'Não autenticado.' });
|
||||||
|
|
||||||
|
if (!dataAgendamento) {
|
||||||
|
return res.status(400).json({ success: false, error: 'Data de agendamento é obrigatória.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se o título pertence ao cliente
|
||||||
|
const carne = await db.query(alias,
|
||||||
|
'SELECT CAR_CODIGO_ID FROM CARNES WHERE CAR_CODIGO_ID = ? AND CAR_CLIENTE_ID = ?',
|
||||||
|
[parseInt(carneId), parseInt(clienteId)]);
|
||||||
|
if (carne.length === 0) {
|
||||||
|
return res.status(404).json({ success: false, error: 'Título não encontrado para este cliente.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(alias,
|
||||||
|
'UPDATE CARNES SET CAR_DT_AGENDAMENTO_COBRANCA = ? WHERE CAR_CODIGO_ID = ?',
|
||||||
|
[dataAgendamento, parseInt(carneId)]);
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Agendamento atualizado.' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientController;
|
module.exports = ClientController;
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ class ConfigController {
|
|||||||
t.members = members.map(m => ({ id: m.USU_CODIGO_ID, nome: (m.USU_NOME || '').trim(), login: (m.USU_LOGIN || '').trim() }));
|
t.members = members.map(m => ({ id: m.USU_CODIGO_ID, nome: (m.USU_NOME || '').trim(), login: (m.USU_LOGIN || '').trim() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, data: teams.map(t => ({ id: t.EQU_CODIGO_ID, nome: (t.EQU_NOME || '').trim(), ordem: t.EQU_ORDEM || 0, membros: t.members })) });
|
res.json({ success: true, data: teams.map(t => ({
|
||||||
|
id: t.EQU_CODIGO_ID,
|
||||||
|
nome: (t.EQU_NOME || '').trim(),
|
||||||
|
ordem: t.EQU_ORDEM || 0,
|
||||||
|
mensagem: (t.EQU_MENSAGEM || '').trim(),
|
||||||
|
etiquetaId: t.EQU_ETIQUETA_ID || null,
|
||||||
|
membros: t.members
|
||||||
|
})) });
|
||||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +46,12 @@ class ConfigController {
|
|||||||
|
|
||||||
const newId = await db.nextId(alias, 'GEN_CHATC2_EQUIPES');
|
const newId = await db.nextId(alias, 'GEN_CHATC2_EQUIPES');
|
||||||
const ordem = req.body.ordem !== undefined ? req.body.ordem : 0;
|
const ordem = req.body.ordem !== undefined ? req.body.ordem : 0;
|
||||||
|
const mensagem = (req.body.mensagem || '').trim();
|
||||||
|
const etiquetaId = req.body.etiquetaId || null;
|
||||||
|
|
||||||
await db.execute(alias, 'INSERT INTO CHATC2_EQUIPES (EQU_CODIGO_ID, EQU_EMPRESA_ID, EQU_NOME, EQU_ORDEM) VALUES (?, ?, ?, ?)', [newId, empresaId, nome, ordem]);
|
await db.execute(alias,
|
||||||
|
'INSERT INTO CHATC2_EQUIPES (EQU_CODIGO_ID, EQU_EMPRESA_ID, EQU_NOME, EQU_ORDEM, EQU_MENSAGEM, EQU_ETIQUETA_ID) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[newId, empresaId, nome, ordem, mensagem || null, etiquetaId]);
|
||||||
|
|
||||||
if (membros && membros.length > 0) {
|
if (membros && membros.length > 0) {
|
||||||
for (const userId of membros) {
|
for (const userId of membros) {
|
||||||
@@ -60,6 +71,8 @@ class ConfigController {
|
|||||||
|
|
||||||
if (nome) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_NOME = ? WHERE EQU_CODIGO_ID = ?', [nome, id]);
|
if (nome) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_NOME = ? WHERE EQU_CODIGO_ID = ?', [nome, id]);
|
||||||
if (req.body.ordem !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_ORDEM = ? WHERE EQU_CODIGO_ID = ?', [req.body.ordem, id]);
|
if (req.body.ordem !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_ORDEM = ? WHERE EQU_CODIGO_ID = ?', [req.body.ordem, id]);
|
||||||
|
if (req.body.mensagem !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_MENSAGEM = ? WHERE EQU_CODIGO_ID = ?', [req.body.mensagem || null, id]);
|
||||||
|
if (req.body.etiquetaId !== undefined) await db.execute(alias, 'UPDATE CHATC2_EQUIPES SET EQU_ETIQUETA_ID = ? WHERE EQU_CODIGO_ID = ?', [req.body.etiquetaId || null, id]);
|
||||||
|
|
||||||
if (membros) {
|
if (membros) {
|
||||||
await db.execute(alias, 'DELETE FROM CHATC2_USU_EQUIPES WHERE EQU_EQUIPE_ID = ?', [id]);
|
await db.execute(alias, 'DELETE FROM CHATC2_USU_EQUIPES WHERE EQU_EQUIPE_ID = ?', [id]);
|
||||||
|
|||||||
@@ -89,15 +89,13 @@ class DashboardController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== Tráfego das conversas (últimos 365 dias, por dia) =====
|
// ===== Tráfego das conversas (últimos 365 dias, por dia) =====
|
||||||
const desde = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
|
||||||
const desdeStr = desde.toISOString().split('T')[0];
|
|
||||||
const trafegoRows = await db.query(alias,
|
const trafegoRows = await db.query(alias,
|
||||||
`SELECT CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE) AS DIA, COUNT(*) AS CT
|
`SELECT CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE) AS DIA, COUNT(*) AS CT
|
||||||
FROM CHATC2_CONVERSAS
|
FROM CHATC2_CONVERSAS
|
||||||
WHERE CON_EMPRESA_ID IN (${ph})
|
WHERE CON_EMPRESA_ID IN (${ph})
|
||||||
AND COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) >= ?
|
AND COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) >= CURRENT_DATE - 365
|
||||||
GROUP BY CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE)`,
|
GROUP BY CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE)`,
|
||||||
empresas.concat([desdeStr]));
|
empresas);
|
||||||
|
|
||||||
const fmtDia = (d) => {
|
const fmtDia = (d) => {
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
|
|||||||
@@ -766,7 +766,8 @@ async function processWebhook(alias, body) {
|
|||||||
|
|
||||||
// Busca conversa existente pelo número exato ou pelos últimos 8 dígitos
|
// Busca conversa existente pelo número exato ou pelos últimos 8 dígitos
|
||||||
let conversations = await db.query(alias,
|
let conversations = await db.query(alias,
|
||||||
`SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO
|
`SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO,
|
||||||
|
CON_MENU_ESTADO, CON_USUARIO_ID, CON_EQUIPE_ID
|
||||||
FROM CHATC2_CONVERSAS WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ?
|
FROM CHATC2_CONVERSAS WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ?
|
||||||
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
||||||
ORDER BY CON_DT_INICIO DESC`,
|
ORDER BY CON_DT_INICIO DESC`,
|
||||||
@@ -776,7 +777,8 @@ async function processWebhook(alias, body) {
|
|||||||
if (conversations.length === 0) {
|
if (conversations.length === 0) {
|
||||||
const ultimos8 = numero.slice(-8);
|
const ultimos8 = numero.slice(-8);
|
||||||
conversations = await db.query(alias,
|
conversations = await db.query(alias,
|
||||||
`SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO
|
`SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_SAUDACAO_ENVIADA, CON_CSAT_ENVIADO,
|
||||||
|
CON_MENU_ESTADO, CON_USUARIO_ID, CON_EQUIPE_ID
|
||||||
FROM CHATC2_CONVERSAS WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ?
|
FROM CHATC2_CONVERSAS WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ?
|
||||||
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
||||||
ORDER BY CON_DT_INICIO DESC`,
|
ORDER BY CON_DT_INICIO DESC`,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class TriageController {
|
|||||||
const boletoNum = String(cfg.CFE_TRIAGEM_BOLETO_NUMERO || '0').trim();
|
const boletoNum = String(cfg.CFE_TRIAGEM_BOLETO_NUMERO || '0').trim();
|
||||||
|
|
||||||
const equipes = await db.query(alias,
|
const equipes = await db.query(alias,
|
||||||
"SELECT EQU_CODIGO_ID, EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME",
|
"SELECT EQU_CODIGO_ID, EQU_NOME, EQU_MENSAGEM, EQU_ETIQUETA_ID FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME",
|
||||||
[empresaId]
|
[empresaId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class TriageController {
|
|||||||
if (estado === 'root' || estado === '') {
|
if (estado === 'root' || estado === '') {
|
||||||
logTriage('7-processResponse root - resposta: ' + JSON.stringify(resposta) + ' | boletoNum: ' + JSON.stringify(boletoNum));
|
logTriage('7-processResponse root - resposta: ' + JSON.stringify(resposta) + ' | boletoNum: ' + JSON.stringify(boletoNum));
|
||||||
const equipes = await db.query(alias,
|
const equipes = await db.query(alias,
|
||||||
"SELECT EQU_CODIGO_ID, EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME",
|
"SELECT EQU_CODIGO_ID, EQU_NOME, EQU_MENSAGEM, EQU_ETIQUETA_ID FROM CHATC2_EQUIPES WHERE EQU_EMPRESA_ID = ? AND EQU_SITUACAO = 'A' ORDER BY EQU_ORDEM, EQU_NOME",
|
||||||
[empresaId]
|
[empresaId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -248,6 +248,26 @@ class TriageController {
|
|||||||
[eq.EQU_CODIGO_ID, conversaId]
|
[eq.EQU_CODIGO_ID, conversaId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Mensagem personalizada da equipe (se configurada)
|
||||||
|
const msgEquipe = (eq.EQU_MENSAGEM || '').trim();
|
||||||
|
if (msgEquipe) {
|
||||||
|
const msgEqId = await db.nextId(alias, 'GEN_CONVERSAS_MENSAGENS');
|
||||||
|
await db.execute(alias, `
|
||||||
|
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||||
|
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||||
|
`, [msgEqId, conversaId, msgEquipe]);
|
||||||
|
await TriageController.sendEvolution(alias, instanciaId, numero, msgEquipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta automática da equipe (se configurada)
|
||||||
|
if (eq.EQU_ETIQUETA_ID) {
|
||||||
|
try {
|
||||||
|
await db.execute(alias,
|
||||||
|
'INSERT INTO CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID, CET_ETIQUETA_ID) VALUES (?, ?)',
|
||||||
|
[conversaId, eq.EQU_ETIQUETA_ID]);
|
||||||
|
} catch (_) { /* ignora duplicata */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Verifica se existem submenus configurados para esta equipe
|
// Verifica se existem submenus configurados para esta equipe
|
||||||
var temSubmenus = await TriageController.enviarSubmenu(alias, conversaId, empresaId, instanciaId, numero, null, eq.EQU_CODIGO_ID);
|
var temSubmenus = await TriageController.enviarSubmenu(alias, conversaId, empresaId, instanciaId, numero, null, eq.EQU_CODIGO_ID);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
.loading { text-align:center; padding:40px; color:var(--text-faint); }
|
.loading { text-align:center; padding:40px; color:var(--text-faint); }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
||||||
|
|||||||
+16
-3
@@ -816,7 +816,7 @@ body.dark-mode .msg.erro .btn-reenviar:hover { background: #8b0000; color: #fff;
|
|||||||
body.dark-mode .msg.enviando { opacity: 0.4; }
|
body.dark-mode .msg.enviando { opacity: 0.4; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -1284,8 +1284,21 @@ function renderInfoConversa(conv) {
|
|||||||
'<div class="field"><div class="label">Contato</div><div class="value">' + esc(dep.telefone || conv.numero || '-') + '</div></div>' +
|
'<div class="field"><div class="label">Contato</div><div class="value">' + esc(dep.telefone || conv.numero || '-') + '</div></div>' +
|
||||||
'<div class="field"><div class="label">' + inads + '</div></div>';
|
'<div class="field"><div class="label">' + inads + '</div></div>';
|
||||||
} else {
|
} else {
|
||||||
fotoContainer.textContent = (conv.nomeContato || '?').charAt(0).toUpperCase();
|
// Sem cliente vinculado: mostra o número e nome do contato WhatsApp
|
||||||
infoContainer.innerHTML = '<div style="color:#9ca3af;font-size:13px;text-align:center;padding:20px 0">Contato não cadastrado como cliente</div>';
|
var numeroExibir = (conv.numero || '').replace(/^(\d{2})(\d{2})(\d{5})(\d{4})$/, '($1) $2 $3-$4').replace(/^(\d{2})(\d{5})(\d{4})$/, '($1) $2-$3');
|
||||||
|
if (!numeroExibir) numeroExibir = conv.numero || '-';
|
||||||
|
var nomeContato = (conv.nomeContato || '').trim();
|
||||||
|
// Se o nome do contato for igual ao número (formato puro), mostra só o número formatado
|
||||||
|
var nomeLimpo = nomeContato.replace(/\D/g, '');
|
||||||
|
var numLimpo = (conv.numero || '').replace(/\D/g, '');
|
||||||
|
if (nomeLimpo === numLimpo) nomeContato = '';
|
||||||
|
|
||||||
|
fotoContainer.innerHTML = '<span style="font-weight:600">?</span>';
|
||||||
|
infoContainer.innerHTML =
|
||||||
|
'<div style="font-size:13px;font-weight:600;color:#d97706;margin-bottom:6px">⚠️ Número não identificado</div>' +
|
||||||
|
'<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(nomeContato || numeroExibir) + '</div>' +
|
||||||
|
(nomeContato ? '<div class="field"><div class="label">WhatsApp</div><div class="value">' + esc(numeroExibir) + '</div></div>' : '') +
|
||||||
|
'<div class="field"><div class="label">Status</div><div class="value" style="color:var(--text-muted)">Sem cadastro no sistema</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Botão "Enviar boleto" (aparece se a empresa habilitar e houver titular)
|
// Botão "Enviar boleto" (aparece se a empresa habilitar e houver titular)
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ table { font-size:13px; }
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -637,6 +637,28 @@ table { font-size:13px; }
|
|||||||
return tipos;
|
return tipos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Editar data de agendamento de cobrança
|
||||||
|
window.editarAgendamento = function(carneId, el) {
|
||||||
|
var dataAtual = el.textContent.trim();
|
||||||
|
var novaData = prompt('Nova data de agendamento (AAAA-MM-DD):', dataAtual !== '-' ? dataAtual : '');
|
||||||
|
if (novaData === null) return; // cancelou
|
||||||
|
if (!novaData.trim()) {
|
||||||
|
novaData = null; // limpar agendamento
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/' + alias + '/clients/' + id_cliente + '/carne/' + carneId + '/agendamento', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ dataAgendamento: novaData })
|
||||||
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
el.textContent = novaData ? novaData.split('-').reverse().join('/') : '-';
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Erro ao atualizar');
|
||||||
|
}
|
||||||
|
}).catch(function() { alert('Erro de conexão'); });
|
||||||
|
};
|
||||||
|
|
||||||
// Torna global para os onclick dos botões de paginação
|
// Torna global para os onclick dos botões de paginação
|
||||||
window.loadCarnes = function(page) {
|
window.loadCarnes = function(page) {
|
||||||
if (page) carnesPage = page;
|
if (page) carnesPage = page;
|
||||||
@@ -697,7 +719,9 @@ table { font-size:13px; }
|
|||||||
'<td>' + vencDot + fmtDate(c.vencimento) + '</td>' +
|
'<td>' + vencDot + fmtDate(c.vencimento) + '</td>' +
|
||||||
'<td class="valor">' + fmtMoney(c.valorParcela) + '</td>' +
|
'<td class="valor">' + fmtMoney(c.valorParcela) + '</td>' +
|
||||||
'<td>' + fmtDate(c.dataPagamento) + '</td>' +
|
'<td>' + fmtDate(c.dataPagamento) + '</td>' +
|
||||||
'<td style="font-size:11px">' + fmtDate(c.agendamentoCobranca) + '</td>' +
|
'<td style="font-size:11px">' +
|
||||||
|
'<span class="agendamento-cell" onclick="editarAgendamento(' + c.id + ',this)" title="Clique para alterar" style="cursor:pointer;border-bottom:1px dashed #9ca3af">' +
|
||||||
|
fmtDate(c.agendamentoCobranca) + '</span></td>' +
|
||||||
'<td>' + (c.nossoNumero || '-') + '</td>' +
|
'<td>' + (c.nossoNumero || '-') + '</td>' +
|
||||||
'<td class="centro">' + (c.parcela || '-') + '/' + (c.totalParcelas || '-') + '</td>' +
|
'<td class="centro">' + (c.parcela || '-') + '/' + (c.totalParcelas || '-') + '</td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
.container { flex: 1; padding: 24px; overflow-y: auto; }
|
.container { flex: 1; padding: 24px; overflow-y: auto; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
||||||
|
|||||||
+192
-83
@@ -3,102 +3,187 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Avalie seu Atendimento</title>
|
<title>Avalie seu Atendimento - Chatc2</title>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--primary-dark: #5a67d8;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #f9fafb;
|
||||||
|
--surface-3: #f3f4f6;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--text-primary: #111827;
|
||||||
|
--text-secondary: #374151;
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
--text-faint: #9ca3af;
|
||||||
|
--success: #059669;
|
||||||
|
--success-bg: #d1fae5;
|
||||||
|
--success-text: #065f46;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--danger-bg: #fef2f2;
|
||||||
|
--danger-text: #991b1b;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
|
-webkit-font-smoothing: antialiased;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 12s ease infinite;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 40px;
|
padding: 44px 40px 36px;
|
||||||
max-width: 480px;
|
max-width: 440px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
|
box-shadow: 0 32px 80px rgba(0,0,0,0.28), 0 0 0 1px rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
.logo { font-size: 48px; margin-bottom: 16px; }
|
|
||||||
h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
|
|
||||||
.subtitle { font-size: 14px; color: var(--text-muted); margin-bottom: 32px; }
|
|
||||||
|
|
||||||
.stars {
|
.logo {
|
||||||
display: flex;
|
width: 76px; height: 76px;
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||||
|
border-radius: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
margin-bottom: 18px;
|
||||||
|
font-size: 32px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px; font-weight: 800;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px; color: var(--text-muted);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stars */
|
||||||
|
.stars {
|
||||||
|
display: flex; justify-content: center; gap: 8px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
direction: rtl;
|
|
||||||
}
|
}
|
||||||
.stars input { display: none; }
|
.stars input { display: none; }
|
||||||
.stars label {
|
.stars label {
|
||||||
font-size: 48px;
|
font-size: 48px; cursor: pointer;
|
||||||
cursor: pointer;
|
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
transition: color .2s, transform .15s;
|
transition: color .2s, transform .15s;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.stars label:hover,
|
.stars label:hover { transform: scale(1.15); }
|
||||||
.stars label:hover ~ label,
|
.stars input:checked ~ label { color: var(--warning); }
|
||||||
.stars input:checked ~ label {
|
.stars input:checked + label { color: var(--warning); }
|
||||||
color: #f59e0b;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
.stars input:checked + label {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: inherit;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 80px;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color .2s;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
textarea:focus { border-color: var(--primary); }
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px;
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity .2s;
|
|
||||||
}
|
|
||||||
.btn:hover { opacity: .9; }
|
|
||||||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
||||||
|
|
||||||
.rating-text {
|
.rating-text {
|
||||||
font-size: 14px;
|
font-size: 14px; color: var(--text-muted);
|
||||||
color: var(--text-muted);
|
margin-bottom: 24px; min-height: 20px;
|
||||||
margin-bottom: 24px;
|
font-weight: 500;
|
||||||
min-height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Textarea */
|
||||||
|
textarea {
|
||||||
|
width: 100%; padding: 10px 14px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px; font-family: inherit;
|
||||||
|
resize: vertical; min-height: 80px;
|
||||||
|
outline: none; background: var(--surface-2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all .15s;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
textarea:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 0 0 3px rgba(102,126,234,0.12);
|
||||||
|
}
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.btn {
|
||||||
|
width: 100%; padding: 12px 20px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none; border-radius: 8px;
|
||||||
|
font-size: 15px; font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--primary-dark); }
|
||||||
|
.btn:active { transform: scale(0.98); }
|
||||||
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||||
|
|
||||||
|
/* Success */
|
||||||
.success { display: none; }
|
.success { display: none; }
|
||||||
.success .icon { font-size: 64px; margin-bottom: 16px; }
|
.success .icon { font-size: 64px; margin-bottom: 16px; }
|
||||||
.success h2 { color: var(--success); margin-bottom: 8px; }
|
.success h2 { color: var(--success); margin-bottom: 8px; font-size: 20px; font-weight: 700; }
|
||||||
.success p { color: var(--text-muted); }
|
.success p { color: var(--text-muted); font-size: 14px; }
|
||||||
|
|
||||||
|
/* Error */
|
||||||
.erro {
|
.erro {
|
||||||
color: var(--danger);
|
background: var(--danger-bg); color: var(--danger-text);
|
||||||
font-size: 14px;
|
padding: 8px 14px; border-radius: 6px;
|
||||||
margin-top: 12px;
|
font-size: 13px; margin-top: 12px;
|
||||||
display: none;
|
display: none; font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== DARK MODE ===== */
|
||||||
|
body.dark-mode { background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%); }
|
||||||
|
body.dark-mode .container {
|
||||||
|
background: #16213e;
|
||||||
|
box-shadow: 0 32px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body.dark-mode h1 { color: #e0e0e0; }
|
||||||
|
body.dark-mode .subtitle { color: #9ca3af; }
|
||||||
|
body.dark-mode .rating-text { color: #9ca3af; }
|
||||||
|
body.dark-mode textarea {
|
||||||
|
background: #1a1a2e; border-color: #0f3460;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body.dark-mode textarea:focus {
|
||||||
|
border-color: #818cf8;
|
||||||
|
background: #1a1a2e;
|
||||||
|
box-shadow: 0 0 0 3px rgba(129,140,248,0.15);
|
||||||
|
}
|
||||||
|
body.dark-mode textarea::placeholder { color: #6b7280; }
|
||||||
|
body.dark-mode .erro { background: #3b1515; color: #fca5a5; }
|
||||||
|
body.dark-mode .success h2 { color: #4ade80; }
|
||||||
|
body.dark-mode .success p { color: #9ca3af; }
|
||||||
|
|
||||||
|
/* Responsivo */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container { padding: 32px 24px 28px; border-radius: 16px; }
|
||||||
|
h1 { font-size: 20px; }
|
||||||
|
.stars label { font-size: 40px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -135,7 +220,13 @@ textarea:focus { border-color: var(--primary); }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var alias, conversaId, empresaId;
|
(function(){
|
||||||
|
var alias, conversaId, empresaId, nota = 0;
|
||||||
|
|
||||||
|
// Detecta dark mode
|
||||||
|
var m = localStorage.getItem('chatc2_dark_mode_manual');
|
||||||
|
var d = m !== null ? m === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (d) document.body.classList.add('dark-mode');
|
||||||
|
|
||||||
function getParams() {
|
function getParams() {
|
||||||
var params = new URLSearchParams(window.location.search);
|
var params = new URLSearchParams(window.location.search);
|
||||||
@@ -144,27 +235,45 @@ function getParams() {
|
|||||||
empresaId = params.get('empresa');
|
empresaId = params.get('empresa');
|
||||||
|
|
||||||
if (!alias || !conversaId || !empresaId) {
|
if (!alias || !conversaId || !empresaId) {
|
||||||
document.getElementById('erro').textContent = 'Link inválido. Entre em contato conosco.';
|
var erro = document.getElementById('erro');
|
||||||
document.getElementById('erro').style.display = 'block';
|
erro.textContent = 'Link inválido. Contate o suporte.';
|
||||||
document.querySelector('.stars').style.display = 'none';
|
erro.style.display = 'block';
|
||||||
document.querySelector('textarea').style.display = 'none';
|
document.getElementById('starContainer').style.display = 'none';
|
||||||
|
document.getElementById('comentario').style.display = 'none';
|
||||||
document.getElementById('btnEnviar').style.display = 'none';
|
document.getElementById('btnEnviar').style.display = 'none';
|
||||||
|
document.getElementById('ratingText').style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nota = 0;
|
|
||||||
document.querySelectorAll('.stars input').forEach(function(input) {
|
document.querySelectorAll('.stars input').forEach(function(input) {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', function() {
|
||||||
nota = parseInt(this.value);
|
nota = parseInt(this.value);
|
||||||
var textos = ['', 'Péssimo', 'Ruim', 'Regular', 'Bom', 'Excelente!'];
|
var textos = ['', 'Péssimo 😞', 'Ruim 😕', 'Regular 😐', 'Bom 😊', 'Excelente! 🤩'];
|
||||||
document.getElementById('ratingText').textContent = textos[nota] || '';
|
document.getElementById('ratingText').textContent = textos[nota] || '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function enviar() {
|
// Verifica se ja foi avaliado
|
||||||
|
async function verificarAvaliacao() {
|
||||||
|
if (!alias || !conversaId) return;
|
||||||
|
try {
|
||||||
|
var resp = await fetch('/api/' + alias + '/csat/check?conversa=' + conversaId);
|
||||||
|
var data = await resp.json();
|
||||||
|
if (data.success && data.avaliado) {
|
||||||
|
document.getElementById('starContainer').style.display = 'none';
|
||||||
|
document.getElementById('comentario').style.display = 'none';
|
||||||
|
document.getElementById('btnEnviar').style.display = 'none';
|
||||||
|
document.getElementById('ratingText').style.display = 'none';
|
||||||
|
document.getElementById('success').style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignora erro de rede */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.enviar = async function() {
|
||||||
if (nota === 0) {
|
if (nota === 0) {
|
||||||
document.getElementById('erro').textContent = 'Selecione uma avaliação de 1 a 5 estrelas.';
|
var erro = document.getElementById('erro');
|
||||||
document.getElementById('erro').style.display = 'block';
|
erro.textContent = 'Selecione uma avaliação de 1 a 5 estrelas.';
|
||||||
|
erro.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +295,8 @@ async function enviar() {
|
|||||||
});
|
});
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.getElementById('app').querySelector('.stars').style.display = 'none';
|
document.getElementById('starContainer').style.display = 'none';
|
||||||
document.querySelector('textarea').style.display = 'none';
|
document.getElementById('comentario').style.display = 'none';
|
||||||
document.getElementById('btnEnviar').style.display = 'none';
|
document.getElementById('btnEnviar').style.display = 'none';
|
||||||
document.getElementById('ratingText').style.display = 'none';
|
document.getElementById('ratingText').style.display = 'none';
|
||||||
document.getElementById('success').style.display = 'block';
|
document.getElementById('success').style.display = 'block';
|
||||||
@@ -195,17 +304,17 @@ async function enviar() {
|
|||||||
throw new Error(data.error || 'Erro ao enviar');
|
throw new Error(data.error || 'Erro ao enviar');
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
document.getElementById('erro').textContent = 'Erro ao enviar: ' + e.message;
|
var erro = document.getElementById('erro');
|
||||||
document.getElementById('erro').style.display = 'block';
|
erro.textContent = 'Erro ao enviar: ' + e.message;
|
||||||
|
erro.style.display = 'block';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Enviar Avaliação';
|
btn.textContent = 'Enviar Avaliação';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
getParams();
|
getParams();
|
||||||
|
verificarAvaliacao();
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<!-- impeccable-live-start -->
|
|
||||||
<script src="http://localhost:8400/live.js"></script>
|
|
||||||
<!-- impeccable-live-end -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
|
/* Anti-flash: aplicado antes do JS (via script inline no <head>) */
|
||||||
|
html.dark body { background: #1a1a2e; color: #e0e0e0; }
|
||||||
|
|
||||||
|
/* Override de variáveis CSS para dark mode */
|
||||||
|
body.dark-mode {
|
||||||
|
--surface: #16213e;
|
||||||
|
--surface-2: #1a1a2e;
|
||||||
|
--surface-3: #0f0f23;
|
||||||
|
--border: #0f3460;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #d1d5db;
|
||||||
|
--text-muted: #9ca3af;
|
||||||
|
--text-faint: #6b7280;
|
||||||
|
--primary: #818cf8;
|
||||||
|
--primary-dark: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== DARK MODE - Estilos completos ===== */
|
/* ===== DARK MODE - Estilos completos ===== */
|
||||||
html.dark-mode-pending body { visibility: hidden; }
|
|
||||||
body.dark-mode {
|
body.dark-mode {
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
@@ -146,13 +162,13 @@ body.dark-mode ::placeholder {
|
|||||||
|
|
||||||
/* ===== BUTTONS ===== */
|
/* ===== BUTTONS ===== */
|
||||||
body.dark-mode .btn-primary {
|
body.dark-mode .btn-primary {
|
||||||
background: #533483;
|
background: #667eea;
|
||||||
border-color: #533483;
|
border-color: #667eea;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
body.dark-mode .btn-primary:hover {
|
body.dark-mode .btn-primary:hover {
|
||||||
background: #6a4c9c;
|
background: #5a67d8;
|
||||||
border-color: #6a4c9c;
|
border-color: #5a67d8;
|
||||||
}
|
}
|
||||||
body.dark-mode .btn-secondary {
|
body.dark-mode .btn-secondary {
|
||||||
background: #0f3460;
|
background: #0f3460;
|
||||||
@@ -234,6 +250,7 @@ body.dark-mode .badge-primary {
|
|||||||
|
|
||||||
/* ===== MODAL ===== */
|
/* ===== MODAL ===== */
|
||||||
body.dark-mode .modal,
|
body.dark-mode .modal,
|
||||||
|
body.dark-mode .modal-box,
|
||||||
body.dark-mode .modal-content,
|
body.dark-mode .modal-content,
|
||||||
body.dark-mode .modal-overlay > div {
|
body.dark-mode .modal-overlay > div {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
@@ -989,26 +1006,91 @@ body.dark-mode .login-dark-toggle button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Modal no dark mode */
|
/* Modal no dark mode */
|
||||||
body.dark-mode .modal {
|
body.dark-mode .modal,
|
||||||
|
body.dark-mode .modal-box {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
border: 1px solid #0f3460;
|
border: 1px solid #0f3460;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
body.dark-mode .modal h3 { color: #e0e0e0; }
|
body.dark-mode .modal h3,
|
||||||
|
body.dark-mode .modal-box h3 { color: #e0e0e0; }
|
||||||
body.dark-mode .modal input,
|
body.dark-mode .modal input,
|
||||||
body.dark-mode .modal select {
|
body.dark-mode .modal-box input,
|
||||||
|
body.dark-mode .modal select,
|
||||||
|
body.dark-mode .modal-box select,
|
||||||
|
body.dark-mode .modal textarea,
|
||||||
|
body.dark-mode .modal-box textarea {
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border-color: #0f3460;
|
border-color: #0f3460;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
body.dark-mode .modal .btn-group button {
|
body.dark-mode .modal .btn-group button,
|
||||||
|
body.dark-mode .modal-box .btn-group button {
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border-color: #0f3460;
|
border-color: #0f3460;
|
||||||
color: #e0e0e0;
|
color: #c0c0c0;
|
||||||
}
|
}
|
||||||
body.dark-mode .modal .btn-group .btn-primary {
|
/* Settings: tabs no dark mode */
|
||||||
background: #533483;
|
body.dark-mode .tabs { background: #16213e; border-color: #0f3460; }
|
||||||
border-color: #533483;
|
body.dark-mode .tabs button { background: #16213e; color: #9ca3af; }
|
||||||
|
body.dark-mode .tabs button:hover { background: #1a1a2e; color: #c0c0c0; }
|
||||||
|
body.dark-mode .tabs button.active { background: #1a2744; color: #818cf8; border-bottom-color: #818cf8; }
|
||||||
|
/* Botoes do painel direito no dark mode */
|
||||||
|
body.dark-mode [style*="background:#eef2ff"] {
|
||||||
|
background: #1e1b4b !important;
|
||||||
|
border-color: #4c1d95 !important;
|
||||||
|
color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
body.dark-mode [style*="border:1px solid #c7d2fe"] {
|
||||||
|
border-color: #4c1d95 !important;
|
||||||
|
}
|
||||||
|
/* Confirm modal input no dark mode */
|
||||||
|
body.dark-mode #confirmarInput { background: #1a1a2e !important; border-color: #0f3460 !important; color: #e0e0e0 !important; }
|
||||||
|
body.dark-mode #confirmarInput:focus { border-color: #818cf8 !important; background: #1a1a2e !important; box-shadow: 0 0 0 3px rgba(129,140,248,0.15) !important; }
|
||||||
|
/* Tables no dark mode */
|
||||||
|
body.dark-mode table { color: #e0e0e0; }
|
||||||
|
body.dark-mode thead { background: #0f3460; }
|
||||||
|
body.dark-mode th { color: #9ca3af; border-color: #1a1a2e; }
|
||||||
|
body.dark-mode td { border-color: #1a1a2e; color: #d1d5db; }
|
||||||
|
body.dark-mode tr:hover td { background: #1a1a2e; }
|
||||||
|
|
||||||
|
/* Tags no dark mode */
|
||||||
|
body.dark-mode .tag {
|
||||||
|
display: inline-block; padding: 1px 8px;
|
||||||
|
border-radius: 10px; font-size: 11px; font-weight: 600;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flow cards e headers de seção no dark mode */
|
||||||
|
body.dark-mode [style*="background:var(--surface-2)"] {
|
||||||
|
background: #1a1a2e !important;
|
||||||
|
}
|
||||||
|
body.dark-mode [style*="background:#e0e7ff"] {
|
||||||
|
background: #312e81 !important;
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* btn-secondary no dark mode */
|
||||||
|
body.dark-mode .btn-secondary {
|
||||||
|
background: #1a1a2e !important;
|
||||||
|
border-color: #0f3460 !important;
|
||||||
|
color: #d1d5db !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .btn-secondary:hover {
|
||||||
|
background: #0f3460 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input focus no dark mode */
|
||||||
|
body.dark-mode input:focus,
|
||||||
|
body.dark-mode select:focus,
|
||||||
|
body.dark-mode textarea:focus {
|
||||||
|
background: #1a1a2e !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(129,140,248,0.15) !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .modal .btn-group .btn-primary,
|
||||||
|
body.dark-mode .modal-box .btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
border-color: #667eea;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ tr:last-child td { border-bottom: none; }
|
|||||||
|
|
||||||
.modal-overlay.show { display: flex; }
|
.modal-overlay.show { display: flex; }
|
||||||
|
|
||||||
.modal-box {
|
.modal-box, .modal {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
@@ -497,14 +497,14 @@ tr:last-child td { border-bottom: none; }
|
|||||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-box h3 {
|
.modal-box h3, .modal h3 {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer, .modal .modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -37,10 +37,18 @@
|
|||||||
.hm-cell.l3 { background:#30a14e; } .hm-cell.l4 { background:#216e39; }
|
.hm-cell.l3 { background:#30a14e; } .hm-cell.l4 { background:#216e39; }
|
||||||
.hm-months { display:flex; gap:3px; font-size:10px; color:var(--text-faint); margin-bottom:4px; height:12px; }
|
.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; }
|
.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; }
|
body.dark-mode .hm-cell { background:#1e293b; }
|
||||||
|
body.dark-mode .hm-cell.l1 { background:#0e4429; }
|
||||||
|
body.dark-mode .hm-cell.l2 { background:#006d32; }
|
||||||
|
body.dark-mode .hm-cell.l3 { background:#26a641; }
|
||||||
|
body.dark-mode .hm-cell.l4 { background:#39d353; }
|
||||||
|
body.dark-mode .stat-card { background: #16213e !important; box-shadow: none !important; }
|
||||||
|
body.dark-mode .stat-card .num { color: #e0e0e0 !important; }
|
||||||
|
body.dark-mode .stat-card .lbl { color: #c0c0c0 !important; }
|
||||||
|
body.dark-mode .section-title { color: #9ca3af !important; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- SIDEBAR -->
|
<!-- SIDEBAR -->
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
// Dark Mode Controller — Unified for all Chatc2 pages
|
// Dark Mode Controller — Unified for all Chatc2 pages
|
||||||
(function() {
|
(function() {
|
||||||
// Detecta preferência do sistema
|
|
||||||
var osPrefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
var osPrefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
var manualOverride = localStorage.getItem('chatc2_dark_mode_manual');
|
var manualOverride = localStorage.getItem('chatc2_dark_mode_manual');
|
||||||
|
|
||||||
// Decide tema inicial: manual > preferência OS > claro
|
|
||||||
function getInitialTheme() {
|
function getInitialTheme() {
|
||||||
if (manualOverride !== null) return manualOverride === 'true';
|
if (manualOverride !== null) return manualOverride === 'true';
|
||||||
return osPrefersDark.matches;
|
return osPrefersDark.matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply on load immediately (FOUC prevention)
|
|
||||||
if (getInitialTheme()) {
|
|
||||||
document.documentElement.classList.add('dark-mode-pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTheme(enable, isManual) {
|
function applyTheme(enable, isManual) {
|
||||||
document.documentElement.classList.remove('dark-mode-pending');
|
|
||||||
if (document.body) {
|
if (document.body) {
|
||||||
document.body.classList.toggle('dark-mode', enable);
|
document.body.classList.toggle('dark-mode', enable);
|
||||||
}
|
}
|
||||||
@@ -31,7 +23,7 @@
|
|||||||
|
|
||||||
window.darkModeToggle = function() {
|
window.darkModeToggle = function() {
|
||||||
var isDark = localStorage.getItem('chatc2_dark_mode') === 'true';
|
var isDark = localStorage.getItem('chatc2_dark_mode') === 'true';
|
||||||
applyTheme(!isDark, true); // toggle manual
|
applyTheme(!isDark, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.darkModeApply = function(enable) { applyTheme(enable, false); };
|
window.darkModeApply = function(enable) { applyTheme(enable, false); };
|
||||||
@@ -40,19 +32,16 @@
|
|||||||
return localStorage.getItem('chatc2_dark_mode') === 'true';
|
return localStorage.getItem('chatc2_dark_mode') === 'true';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reage a mudanças no tema do sistema (só quando não há override manual)
|
// Reage a mudanças no SO (só sem override manual)
|
||||||
osPrefersDark.addEventListener('change', function(e) {
|
osPrefersDark.addEventListener('change', function(e) {
|
||||||
if (localStorage.getItem('chatc2_dark_mode_manual') === null) {
|
if (localStorage.getItem('chatc2_dark_mode_manual') === null) {
|
||||||
applyTheme(e.matches, false);
|
applyTheme(e.matches, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply after DOM ready
|
// Aplica imediatamente (sem esperar DOMContentLoaded)
|
||||||
if (document.readyState === 'loading') {
|
// Sincroniza html.dark (setado pelo script inline no <head>) com body.dark-mode
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
var htmlDark = document.documentElement.classList.contains('dark');
|
||||||
applyTheme(getInitialTheme(), false);
|
applyTheme(htmlDark || getInitialTheme(), false);
|
||||||
});
|
document.documentElement.classList.remove('dark');
|
||||||
} else {
|
|
||||||
applyTheme(getInitialTheme(), false);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
.empty { text-align:center; padding:50px; color:var(--text-faint); font-size:14px; }
|
.empty { text-align:center; padding:50px; color:var(--text-faint); font-size:14px; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
||||||
|
|||||||
+143
-38
@@ -26,8 +26,17 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
.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 { 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:var(--primary); color:#fff; }
|
.btn-primary { background:var(--primary); color:#fff; }
|
||||||
.btn-primary:hover { background:var(--primary-dark); }
|
.btn-primary:hover { background:var(--primary-dark); }
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 10px 20px; border: 1px solid var(--border);
|
||||||
|
border-radius: 8px; font-size: 14px; font-weight: 600;
|
||||||
|
cursor: pointer; transition: all .15s;
|
||||||
|
background: var(--surface); color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: var(--surface-2); border-color: #d1d5db; }
|
||||||
.btn-danger { background:var(--danger); color:#fff; }
|
.btn-danger { background:var(--danger); color:#fff; }
|
||||||
.btn-danger:hover { background:var(--danger-text); }
|
.btn-danger:hover { background:#dc2626; }
|
||||||
|
.btn-danger:disabled { opacity:0.5; cursor:not-allowed; background:var(--danger); }
|
||||||
.btn-sm { padding:6px 12px; font-size:12px; border-radius:6px; }
|
.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 { display:inline-block; padding:2px 9px; border-radius:10px; font-size:11px; font-weight:600; }
|
||||||
.badge-success { background:var(--success-bg); color:var(--success-text); }
|
.badge-success { background:var(--success-bg); color:var(--success-text); }
|
||||||
@@ -51,7 +60,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/css/dark-mode.css">
|
<link rel="stylesheet" href="/css/dark-mode.css">
|
||||||
<script>if(localStorage.getItem("chatc2_dark_mode")==="true")document.documentElement.classList.add("dark-mode-pending")</script>
|
<script>(function(){var m=localStorage.getItem("chatc2_dark_mode_manual");var d=m!==null?m==="true":window.matchMedia("(prefers-color-scheme: dark)").matches;if(d)document.documentElement.className="dark"})()</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
<aside class="sidebar" role="navigation" aria-label="Navegação principal">
|
||||||
@@ -169,12 +178,34 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
|
|
||||||
<!-- Modal Equipe -->
|
<!-- Modal Equipe -->
|
||||||
<div class="modal-overlay" id="modalEquipe">
|
<div class="modal-overlay" id="modalEquipe">
|
||||||
<div class="modal" style="max-width:400px">
|
<div class="modal-box" style="max-width:480px">
|
||||||
<h3 id="modalEquipeTitle">Nova Equipe</h3>
|
<h3 id="modalEquipeTitle">Nova Equipe</h3>
|
||||||
<input type="hidden" id="editEquipeId">
|
<input type="hidden" id="editEquipeId">
|
||||||
<div class="form-group"><label for="equipeNome">Nome da Equipe</label><input type="text" id="equipeNome" placeholder="Ex: Atendimento"></div>
|
<div class="form-group">
|
||||||
<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>
|
<label for="equipeNome">Nome da Equipe</label>
|
||||||
<div class="form-group"><label>Membros</label><div id="equipeMembros"></div></div>
|
<input type="text" id="equipeNome" placeholder="Ex: Atendimento">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="equipeOrdem">Ordem</label>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<input type="number" id="equipeOrdem" value="0" min="0" style="width:80px">
|
||||||
|
<span style="font-size:12px;color:var(--text-faint)">Menor = aparece primeiro</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="equipeMensagem">Mensagem automática</label>
|
||||||
|
<textarea id="equipeMensagem" rows="2" placeholder="Mensagem enviada quando o cliente escolhe esta equipe..."></textarea>
|
||||||
|
<span style="font-size:11px;color:var(--text-faint);margin-top:4px;display:block">Opcional. Se preenchida, será enviada ao cliente após escolher esta opção.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="equipeEtiqueta">Etiqueta automática</label>
|
||||||
|
<select id="equipeEtiqueta"><option value="">Nenhuma</option></select>
|
||||||
|
<span style="font-size:11px;color:var(--text-faint);margin-top:4px;display:block">Opcional. Etiqueta adicionada automaticamente à conversa.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Membros</label>
|
||||||
|
<div id="equipeMembros"></div>
|
||||||
|
</div>
|
||||||
<div id="equipeFeedback" class="feedback-msg"></div>
|
<div id="equipeFeedback" class="feedback-msg"></div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" onclick="fecharModal('modalEquipe')">Cancelar</button>
|
<button class="btn-secondary" onclick="fecharModal('modalEquipe')">Cancelar</button>
|
||||||
@@ -185,7 +216,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
|
|
||||||
<!-- Modal Etiqueta -->
|
<!-- Modal Etiqueta -->
|
||||||
<div class="modal-overlay" id="modalEtiqueta">
|
<div class="modal-overlay" id="modalEtiqueta">
|
||||||
<div class="modal" style="max-width:400px">
|
<div class="modal-box" style="max-width:400px">
|
||||||
<h3 id="modalEtiquetaTitle">Nova Etiqueta</h3>
|
<h3 id="modalEtiquetaTitle">Nova Etiqueta</h3>
|
||||||
<input type="hidden" id="editEtiquetaId">
|
<input type="hidden" id="editEtiquetaId">
|
||||||
<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="etiquetaNome">Nome</label><input type="text" id="etiquetaNome" placeholder="Ex: Cliente VIP"></div>
|
||||||
@@ -200,7 +231,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
|
|
||||||
<!-- Modal Menu -->
|
<!-- Modal Menu -->
|
||||||
<div class="modal-overlay" id="modalMenu">
|
<div class="modal-overlay" id="modalMenu">
|
||||||
<div class="modal" style="max-width:500px">
|
<div class="modal-box" style="max-width:520px">
|
||||||
<h3 id="modalMenuTitle">Novo Submenu</h3>
|
<h3 id="modalMenuTitle">Novo Submenu</h3>
|
||||||
<input type="hidden" id="editMenuId">
|
<input type="hidden" id="editMenuId">
|
||||||
<input type="hidden" id="editMenuEquipeId">
|
<input type="hidden" id="editMenuEquipeId">
|
||||||
@@ -241,7 +272,7 @@ body { background:var(--surface-3); display:flex; min-height:100vh; }
|
|||||||
|
|
||||||
<!-- Modal Conexão -->
|
<!-- Modal Conexão -->
|
||||||
<div class="modal-overlay" id="modalConexao">
|
<div class="modal-overlay" id="modalConexao">
|
||||||
<div class="modal" style="max-width:500px">
|
<div class="modal-box" style="max-width:520px">
|
||||||
<h3 id="modalConexaoTitle">Nova Conexão WhatsApp</h3>
|
<h3 id="modalConexaoTitle">Nova Conexão WhatsApp</h3>
|
||||||
<input type="hidden" id="editConexaoId">
|
<input type="hidden" id="editConexaoId">
|
||||||
<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="conNome">Nome da Instância</label><input type="text" id="conNome" placeholder="Ex: WhatsApp Comercial"></div>
|
||||||
@@ -311,6 +342,41 @@ window.ativarAba = function(aba, btn) {
|
|||||||
document.getElementById('tab' + aba.charAt(0).toUpperCase() + aba.slice(1)).classList.add('active');
|
document.getElementById('tab' + aba.charAt(0).toUpperCase() + aba.slice(1)).classList.add('active');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== MODAL DE CONFIRMAÇÃO =====
|
||||||
|
let _confirmarCallback = null;
|
||||||
|
|
||||||
|
window.mostrarConfirmacao = function(titulo, mensagem, nomeConfirmar, callback) {
|
||||||
|
document.getElementById('confirmarTitulo').textContent = titulo;
|
||||||
|
document.getElementById('confirmarMensagem').textContent = mensagem;
|
||||||
|
document.getElementById('confirmarInput').value = '';
|
||||||
|
document.getElementById('confirmarInput').placeholder = 'Digite: ' + nomeConfirmar;
|
||||||
|
document.getElementById('btnConfirmarExclusao').disabled = true;
|
||||||
|
_confirmarCallback = callback;
|
||||||
|
_initConfirmModal();
|
||||||
|
document.getElementById('modalConfirmar').classList.add('show');
|
||||||
|
document.getElementById('confirmarInput').focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listeners do modal de confirmação (inicializados após DOM pronto)
|
||||||
|
function _initConfirmModal() {
|
||||||
|
var inp = document.getElementById('confirmarInput');
|
||||||
|
if (!inp || inp._confirmInit) return;
|
||||||
|
inp._confirmInit = true;
|
||||||
|
inp.addEventListener('input', function() {
|
||||||
|
var esperado = this.placeholder.replace('Digite: ', '');
|
||||||
|
document.getElementById('btnConfirmarExclusao').disabled = this.value.trim() !== esperado;
|
||||||
|
});
|
||||||
|
inp.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && !document.getElementById('btnConfirmarExclusao').disabled) {
|
||||||
|
document.getElementById('btnConfirmarExclusao').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('btnConfirmarExclusao').addEventListener('click', function() {
|
||||||
|
if (_confirmarCallback) { _confirmarCallback(); _confirmarCallback = null; }
|
||||||
|
fecharModal('modalConfirmar');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ===== EQUIPES =====
|
// ===== EQUIPES =====
|
||||||
async function carregarEquipes() {
|
async function carregarEquipes() {
|
||||||
var data = await api('/teams?empresaId=' + empresaId);
|
var data = await api('/teams?empresaId=' + empresaId);
|
||||||
@@ -322,9 +388,10 @@ async function carregarEquipes() {
|
|||||||
div.innerHTML = '<table><thead><tr><th>Ordem</th><th>Nome</th><th>Membros</th><th>Ações</th></tr></thead><tbody>' +
|
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) {
|
data.data.map(function(eq) {
|
||||||
var membros = (eq.membros || []).map(function(m) { return m.nome; }).join(', ') || '-';
|
var membros = (eq.membros || []).map(function(m) { return m.nome; }).join(', ') || '-';
|
||||||
|
var mensagemEsc = (eq.mensagem || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||||
return '<tr><td>' + (eq.ordem || 0) + '</td><td><strong>' + eq.nome + '</strong></td><td>' + membros + '</td><td>' +
|
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:var(--surface-3);margin-right:4px">✏️</button>' +
|
'<button class="btn btn-sm" onclick="editarEquipe(' + eq.id + ',' + (eq.ordem || 0) + ',\'' + eq.nome.replace(/'/g,"\\'") + '\',\'' + mensagemEsc + '\',' + (eq.etiquetaId || 'null') + ')" style="background:var(--surface-3);margin-right:4px">✏️</button>' +
|
||||||
'<button class="btn btn-sm btn-danger" onclick="excluirEquipe(' + eq.id + ')">🗑️</button></td></tr>';
|
'<button class="btn btn-sm btn-danger" onclick="excluirEquipe(' + eq.id + ',\'' + eq.nome.replace(/'/g,"\\'") + '\')">🗑️</button></td></tr>';
|
||||||
}).join('') + '</tbody></table>';
|
}).join('') + '</tbody></table>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,16 +399,22 @@ window.mostrarModalEquipe = function() {
|
|||||||
document.getElementById('editEquipeId').value = '';
|
document.getElementById('editEquipeId').value = '';
|
||||||
document.getElementById('equipeOrdem').value = 0;
|
document.getElementById('equipeOrdem').value = 0;
|
||||||
document.getElementById('equipeNome').value = '';
|
document.getElementById('equipeNome').value = '';
|
||||||
|
document.getElementById('equipeMensagem').value = '';
|
||||||
|
document.getElementById('equipeEtiqueta').value = '';
|
||||||
document.getElementById('modalEquipeTitle').textContent = 'Nova Equipe';
|
document.getElementById('modalEquipeTitle').textContent = 'Nova Equipe';
|
||||||
carregarUsuariosCheckbox();
|
carregarUsuariosCheckbox();
|
||||||
|
carregarEtiquetasDropdown();
|
||||||
document.getElementById('modalEquipe').classList.add('show');
|
document.getElementById('modalEquipe').classList.add('show');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.editarEquipe = function(id, ordem, nome) {
|
window.editarEquipe = async function(id, ordem, nome, mensagem, etiquetaId) {
|
||||||
document.getElementById('editEquipeId').value = id;
|
document.getElementById('editEquipeId').value = id;
|
||||||
document.getElementById('equipeOrdem').value = ordem;
|
document.getElementById('equipeOrdem').value = ordem;
|
||||||
document.getElementById('equipeNome').value = nome;
|
document.getElementById('equipeNome').value = nome;
|
||||||
|
document.getElementById('equipeMensagem').value = mensagem || '';
|
||||||
|
document.getElementById('equipeEtiqueta').value = etiquetaId || '';
|
||||||
document.getElementById('modalEquipeTitle').textContent = 'Editar Equipe';
|
document.getElementById('modalEquipeTitle').textContent = 'Editar Equipe';
|
||||||
|
await carregarEtiquetasDropdown();
|
||||||
carregarUsuariosCheckbox(id);
|
carregarUsuariosCheckbox(id);
|
||||||
document.getElementById('modalEquipe').classList.add('show');
|
document.getElementById('modalEquipe').classList.add('show');
|
||||||
};
|
};
|
||||||
@@ -364,27 +437,41 @@ async function carregarUsuariosCheckbox(equipeId) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function carregarEtiquetasDropdown() {
|
||||||
|
var sel = document.getElementById('equipeEtiqueta');
|
||||||
|
var data = await api('/labels?empresaId=' + empresaId);
|
||||||
|
sel.innerHTML = '<option value="">Nenhuma</option>';
|
||||||
|
if (data.success && data.data) {
|
||||||
|
data.data.forEach(function(l) {
|
||||||
|
sel.innerHTML += '<option value="' + l.id + '">' + l.nome + '</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.salvarEquipe = async function() {
|
window.salvarEquipe = async function() {
|
||||||
var id = document.getElementById('editEquipeId').value;
|
var id = document.getElementById('editEquipeId').value;
|
||||||
var nome = document.getElementById('equipeNome').value.trim();
|
var nome = document.getElementById('equipeNome').value.trim();
|
||||||
var ordem = parseInt(document.getElementById('equipeOrdem').value) || 0;
|
var ordem = parseInt(document.getElementById('equipeOrdem').value) || 0;
|
||||||
var membros = Array.from(document.querySelectorAll('#equipeMembros input:checked')).map(function(cb) { return parseInt(cb.value); });
|
var membros = Array.from(document.querySelectorAll('#equipeMembros input:checked')).map(function(cb) { return parseInt(cb.value); });
|
||||||
|
var mensagem = document.getElementById('equipeMensagem').value.trim();
|
||||||
|
var etiquetaId = document.getElementById('equipeEtiqueta').value || null;
|
||||||
|
|
||||||
if (!nome) { mostrarFeedbackModal('equipeFeedback', 'Informe o nome da equipe', 'erro'); return; }
|
if (!nome) { mostrarFeedbackModal('equipeFeedback', 'Informe o nome da equipe', 'erro'); return; }
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
var r = await api('/teams/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros }) });
|
var r = await api('/teams/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros, mensagem: mensagem, etiquetaId: etiquetaId }) });
|
||||||
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
|
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
|
||||||
} else {
|
} else {
|
||||||
var r = await api('/teams', { method: 'POST', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros, empresaId: empresaId }) });
|
var r = await api('/teams', { method: 'POST', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros, empresaId: empresaId, mensagem: mensagem, etiquetaId: etiquetaId }) });
|
||||||
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
|
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.excluirEquipe = async function(id) {
|
window.excluirEquipe = async function(id, nome) {
|
||||||
if (!confirm('Excluir esta equipe?')) return;
|
mostrarConfirmacao('Excluir Equipe', 'Esta ação não pode ser desfeita. Todos os submenus vinculados serão perdidos.', nome || 'equipe', async function() {
|
||||||
var r = await api('/teams/' + id, { method: 'DELETE' });
|
var r = await api('/teams/' + id, { method: 'DELETE' });
|
||||||
if (r.success) carregarEquipes();
|
if (r.success) carregarEquipes();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== USUÁRIOS =====
|
// ===== USUÁRIOS =====
|
||||||
@@ -649,9 +736,10 @@ window.salvarMenu = async function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.excluirMenu = async function(id, titulo) {
|
window.excluirMenu = async function(id, titulo) {
|
||||||
if (!confirm('Excluir o submenu "' + titulo + '" e todos os seus sub-itens?')) return;
|
mostrarConfirmacao('Excluir Submenu', 'O submenu "' + titulo + '" e todos os seus sub-itens serão removidos.', titulo, async function() {
|
||||||
var r = await api('/menus/' + id, { method: 'DELETE' });
|
var r = await api('/menus/' + id, { method: 'DELETE' });
|
||||||
if (r.success) carregarMenus();
|
if (r.success) carregarMenus();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== ETIQUETAS =====
|
// ===== ETIQUETAS =====
|
||||||
@@ -666,7 +754,7 @@ async function carregarEtiquetas() {
|
|||||||
data.data.map(function(l) {
|
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>' +
|
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:var(--surface-3);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>';
|
'<button class="btn btn-sm btn-danger" onclick="excluirEtiqueta(' + l.id + ',\'' + l.nome.replace(/'/g,"\\'") + '\')">🗑️</button></td></tr>';
|
||||||
}).join('') + '</tbody></table>';
|
}).join('') + '</tbody></table>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,10 +789,11 @@ window.salvarEtiqueta = async function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.excluirEtiqueta = async function(id) {
|
window.excluirEtiqueta = async function(id, nome) {
|
||||||
if (!confirm('Excluir esta etiqueta?')) return;
|
mostrarConfirmacao('Excluir Etiqueta', 'A etiqueta será removida permanentemente.', nome, async function() {
|
||||||
var r = await api('/labels/' + id, { method: 'DELETE' });
|
var r = await api('/labels/' + id, { method: 'DELETE' });
|
||||||
if (r.success) carregarEtiquetas();
|
if (r.success) carregarEtiquetas();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== CONFIGURAÇÕES EMPRESA =====
|
// ===== CONFIGURAÇÕES EMPRESA =====
|
||||||
@@ -784,7 +873,7 @@ async function carregarResolucao() {
|
|||||||
? md.data.map(function(m) {
|
? md.data.map(function(m) {
|
||||||
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">' +
|
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>' +
|
'<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>';
|
'<button class="btn btn-sm" style="color:#dc2626;background:none;border:none;cursor:pointer" onclick="removerMotivo(' + m.id + ',\'' + escc(m.descricao).replace(/'/g,"\\'") + '\')">🗑️</button></div>';
|
||||||
}).join('')
|
}).join('')
|
||||||
: '<p style="color:var(--text-faint);font-size:13px">Nenhum motivo cadastrado.</p>';
|
: '<p style="color:var(--text-faint);font-size:13px">Nenhum motivo cadastrado.</p>';
|
||||||
}
|
}
|
||||||
@@ -806,10 +895,11 @@ window.adicionarMotivo = async function() {
|
|||||||
if (r.success) { inp.value = ''; carregarResolucao(); } else mostrarFeedbackResolucao(r.error || 'Erro ao adicionar motivo', 'erro');
|
if (r.success) { inp.value = ''; carregarResolucao(); } else mostrarFeedbackResolucao(r.error || 'Erro ao adicionar motivo', 'erro');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.removerMotivo = async function(id) {
|
window.removerMotivo = async function(id, descricao) {
|
||||||
if (!confirm('Remover este motivo?')) return;
|
mostrarConfirmacao('Remover Motivo', 'O motivo será removido permanentemente.', descricao, async function() {
|
||||||
var r = await api('/motivos/' + id, { method: 'DELETE' });
|
var r = await api('/motivos/' + id, { method: 'DELETE' });
|
||||||
if (r.success) carregarResolucao(); else mostrarFeedbackResolucao(r.error || 'Erro ao remover', 'erro');
|
if (r.success) carregarResolucao(); else mostrarFeedbackResolucao(r.error || 'Erro ao remover', 'erro');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function mostrarFeedbackResolucao(msg, tipo) {
|
function mostrarFeedbackResolucao(msg, tipo) {
|
||||||
@@ -899,13 +989,11 @@ window.editarConexao = async function(id) {
|
|||||||
|
|
||||||
// ===== EXCLUIR CONEXÃO =====
|
// ===== EXCLUIR CONEXÃO =====
|
||||||
window.excluirConexao = async function(id, nome) {
|
window.excluirConexao = async function(id, nome) {
|
||||||
if (!confirm('Excluir a conexão "' + nome + '"?')) return;
|
mostrarConfirmacao('Excluir Conexao', 'A conexao WhatsApp "' + nome + '" sera removida.', nome, async function() {
|
||||||
var r = await api('/evolution/instances/' + id, { method: 'DELETE' });
|
var r = await api('/evolution/instances/' + id, { method: 'DELETE' });
|
||||||
if (r.success) {
|
if (r.success) carregarConexoes();
|
||||||
carregarConexoes();
|
else mostrarFeedbackModal('conexaoFeedback', 'Erro: ' + r.error, 'erro');
|
||||||
} else {
|
});
|
||||||
mostrarFeedbackModal('conexaoFeedback', 'Erro: ' + r.error, 'erro');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.salvarConexao = async function() {
|
window.salvarConexao = async function() {
|
||||||
@@ -969,6 +1057,23 @@ carregarResolucao();
|
|||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<!-- Modal Confirmação de Exclusão -->
|
||||||
|
<div class="modal-overlay" id="modalConfirmar">
|
||||||
|
<div class="modal-box" style="max-width:420px">
|
||||||
|
<h3 id="confirmarTitulo">Confirmar exclusão</h3>
|
||||||
|
<p id="confirmarMensagem" style="font-size:14px;color:var(--text-secondary);margin-bottom:14px;line-height:1.5"></p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmarInput" style="font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:5px">Digite o nome para confirmar:</label>
|
||||||
|
<input type="text" id="confirmarInput" placeholder="Digite exatamente o nome..." style="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" onfocus="this.style.borderColor='var(--primary)';this.style.background='var(--surface)'" onblur="this.style.borderColor='var(--border)';this.style.background='var(--surface-2)'">
|
||||||
|
</div>
|
||||||
|
<div id="confirmarFeedback" class="feedback-msg"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" onclick="fecharModal('modalConfirmar')">Cancelar</button>
|
||||||
|
<button class="btn-danger" id="btnConfirmarExclusao" disabled>Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/dark-mode.js"></script>
|
<script src="/js/dark-mode.js"></script>
|
||||||
<!-- impeccable-live-start -->
|
<!-- impeccable-live-start -->
|
||||||
<script src="http://localhost:8400/live.js"></script>
|
<script src="http://localhost:8400/live.js"></script>
|
||||||
|
|||||||
@@ -63,4 +63,7 @@ router.get('/:alias/clients/:id/convalescentes', ClientController.clientConvales
|
|||||||
// Historico de conversas do cliente (inclui dependentes)
|
// Historico de conversas do cliente (inclui dependentes)
|
||||||
router.get('/:alias/clients/:id/conversations', ClientController.clientConversations);
|
router.get('/:alias/clients/:id/conversations', ClientController.clientConversations);
|
||||||
|
|
||||||
|
// Atualizar data de agendamento de cobrança
|
||||||
|
router.put('/:alias/clients/:clienteId/carne/:carneId/agendamento', authenticateToken, ClientController.atualizarAgendamento);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ router.get('/api/:alias/webhook/ping', EvolutionController.webhookPing);
|
|||||||
router.get('/api/:alias/media/:mediaId', require('../controllers/chatController').getMedia);
|
router.get('/api/:alias/media/:mediaId', require('../controllers/chatController').getMedia);
|
||||||
|
|
||||||
// CSAT (sem auth - formulário público de avaliação)
|
// CSAT (sem auth - formulário público de avaliação)
|
||||||
|
router.get('/api/:alias/csat/check', require('../controllers/chatController').csatCheck);
|
||||||
router.post('/api/:alias/csat/avaliar', require('../controllers/chatController').csatAvaliar);
|
router.post('/api/:alias/csat/avaliar', require('../controllers/chatController').csatAvaliar);
|
||||||
|
|
||||||
// Métricas (apenas gerentes)
|
// Métricas (apenas gerentes)
|
||||||
|
|||||||
Reference in New Issue
Block a user