diff --git a/manager/components/Finance.tsx b/manager/components/Finance.tsx index 9beafb0..052bd6f 100644 --- a/manager/components/Finance.tsx +++ b/manager/components/Finance.tsx @@ -387,6 +387,10 @@ const Finance: React.FC = ({ data, updateData }) => { payments: sorted, studentId: sorted[0].studentId, totalAmount: sorted.reduce((sum, p) => sum + Number(p.amount), 0), + totalReceived: sorted.reduce((sum, p) => { + const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase()); + return sum + (isPaid ? (Number((p as any).valor_pago) || (Number(p.amount) - (Number(p.discount) || 0))) : 0); + }, 0), totalInstallments: sorted[0].totalInstallments || sorted.length, description: sorted[0].description?.split(' (')[0] || 'Parcelamento', dueDate: sorted[0].dueDate @@ -1055,6 +1059,9 @@ const Finance: React.FC = ({ data, updateData }) => {
R$ {group.totalAmount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
+ {group.totalReceived > 0 && ( +
PAGO: R$ {group.totalReceived.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
+ )}
Total do Carnê
@@ -1402,7 +1409,22 @@ const Finance: React.FC = ({ data, updateData }) => { {p.installmentNumber &&
{p.installmentNumber}/{p.totalInstallments}
} {new Date(p.dueDate + 'T12:00:00Z').toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' })} - R$ {p.amount.toFixed(2)} + +
+ R$ {(() => { + const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase()); + const valorPago = (p as any).valor_pago ? Number((p as any).valor_pago) : 0; + const bruto = Math.max(Number(p.amount), Number((p as any).amount_original || 0)); + + if (isPaid && valorPago > 0) return valorPago.toFixed(2); + if (isPaid && p.discount > 0) return (bruto - p.discount).toFixed(2); + return bruto.toFixed(2); + })()} +
+ {['paid', 'pago', 'received', 'confirmed'].includes((p.status || '').toLowerCase()) && (p as any).valor_pago > 0 && ( +
Líquido Recebido
+ )} + {getStatusBadge(p)} {p.asaasPaymentId && ( diff --git a/manager/scratch/mass_sync_valor_pago.cjs b/manager/scratch/mass_sync_valor_pago.cjs new file mode 100644 index 0000000..475659a --- /dev/null +++ b/manager/scratch/mass_sync_valor_pago.cjs @@ -0,0 +1,41 @@ + +const fs = require('fs'); +const path = require('path'); +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager' +}); + +async function massSync() { + console.log('--- Iniciando Sincronização em Massa (SQL -> JSON) ---'); + + try { + const { rows: dbPayments } = await pool.query('SELECT asaas_payment_id, valor_pago FROM alunos_cobrancas WHERE valor_pago > 0'); + console.log(`Encontrados ${dbPayments.length} pagamentos com valor_pago no SQL.`); + + const jsonPath = 'C:/Users/Professor/Downloads/remix_-edumanager---sistema-de-gestão-escolar-para-porteiner/school_data.json'; + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + let updatedCount = 0; + + dbPayments.forEach(dbP => { + const pIdx = data.payments.findIndex(p => p.asaasPaymentId === dbP.asaas_payment_id); + if (pIdx !== -1) { + data.payments[pIdx].valor_pago = Number(dbP.valor_pago); + updatedCount++; + } + }); + + data.lastUpdated = new Date().toISOString(); + fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2)); + + console.log(`--- Sincronização Concluída ---`); + console.log(`JSON atualizado com ${updatedCount} valores reais de pagamento.`); + } catch (err) { + console.error('Erro na sincronização:', err); + } finally { + process.exit(); + } +} + +massSync(); diff --git a/manager/scratch/portal_debug.cjs b/manager/scratch/portal_debug.cjs new file mode 100644 index 0000000..7724946 --- /dev/null +++ b/manager/scratch/portal_debug.cjs @@ -0,0 +1,32 @@ + +const fs = require('fs'); +const path = require('path'); +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager' +}); + +async function debug() { + const studentId = '311709fb-68ab-4168-8684-887b5ec2d731'; + + console.log('--- SQL DATA (Napoleão) ---'); + const { rows } = await pool.query('SELECT asaas_payment_id, valor, amount_original, valor_pago, vencimento, status FROM alunos_cobrancas WHERE aluno_id = $1', [studentId]); + console.table(rows); + + console.log('--- JSON DATA (Napoleão) ---'); + const jsonPath = 'C:/Users/Professor/Downloads/remix_-edumanager---sistema-de-gestão-escolar-para-porteiner/school_data.json'; + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + const payments = data.payments.filter(p => p.studentId === studentId); + console.table(payments.map(p => ({ + asaasPaymentId: p.asaasPaymentId, + amount: p.amount, + discount: p.discount, + dueDate: p.dueDate, + status: p.status + }))); + + process.exit(); +} + +debug(); diff --git a/manager/scratch/portal_debug.js b/manager/scratch/portal_debug.js deleted file mode 100644 index 4477128..0000000 --- a/manager/scratch/portal_debug.js +++ /dev/null @@ -1,89 +0,0 @@ -import pg from 'pg'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const pool = new pg.Pool({ - connectionString: process.env.DATABASE_URL || 'postgresql://edumanager:edumanager2024@localhost:5432/edumanager' -}); - -async function debug() { - try { - // 1. Buscar todos os alunos - const alunos = await pool.query('SELECT id, nome FROM alunos LIMIT 20'); - console.log('\n=== ALUNOS CADASTRADOS ==='); - alunos.rows.forEach(a => console.log(` ${a.id} | ${a.nome}`)); - - // 2. Buscar TODAS as cobranças do SQL - const cobrancas = await pool.query(` - SELECT asaas_payment_id, aluno_id, status, - TO_CHAR(vencimento, 'YYYY-MM-DD') as vencimento, - valor, amount_original, discount, description, type, - TO_CHAR(data_pagamento, 'YYYY-MM-DD') as data_pagamento, - transaction_receipt_url - FROM alunos_cobrancas - ORDER BY aluno_id, vencimento ASC - `); - - console.log(`\n=== COBRANÇAS NO SQL (${cobrancas.rows.length} total) ===`); - const byStudent = {}; - cobrancas.rows.forEach(c => { - if (!byStudent[c.aluno_id]) byStudent[c.aluno_id] = []; - byStudent[c.aluno_id].push(c); - }); - - for (const [alunoId, payments] of Object.entries(byStudent)) { - const aluno = alunos.rows.find(a => a.id === alunoId); - console.log(`\n 📌 ${aluno?.nome || alunoId} (${payments.length} cobranças):`); - payments.forEach(p => { - const status = p.status?.toUpperCase(); - const icon = status === 'PAGO' ? '✅' : status === 'PENDENTE' ? '⏳' : status === 'ATRASADO' ? '🔴' : '❓'; - console.log(` ${icon} ${p.asaas_payment_id} | ${p.vencimento} | R$${Number(p.valor).toFixed(2)} | Status: ${p.status} | Pago em: ${p.data_pagamento || '-'} | Desc: ${p.description || '-'} | amount_original: ${p.amount_original || '-'}`); - }); - } - - // 3. Buscar do JSON - const dataPath = path.join(__dirname, '..', 'data', 'school_data.json'); - if (fs.existsSync(dataPath)) { - const data = JSON.parse(fs.readFileSync(dataPath, 'utf-8')); - const jsonPayments = data.payments || []; - console.log(`\n=== COBRANÇAS NO JSON (${jsonPayments.length} total) ===`); - - const jsonByStudent = {}; - jsonPayments.forEach(p => { - if (!jsonByStudent[p.studentId]) jsonByStudent[p.studentId] = []; - jsonByStudent[p.studentId].push(p); - }); - - for (const [studentId, payments] of Object.entries(jsonByStudent)) { - const aluno = alunos.rows.find(a => a.id === studentId); - console.log(`\n 📌 ${aluno?.nome || studentId} (${payments.length} cobranças no JSON):`); - payments.forEach(p => { - const inSql = cobrancas.rows.find(c => c.asaas_payment_id === p.asaasPaymentId); - console.log(` ${inSql ? '🔗' : '⚠️'} ${p.asaasPaymentId || 'SEM_ID'} | ${p.dueDate} | R$${p.amount} | Status: ${p.status} | Desc: ${p.description || '-'} | discount: ${p.discount || 0} | ${inSql ? 'Existe no SQL' : 'SÓ NO JSON!'}`); - }); - } - - // 4. Verificar cobranças que existem no SQL mas NÃO no JSON - console.log('\n=== COBRANÇAS QUE EXISTEM NO SQL MAS NÃO NO JSON ==='); - let orphanCount = 0; - cobrancas.rows.forEach(c => { - const inJson = jsonPayments.find(p => p.asaasPaymentId === c.asaas_payment_id); - if (!inJson) { - orphanCount++; - console.log(` ⚠️ ${c.asaas_payment_id} | Aluno: ${c.aluno_id} | ${c.vencimento} | R$${Number(c.valor).toFixed(2)} | Status: ${c.status}`); - } - }); - if (orphanCount === 0) console.log(' ✅ Nenhuma — tudo sincronizado.'); - } - - } catch (err) { - console.error('Erro:', err); - } finally { - await pool.end(); - } -} - -debug(); diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index ce3127a..d4521a6 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -802,10 +802,16 @@ app.post('/api/webhook_asaas', async (req, res) => { break; case 'PAYMENT_UPDATED': + updateData = { + valor: payload.payment.value, + vencimento: payload.payment.dueDate, + link_boleto: payload.payment.bankSlipUrl || payload.payment.link || null + }; + // Alerta no Sino (Admin) createAdminNotification( '📝 Cobrança Alterada', - `A cobrança de ${targetName} foi atualizada no Asaas.`, + `A cobrança de ${targetName} foi atualizada no Asaas para R$ ${Number(payload.payment.value).toFixed(2)}.`, { type: 'finance', status: 'updated', paymentId: asaasPaymentId } ); if (payload.event === 'PAYMENT_UPDATED') sendEvolutionMessage(asaasPaymentId, 'PAYMENT_UPDATED'); @@ -866,6 +872,7 @@ app.post('/api/webhook_asaas', async (req, res) => { ...p, status: newStatus, amount: shouldUpdateAmount ? updateData.valor : p.amount, + valor_pago: updateData.valor_pago || p.valor_pago || 0, dueDate: updateData.vencimento || p.dueDate, paidDate: updateData.data_pagamento || p.paidDate }; @@ -1578,40 +1585,49 @@ async function syncPaymentsWithAsaasAPI() { const valorNum = Number(payment.value); - // A. Atualiza SQL (Silencioso) + // A. Atualiza SQL (Prioridade Máxima) + const receivedValue = (internalStatus === 'paid') ? valorNum : 0; await pool.query(` - INSERT INTO alunos_cobrancas (asaas_payment_id, valor, vencimento, status, data_pagamento) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (asaas_payment_id) DO UPDATE SET status = EXCLUDED.status, data_pagamento = EXCLUDED.data_pagamento - `, [payment.id, valorNum, payment.dueDate, internalStatus, payment.confirmedDate || payment.paymentDate]).catch(() => {}); + INSERT INTO alunos_cobrancas (asaas_payment_id, valor, vencimento, status, data_pagamento, valor_pago, amount_original) + VALUES ($1, $2, $3, $4, $5, $6, $2) + ON CONFLICT (asaas_payment_id) DO UPDATE SET + status = EXCLUDED.status, + data_pagamento = EXCLUDED.data_pagamento, + valor_pago = GREATEST(alunos_cobrancas.valor_pago, EXCLUDED.valor_pago), + valor = GREATEST(alunos_cobrancas.valor, EXCLUDED.valor) + `, [payment.id, valorNum, payment.dueDate, internalStatus, payment.confirmedDate || payment.paymentDate, receivedValue]).catch(() => {}); // B. Atualiza JSON const pIdx = appData.payments.findIndex(p => p.asaasPaymentId === payment.id); if (pIdx !== -1) { const newStatus = jsonStatusMap[internalStatus]; + const p = appData.payments[pIdx]; let changed = false; - if (appData.payments[pIdx].status !== newStatus) { - appData.payments[pIdx].status = newStatus; + if (p.status !== newStatus) { + p.status = newStatus; changed = true; } // [Bugfix Crítico]: Não sobrescrever o valor BRUTO com o valor LÍQUIDO (descontado) do Asaas - const currentAmount = Number(appData.payments[pIdx].amount || 0); - const currentDiscount = Number(appData.payments[pIdx].discount || 0); - - // Se o valor vindo do Asaas for menor que o atual E a diferença bater com o desconto, ignoramos o update do valor - // para preservar o valor bruto original no display do portal/gerenciador. + const currentAmount = Number(p.amount || 0); + const currentDiscount = Number(p.discount || 0); const isNetValueOverwrite = valorNum < currentAmount && Math.abs((currentAmount - currentDiscount) - valorNum) < 0.01; - if (appData.payments[pIdx].amount !== valorNum && !isNetValueOverwrite) { - appData.payments[pIdx].amount = valorNum; + if (p.amount !== valorNum && !isNetValueOverwrite) { + p.amount = valorNum; + changed = true; + } + + // Adicionar valor_pago ao JSON para o Manager ler + if (receivedValue > 0 && Number(p.valor_pago || 0) !== receivedValue) { + p.valor_pago = receivedValue; changed = true; } const newPaidDate = payment.confirmedDate || payment.paymentDate; - if (newPaidDate && appData.payments[pIdx].paidDate !== newPaidDate) { - appData.payments[pIdx].paidDate = newPaidDate; + if (newPaidDate && p.paidDate !== newPaidDate) { + p.paidDate = newPaidDate; changed = true; } @@ -1648,9 +1664,19 @@ async function syncRelationalToJsonPayments() { statusStr === 'atrasado' ? 'overdue' : statusStr === 'cancelado' ? 'cancelled' : 'pending'; - if (p.status !== newStatus) { + const hasChanges = p.status !== newStatus || + Number(p.valor_pago || 0) !== Number(match.valor_pago || 0) || + Number(p.amount || 0) !== Math.max(Number(match.amount_original || 0), Number(match.valor || 0)); + + if (hasChanges) { updatedCount++; - return { ...p, status: newStatus, paidDate: match.data_pagamento || p.paidDate }; + return { + ...p, + status: newStatus, + paidDate: match.data_pagamento || p.paidDate, + valor_pago: Number(match.valor_pago || 0), + amount: Math.max(Number(match.amount_original || 0), Number(match.valor || 0)) + }; } } return p; diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index aeb01a7..9a0b58f 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -283,18 +283,22 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { const isPaid = ['paid', 'pago', 'received', 'confirmed'].includes(normalizedStatus); const valorPagoNoSQL = Number(db.valor_pago || 0); - // Tentamos pegar o maior valor disponível como o Bruto - let amountBruto = Number(db.amount_original) || Number(jsonP.amount) || Number(db.valor) || 0; + // [NOVA LÓGICA]: Pegar o MAIOR valor entre todas as fontes para garantir que seja o BRUTO + let amountBruto = Math.max( + Number(db.amount_original || 0), + Number(db.valor || 0), + Number(jsonP.amount || 0) + ); - // Se está pago e o amountBruto parece ser o líquido (igual ao pago), recomponha - if (isPaid && discount > 0 && amountBruto > 0 && (amountBruto === valorPagoNoSQL || (valorPagoNoSQL === 0 && amountBruto === Number(db.valor)))) { - // Se o SQL tem o valor_pago correto, e o amountBruto é igual a ele, some o desconto - if (amountBruto === valorPagoNoSQL) { - amountBruto += discount; - } else if (valorPagoNoSQL === 0 && amountBruto === Number(db.valor)) { - // Fallback para quando valor_pago ainda não foi preenchido (primeira vez) - amountBruto += discount; - } + // Se o valor bruto encontrado é igual ao que foi pago, e existe desconto, + // então o que encontramos era na verdade o valor líquido. Recuperamos o bruto somando o desconto. + if (isPaid && discount > 0 && amountBruto > 0) { + if (valorPagoNoSQL > 0 && amountBruto <= valorPagoNoSQL) { + amountBruto = valorPagoNoSQL + discount; + } else if (amountBruto < (amountBruto + discount) && amountBruto === (jsonP.amount || 0)) { + // Se veio do JSON e parece ser o líquido + amountBruto = Number(jsonP.amount) + discount; + } } finalPayments.push({ @@ -304,7 +308,7 @@ app.get('/api/portal/financeiro', authMiddleware, async (req, res) => { asaasPaymentUrl: db.asaas_payment_url || jsonP.asaasPaymentUrl || null, amount: amountBruto, discount: discount, - valor_pago: valorPagoNoSQL || (isPaid ? Number(db.valor) : 0), + valor_pago: valorPagoNoSQL > 0 ? valorPagoNoSQL : (isPaid ? (amountBruto - discount) : 0), dueDate: db.vencimento || jsonP.dueDate, status: normalizedStatus, paidDate: db.data_pagamento || jsonP.paidDate || null, diff --git a/portal/src/pages/Financeiro.tsx b/portal/src/pages/Financeiro.tsx index 435df72..e77a463 100644 --- a/portal/src/pages/Financeiro.tsx +++ b/portal/src/pages/Financeiro.tsx @@ -152,56 +152,28 @@ export default function Financeiro() { return (boleto as any)?.link_boleto || null; }; - const getEffectiveValue = (payment: Payment) => { - const baseAmount = payment.amount || 0; - const discount = payment.discount || 0; - const netAmount = baseAmount - discount; + const getDisplayValue = (payment: Payment) => { const status = normalizeStatus(payment); + const isPaid = status === 'paid'; + const valorPago = (payment as any).valor_pago ? Number((payment as any).valor_pago) : 0; + + // Se está pago e temos o valor real no banco, mostramos ele SEM CÁLCULOS + if (isPaid && valorPago > 0) return valorPago; + + // Se está pago mas o banco ainda não sincronizou o valor_pago (fallback) + if (isPaid) return payment.amount - (payment.discount || 0); - // Try to find matching boleto from Supabase sync - const asaasId = payment.asaasPaymentId || (payment as any).asaas_payment_id; - let boleto = null; - - if (asaasId) { - boleto = boletos.find(b => (b as any).asaas_payment_id === asaasId); - } - - if (!boleto) { - // Fallback: Match by due date and base amount (allowing for interest/fines) - boleto = boletos.find(b => { - const bVenc = (b as any).vencimento; - const bVal = Number((b as any).valor); - - // Exact date match - if (bVenc === payment.dueDate) { - // If value is exactly base or exactly net - if (Math.abs(bVal - baseAmount) < 1 || Math.abs(bVal - netAmount) < 1) return true; - // If it's overdue, the boleto value will be HIGHER than baseAmount - if (status === 'overdue' && bVal > netAmount) return true; - } - return false; - }); - } - - // If we have a boleto and it is overdue or paid, use current Asaas value - if (boleto && (boleto as any).valor) { - const bValue = Number((boleto as any).valor); - if (status === 'overdue' || status === 'paid') { - return bValue; - } - } - - // Default: use the discounted base value (net amount) - return netAmount; + // Se está pendente ou atrasado, mostramos o que falta pagar (Líquido esperado) + return payment.amount - (payment.discount || 0); }; const totalPending = payments .filter(p => isPending(p)) - .reduce((s, p) => s + getEffectiveValue(p), 0); + .reduce((s, p) => s + getDisplayValue(p), 0); const totalPaid = payments .filter(p => isPaid(p)) - .reduce((s, p) => s + getEffectiveValue(p), 0); + .reduce((s, p) => s + getDisplayValue(p), 0); const filters: { key: FilterType; label: string }[] = [ { key: 'all', label: 'Todos' }, @@ -348,7 +320,7 @@ export default function Financeiro() { fontWeight: 600, color: normalizeStatus(payment) === 'overdue' ? 'var(--color-danger)' : 'var(--color-primary-light)' }}> - {formatCurrency(getEffectiveValue(payment))} + {formatCurrency(getDisplayValue(payment))} {getStatusBadge(payment)} @@ -415,7 +387,7 @@ export default function Financeiro() {
Valor Pago: - {formatCurrency(receiptPayment.amount - (receiptPayment.discount || 0))} + {formatCurrency(getEffectiveValue(receiptPayment))}
Data de Vencimento: