Fix: WhatsApp deletion flow, AdminBell attachments and UI stability

This commit is contained in:
Sidney 2026-05-13 09:25:19 -03:00
parent bfb2bc12db
commit 5a767ab87b
5 changed files with 18 additions and 10 deletions

View File

@ -60,3 +60,4 @@
31. **Real-Time Bidirectional Sync**: Asaas webhooks MUST update both the PostgreSQL `alunos_cobrancas` table and the legacy `school_data.json` file in real-time to ensure immediate UI feedback in the administrative panel. 31. **Real-Time Bidirectional Sync**: Asaas webhooks MUST update both the PostgreSQL `alunos_cobrancas` table and the legacy `school_data.json` file in real-time to ensure immediate UI feedback in the administrative panel.
32. **Numerical Type Enforcement**: When serving data from PostgreSQL `NUMERIC` columns, the backend MUST map the result to ensure amounts are sent as `Number` (not String) to prevent string concatenation bugs in the frontend. 32. **Numerical Type Enforcement**: When serving data from PostgreSQL `NUMERIC` columns, the backend MUST map the result to ensure amounts are sent as `Number` (not String) to prevent string concatenation bugs in the frontend.
33. **Reverse Sync Requirement (SQL -> JSON)**: To ensure visual consistency and avoid automation errors (like double billing), the system MUST perform a reverse sync from `alunos_cobrancas` (SQL) to `school_data.payments` (JSON) during server initialization and on-demand via the `/api/admin/sync-finance-json` endpoint. 33. **Reverse Sync Requirement (SQL -> JSON)**: To ensure visual consistency and avoid automation errors (like double billing), the system MUST perform a reverse sync from `alunos_cobrancas` (SQL) to `school_data.payments` (JSON) during server initialization and on-demand via the `/api/admin/sync-finance-json` endpoint.
34. **Deletion & Notification Order (Async)**: When processing deletions that trigger notifications (e.g., WhatsApp via Asaas Webhook), local database deletion MUST happen only AFTER the notification dispatch. Manual deletion routes should only trigger the external API delete and wait for the webhook to finalize local cleanup, ensuring data availability for message variables.

View File

@ -37,6 +37,10 @@
- [x] **Paridade Lógica Absoluta (Frequência):** A lógica de matching de frequência do Portal (`Frequencia.tsx` e `Dashboard.tsx`) agora é um clone 100% idêntico ao do Manager (`AttendanceQuery.tsx`). - [x] **Paridade Lógica Absoluta (Frequência):** A lógica de matching de frequência do Portal (`Frequencia.tsx` e `Dashboard.tsx`) agora é um clone 100% idêntico ao do Manager (`AttendanceQuery.tsx`).
- [x] **Fix Janela de Matching (Portal):** Corrigido bug onde aulas sem `endTime` fechavam a janela de presença prematuramente (fallback alterado de `00:00:00` para `23:59:00`). - [x] **Fix Janela de Matching (Portal):** Corrigido bug onde aulas sem `endTime` fechavam a janela de presença prematuramente (fallback alterado de `00:00:00` para `23:59:00`).
- [x] **Resolução de Justificativas Invisíveis:** Justificativas enviadas agora são corretamente mapeadas às aulas no Portal através da sincronização de janelas de tempo e IDs. - [x] **Resolução de Justificativas Invisíveis:** Justificativas enviadas agora são corretamente mapeadas às aulas no Portal através da sincronização de janelas de tempo e IDs.
- [x] **Correção do Fluxo de Exclusão (WhatsApp):** Ajustada a ordem de exclusão no `server.selfhosted.js` e `Finance.tsx`. A limpeza local agora ocorre apenas após o disparo bem-sucedido do WhatsApp via Webhook, garantindo que as variáveis `{nome}` e `{descricao}` nunca cheguem vazias.
- [x] **Feedback Real de Exclusão:** Implementado `showAlert` detalhado no Financeiro e confirmação real via **Sino de Notificações** (Admin) após o processamento do Webhook.
- [x] **Refinamento do Sino (AdminBell):** O botão "Ver Anexo" agora é exclusivo de notificações de justificativa que possuem arquivo físico, ocultando-se em notificações de sistema que usam o campo anexo para metadados JSON.
- [x] **Blindagem de UI (Financeiro):** Corrigido potencial erro de referência no parse de erro do Asaas que causava "White Screen" em exclusões negadas.
## 📋 Próximos Passos ## 📋 Próximos Passos
- [ ] Iniciar a migração do módulo Financeiro para 100% SQL seguindo o padrão do Boletim. - [ ] Iniciar a migração do módulo Financeiro para 100% SQL seguindo o padrão do Boletim.

View File

@ -311,7 +311,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
<CheckCircle size={12} /> Aceitar <CheckCircle size={12} /> Aceitar
</button> </button>
)} )}
{finalAttachment && ( {isJustificativa && finalAttachment && !String(finalAttachment).startsWith('{') && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@ -712,10 +712,8 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
body: JSON.stringify({ id: asaasIdToDelete }) body: JSON.stringify({ id: asaasIdToDelete })
}); });
const result = await response.json();
if (response.ok) { if (response.ok) {
showAlert('Sucesso', 'Cobrança excluída com sucesso.', 'success'); showAlert('Exclusão Processada', '✅ Cobrança removida no Asaas. O envio do WhatsApp e a limpeza local ocorrerão em segundos. Confira a confirmação final no Sino de Notificações.', 'success');
// SO atualiza se backend confirmou (200 OK) // SO atualiza se backend confirmou (200 OK)
let updatedPayments = [...data.payments]; let updatedPayments = [...data.payments];
@ -727,6 +725,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
updateData({ payments: updatedPayments }); updateData({ payments: updatedPayments });
closeModal(); closeModal();
} else { } else {
const result = await response.json().catch(() => ({}));
showAlert('Erro', result.error || 'Não é possível excluir. Verifique se já foi paga.', 'error'); showAlert('Erro', result.error || 'Não é possível excluir. Verifique se já foi paga.', 'error');
} }
} catch (error) { } catch (error) {

View File

@ -804,9 +804,17 @@ app.post('/api/webhook_asaas', async (req, res) => {
cancelCache.add(installmentId); cancelCache.add(installmentId);
setTimeout(() => cancelCache.delete(installmentId), 60000); setTimeout(() => cancelCache.delete(installmentId), 60000);
} }
await sendEvolutionMessage(asaasPaymentId, 'PAYMENT_DELETED'); const sent = await sendEvolutionMessage(asaasPaymentId, 'PAYMENT_DELETED');
await deleteCobranca(asaasPaymentId); await deleteCobranca(asaasPaymentId);
addLog('Webhook', 'PAYMENT_DELETED', { asaasPaymentId });
if (sent) {
createAdminNotification('✅ WhatsApp Enviado', `O aluno ${targetName} foi notificado sobre o cancelamento da cobrança.`, { type: 'whatsapp', status: 'success' });
addLog('WhatsApp', 'Cancelamento Enviado', { aluno: targetName, asaasPaymentId });
} else {
createAdminNotification('⚠️ Falha no WhatsApp', `Não foi possível enviar a notificação de cancelamento para ${targetName}.`, { type: 'whatsapp', status: 'error' });
addLog('WhatsApp', 'Erro no Cancelamento', { aluno: targetName, asaasPaymentId });
}
return res.status(200).send('OK'); return res.status(200).send('OK');
default: default:
@ -1144,15 +1152,11 @@ app.post('/api/excluir_cobranca', async (req, res) => {
const resp = await fetch(`${ASAAS_BASE_URL}/v3/installments/${asaasTargetId}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } }); const resp = await fetch(`${ASAAS_BASE_URL}/v3/installments/${asaasTargetId}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
if (resp.ok) { if (resp.ok) {
addLog('Asaas', 'Exclusão Parcelamento OK', { id: asaasTargetId }); addLog('Asaas', 'Exclusão Parcelamento OK', { id: asaasTargetId });
// Exclusão imediata no SQL local para evitar que reapareça na UI antes do webhook
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_installment_id = $1', [asaasTargetId]);
} }
} else { } else {
const resp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${id}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } }); const resp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${id}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); } if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); }
// Exclusão imediata no SQL local
await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1', [id]);
addLog('Asaas', 'Exclusão Cobrança OK', { id }); addLog('Asaas', 'Exclusão Cobrança OK', { id });
} }