fix: estabilização final dos lembretes, correção definitiva de timezone e ferramentas de debug para mensagens
This commit is contained in:
parent
db7b79fe87
commit
161b074bf2
|
|
@ -107,7 +107,7 @@
|
|||
- **Melhoria:** O build agora ocorre diretamente na arquitetura de destino, sem emulação QEMU, garantindo velocidade e estabilidade total.
|
||||
|
||||
### 📢 Automação de Mensagens
|
||||
- [x] **Estabilização de Lembretes:** Sistema de avisos preventivos e de inadimplência agora é 100% confiável, com tracking preciso de envios e suporte a múltiplos templates dinâmicos.
|
||||
- [x] **Estabilização de Lembretes (V2):** Resolvido bug de deslocamento de fuso horário (-1 dia) nas datas de vencimento. Implementadas ferramentas de debug (ignorar trava diária e reset de contadores) e **controle manual de delay para disparos em massa** para facilitar testes e garantir segurança contra banimento.
|
||||
|
||||
## 📋 Próximos Passos Pendentes
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
|
|||
const [targetId, setTargetId] = useState('');
|
||||
const [messageText, setMessageText] = useState('');
|
||||
const [isSendingMass, setIsSendingMass] = useState(false);
|
||||
const [massDelay, setMassDelay] = useState('60');
|
||||
const [isSendingBdays, setIsSendingBdays] = useState(false);
|
||||
|
||||
// Modal de Edição de Modelo
|
||||
|
|
@ -237,7 +238,11 @@ const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
|
|||
const resp = await fetch('/api/enviar-massa', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alunos: payloadAlunos, mensagem: normalizeLineBreaks(messageText) })
|
||||
body: JSON.stringify({
|
||||
alunos: payloadAlunos,
|
||||
mensagem: normalizeLineBreaks(messageText),
|
||||
delay: parseInt(massDelay) || 60
|
||||
})
|
||||
});
|
||||
const resData = await resp.json();
|
||||
|
||||
|
|
@ -366,6 +371,21 @@ const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/50 p-3 rounded-xl border border-emerald-100 mb-2">
|
||||
<label className="block text-[9px] font-black text-emerald-600 uppercase tracking-widest mb-1.5 ml-1">Intervalo Mínimo (Segundos)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="10" max="600"
|
||||
value={massDelay}
|
||||
onChange={(e) => setMassDelay(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-emerald-200 rounded-lg text-sm font-bold text-center focus:ring-emerald-500 focus:outline-none"
|
||||
/>
|
||||
<div className="text-[10px] text-emerald-400 font-bold whitespace-nowrap">seg/msg</div>
|
||||
</div>
|
||||
<p className="text-[8px] text-emerald-400 mt-1 font-medium leading-tight">Recomendado: 60s ou mais para evitar banimento.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleMassSend}
|
||||
disabled={isSendingMass || !data.evolutionConfig?.apiUrl}
|
||||
|
|
@ -385,15 +405,52 @@ const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
|
|||
<p className="text-[10px] text-indigo-600 font-medium mb-4">
|
||||
Envia avisos para boletos que vencem em até {templates.automationRules.sendDaysBefore} dias.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleDispararPreventivos}
|
||||
disabled={isSendingPreventive || !data.evolutionConfig?.apiUrl}
|
||||
className={`w-full py-3.5 px-4 rounded-xl font-black text-sm text-white shadow-lg transition-all active:scale-95 ${
|
||||
isSendingPreventive || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-indigo-600 hover:bg-indigo-700'
|
||||
}`}
|
||||
>
|
||||
{isSendingPreventive ? 'Processando...' : 'Enviar Lembretes Agora'}
|
||||
</button>
|
||||
<div className="space-y-3 mb-4">
|
||||
<button
|
||||
onClick={handleDispararPreventivos}
|
||||
disabled={isSendingPreventive || !data.evolutionConfig?.apiUrl}
|
||||
className={`w-full py-3.5 px-4 rounded-xl font-black text-sm text-white shadow-lg transition-all active:scale-95 ${
|
||||
isSendingPreventive || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-indigo-600 hover:bg-indigo-700'
|
||||
}`}
|
||||
>
|
||||
{isSendingPreventive ? 'Processando...' : 'Enviar Lembretes Agora'}
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col gap-2 p-3 bg-white/50 rounded-xl border border-indigo-100">
|
||||
<label className="flex items-center gap-2 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!templates.automationRules.ignoreDailyLock}
|
||||
onChange={(e) => {
|
||||
const newRules = { ...templates.automationRules, ignoreDailyLock: e.target.checked };
|
||||
setTemplates(prev => ({ ...prev, automationRules: newRules }));
|
||||
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
|
||||
}}
|
||||
className="w-4 h-4 rounded border-indigo-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-[10px] font-black text-indigo-700 uppercase tracking-tight group-hover:text-indigo-900 transition-colors">
|
||||
Ignorar trava de envio diário (Debug)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
showConfirm('Zerar Contadores', 'Tem certeza que deseja zerar todos os contadores de avisos (preventivos e atrasos) de todos os alunos? Isso permitirá que eles recebam os avisos novamente.', async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/admin/reset-cobrancas-counters', { method: 'POST' });
|
||||
if (resp.ok) showAlert('Sucesso', 'Todos os contadores foram zerados!', 'success');
|
||||
else showAlert('Erro', 'Falha ao zerar contadores.', 'error');
|
||||
} catch (e) {
|
||||
showAlert('Erro', 'Erro de conexão.', 'error');
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="w-full py-2 px-3 bg-white border border-indigo-200 text-indigo-600 rounded-lg font-black text-[9px] uppercase tracking-widest hover:bg-indigo-50 hover:border-indigo-300 transition-all active:scale-95"
|
||||
>
|
||||
Zerar Contadores de Avisos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agendamento Automático */}
|
||||
<div className="mt-5 pt-5 border-t border-indigo-200">
|
||||
|
|
|
|||
|
|
@ -937,13 +937,13 @@ app.put('/api/notificacoes/remover-anexo/:id', async (req, res) => {
|
|||
// Disparo em Massa
|
||||
// ============================================================
|
||||
app.post('/api/enviar-massa', (req, res) => {
|
||||
const { alunos, mensagem } = req.body;
|
||||
const { alunos, mensagem, delay } = req.body;
|
||||
if (!alunos || !Array.isArray(alunos) || alunos.length === 0) return res.status(400).json({ error: 'Nenhum aluno.' });
|
||||
res.status(200).json({ success: true, message: 'Background iniciado.' });
|
||||
processarFilaWhatsApp(alunos, mensagem);
|
||||
processarFilaWhatsApp(alunos, mensagem, delay || 60);
|
||||
});
|
||||
|
||||
async function processarFilaWhatsApp(alunos, mensagemTemplate) {
|
||||
async function processarFilaWhatsApp(alunos, mensagemTemplate, customDelay = 60) {
|
||||
const appData = await getSchoolData();
|
||||
const evoConfig = appData?.evolutionConfig;
|
||||
if (!evoConfig?.apiUrl || !evoConfig?.apiKey || !evoConfig?.instanceName) return;
|
||||
|
|
@ -957,7 +957,11 @@ async function processarFilaWhatsApp(alunos, mensagemTemplate) {
|
|||
const url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/sendText/${evoConfig.instanceName}`;
|
||||
await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify({ number: cleanPhone, text: msg }) });
|
||||
} catch (error) { console.error(`[Massa] Erro ${aluno.nome}:`, error.message); }
|
||||
if (i < alunos.length - 1) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 120000) + 60000));
|
||||
if (i < alunos.length - 1) {
|
||||
// Delay base informado pelo usuário + variância aleatória de 0-30s para evitar padrões robóticos
|
||||
const delayMs = (customDelay * 1000) + (Math.floor(Math.random() * 30000));
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1103,6 +1107,27 @@ app.get('/api/alunos/:id/carne', async (req, res) => {
|
|||
// ============================================================
|
||||
// ============================================================
|
||||
// LÓGICA REUTILIZÁVEL DE DISPARO DE COBRANÇAS
|
||||
// ============================================================
|
||||
// Helpers de Data
|
||||
// ============================================================
|
||||
const getLocalSafeDate = (val) => {
|
||||
if (!val) return null;
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
|
||||
// Se for string YYYY-MM-DD pura, o JS cria em UTC.
|
||||
// Forçamos para os componentes locais para evitar o deslocamento de -1 dia.
|
||||
if (typeof val === 'string' && val.includes('-') && !val.includes('T') && !val.includes(':')) {
|
||||
const parts = val.split(' ')[0].split('-');
|
||||
if (parts.length === 3) {
|
||||
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Para objetos Date ou strings com tempo, extraímos o dia civil local
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
async function executarRotinaCobrancas(tipo = 'ambos') {
|
||||
const appData = await getSchoolData();
|
||||
|
|
@ -1124,19 +1149,19 @@ async function executarRotinaCobrancas(tipo = 'ambos') {
|
|||
for (const cob of atrasados) {
|
||||
if (!cob.asaas_payment_id || !cob.vencimento) continue;
|
||||
|
||||
const vencimento = new Date(cob.vencimento);
|
||||
vencimento.setHours(0,0,0,0);
|
||||
const vencimento = getLocalSafeDate(cob.vencimento);
|
||||
if (!vencimento) continue;
|
||||
|
||||
const diffDiasAtraso = Math.floor((hoje.getTime() - vencimento.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDiasAtraso >= sendDaysAfter) {
|
||||
const lastWarn = cob.last_overdue_warning_at ? new Date(cob.last_overdue_warning_at) : null;
|
||||
if (lastWarn) lastWarn.setHours(0,0,0,0);
|
||||
const lastWarn = getLocalSafeDate(cob.last_overdue_warning_at);
|
||||
|
||||
const diasDesdeUltimoAviso = lastWarn
|
||||
? Math.floor((hoje.getTime() - lastWarn.getTime()) / (1000 * 60 * 60 * 24))
|
||||
: null;
|
||||
|
||||
const jaEnviadoHoje = lastWarn && lastWarn.getTime() === hoje.getTime();
|
||||
const jaEnviadoHoje = !rules.ignoreDailyLock && lastWarn && lastWarn.toDateString() === hoje.toDateString();
|
||||
|
||||
if (!jaEnviadoHoje && (diasDesdeUltimoAviso === null || diasDesdeUltimoAviso >= repeatEveryDays)) {
|
||||
const sent = await sendEvolutionMessage(cob.asaas_payment_id, 'PAYMENT_OVERDUE');
|
||||
|
|
@ -1164,8 +1189,8 @@ async function executarRotinaCobrancas(tipo = 'ambos') {
|
|||
for (const cob of pendentes) {
|
||||
if (!cob.asaas_payment_id || !cob.vencimento) continue;
|
||||
|
||||
const vencimento = new Date(cob.vencimento);
|
||||
vencimento.setHours(0,0,0,0);
|
||||
const vencimento = getLocalSafeDate(cob.vencimento);
|
||||
if (!vencimento) continue;
|
||||
|
||||
const diffDias = Math.ceil((vencimento.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const sendOnDueDate = rules.sendOnDueDate !== false;
|
||||
|
|
@ -1174,8 +1199,8 @@ async function executarRotinaCobrancas(tipo = 'ambos') {
|
|||
const currentCount = parseInt(cob.pre_warnings_count) || 0;
|
||||
|
||||
if (currentCount < maxPreWarnings) {
|
||||
const lastWarn = cob.last_pre_warning_at ? new Date(cob.last_pre_warning_at) : null;
|
||||
const jaEnviadoHoje = lastWarn && lastWarn.toDateString() === hoje.toDateString();
|
||||
const lastWarn = getLocalSafeDate(cob.last_pre_warning_at);
|
||||
const jaEnviadoHoje = !rules.ignoreDailyLock && lastWarn && lastWarn.toDateString() === hoje.toDateString();
|
||||
|
||||
if (!jaEnviadoHoje) {
|
||||
const sent = await sendEvolutionMessage(cob.asaas_payment_id, 'PAYMENT_UPCOMING');
|
||||
|
|
@ -1314,6 +1339,16 @@ async function inicializarAgendamento() {
|
|||
|
||||
async function startServer() {
|
||||
|
||||
// Rota para zerar contadores de avisos
|
||||
app.post('/api/admin/reset-cobrancas-counters', async (req, res) => {
|
||||
try {
|
||||
await pool.query('UPDATE alunos_cobrancas SET pre_warnings_count = 0, last_pre_warning_at = NULL, overdue_warnings_count = 0, last_overdue_warning_at = NULL');
|
||||
return res.json({ success: true, message: 'Contadores zerados com sucesso!' });
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Disparo Manual de Inadimplência e Lembretes
|
||||
app.post('/api/disparar_cobrancas', async (req, res) => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue