From 024ef1f088aca573a416dc45d807359b58835a2b Mon Sep 17 00:00:00 2001 From: Sidney Date: Thu, 21 May 2026 09:07:37 -0300 Subject: [PATCH] feat(finance): configure global numeric parser and await SQL update in handleEditSave --- manager/components/Finance.tsx | 87 ++++++++++++++++++++++++++++------ manager/services/database.js | 3 ++ portal/server.selfhosted.js | 4 ++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/manager/components/Finance.tsx b/manager/components/Finance.tsx index 9a32e35..360d079 100644 --- a/manager/components/Finance.tsx +++ b/manager/components/Finance.tsx @@ -103,7 +103,7 @@ const Finance: React.FC = ({ data, updateData }) => { }; const checkInstallmentsForStudent = (studentId: string) => { - const studentPayments = data.payments.filter(p => p.studentId === studentId && (p.asaasInstallmentId || p.installmentId || p.installment)); + const studentPayments = currentPayments.filter(p => p.studentId === studentId && (p.asaasInstallmentId || p.installmentId || p.installment)); const grouped = {} as Record; studentPayments.forEach(p => { const iid = p.asaasInstallmentId || p.installmentId || (typeof p.installment === 'object' ? p.installment.id : p.installment); @@ -173,11 +173,63 @@ const Finance: React.FC = ({ data, updateData }) => { } }; - const dataPaymentsRef = React.useRef(data.payments); + const [postgresPayments, setPostgresPayments] = useState([]); + const [loadedFromDb, setLoadedFromDb] = useState(false); + + const fetchPostgresPayments = async () => { + try { + const resp = await fetch('/api/admin/cobrancas'); + if (resp.ok) { + const records = await resp.json(); + const normalized = (records || []).map((r: any) => { + const statusStr = (r.status || 'pending').toLowerCase(); + const normalizedStatus = statusStr === 'pago' || statusStr === 'paid' || statusStr === 'received' || statusStr === 'confirmed' ? 'paid' : + statusStr === 'atrasado' || statusStr === 'overdue' || statusStr === 'vencido' ? 'overdue' : + statusStr === 'cancelado' || statusStr === 'cancelled' || statusStr === 'refunded' ? 'cancelled' : 'pending'; + return { + id: r.local_id || r.asaas_payment_id || String(r.id), + studentId: r.aluno_id, + asaasPaymentId: r.asaas_payment_id || null, + asaasInstallmentId: r.asaas_installment_id || null, + installmentId: r.asaas_installment_id || null, + installment: r.installment || null, + amount: Number(r.valor), + discount: Number(r.discount || 0), + valor_pago: Number(r.valor_pago || 0), + dueDate: r.vencimento ? (r.vencimento.includes('T') ? r.vencimento.split('T')[0] : r.vencimento) : '', + status: normalizedStatus, + paidDate: r.data_pagamento ? (r.data_pagamento.includes('T') ? r.data_pagamento.split('T')[0] : r.data_pagamento) : null, + type: r.type || 'monthly', + description: r.description || null, + installmentNumber: r.installment_number || null, + totalInstallments: r.total_installments || null, + bankSlipUrl: r.link_boleto || null, + transactionReceiptUrl: r.transaction_receipt_url || null, + // retrocompatibilidade + asaas_payment_id: r.asaas_payment_id || null, + asaas_installment_id: r.asaas_installment_id || null, + local_id: r.local_id || null, + valor: Number(r.valor), + vencimento: r.vencimento ? (r.vencimento.includes('T') ? r.vencimento.split('T')[0] : r.vencimento) : '', + status_original: r.status + }; + }); + setPostgresPayments(normalized); + setLoadedFromDb(true); + } + } catch (e) { + console.error('Erro ao buscar cobranças do Postgres:', e); + } + }; + React.useEffect(() => { - dataPaymentsRef.current = data.payments; + fetchPostgresPayments(); }, [data.payments]); + const currentPayments = useMemo(() => { + return loadedFromDb ? postgresPayments : data.payments; + }, [loadedFromDb, postgresPayments, data.payments]); + const syncAsaasPayments = async () => { if (isSyncing) return; @@ -327,21 +379,21 @@ const Finance: React.FC = ({ data, updateData }) => { }; const paymentIndexMap = useMemo(() => { - return new Map(data.payments.map((p, i) => [p.id, i])); - }, [data.payments]); + return new Map(currentPayments.map((p, i) => [p.id, i])); + }, [currentPayments]); const maxIndexMap = useMemo(() => { const map = new Map(); - data.payments.forEach(p => { + currentPayments.forEach(p => { const key = p.installmentId || p.id; const currentIndex = paymentIndexMap.get(p.id) || 0; const maxSoFar = map.get(key) || -1; if (currentIndex > maxSoFar) map.set(key, currentIndex); }); return map; - }, [data.payments, paymentIndexMap]); + }, [currentPayments, paymentIndexMap]); - const filteredPayments = data.payments + const filteredPayments = currentPayments .filter(p => { const statusMatch = filterStatus === 'all' || p.status === filterStatus; const studentMatch = filterStudent === 'all' || p.studentId === filterStudent; @@ -757,11 +809,18 @@ const Finance: React.FC = ({ data, updateData }) => { const result = await response.json(); if (response.ok) { // 2. Escrita dupla: Atualizar no SQL (Fase 2) - fetch(`/api/admin/cobrancas/${targetId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ valor: newValor, vencimento: editDate, amount_original: newValor }) - }).catch(err => console.warn('[Fase2:SQL] Erro ao sincronizar edição:', err)); + try { + const sqlResp = await fetch(`/api/admin/cobrancas/${targetId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ valor: newValor, vencimento: editDate, amount_original: newValor }) + }); + if (!sqlResp.ok) { + console.warn('[Fase2:SQL] Erro ao sincronizar edição no SQL'); + } + } catch (err) { + console.warn('[Fase2:SQL] Erro ao sincronizar edição:', err); + } // 3. Atualizar no JSON (manter compatibilidade) updateData({ @@ -1427,7 +1486,7 @@ const Finance: React.FC = ({ data, updateData }) => { - {data.payments.filter(p => p.studentId === selectedStudentHistory.id).sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()).map(p => ( + {currentPayments.filter(p => p.studentId === selectedStudentHistory.id).sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()).map(p => (
{p.description || (p.type === 'monthly' ? 'Mensalidade' : 'Taxa')}
diff --git a/manager/services/database.js b/manager/services/database.js index a757c19..09a681f 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -6,6 +6,9 @@ */ import pg from 'pg'; +// Registrar parser global para tipo NUMERIC (OID 1700) para retornar como Number +pg.types.setTypeParser(1700, (val) => val === null ? null : parseFloat(val)); + const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager'; const pool = new pg.Pool({ diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index a795fc0..11f5b01 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -14,6 +14,10 @@ import express from 'express'; import cors from 'cors'; import jwt from 'jsonwebtoken'; import pg from 'pg'; + +// Registrar parser global para tipo NUMERIC (OID 1700) para retornar como Number +pg.types.setTypeParser(1700, (val) => val === null ? null : parseFloat(val)); + import path from 'path'; import { fileURLToPath } from 'url'; import multer from 'multer';