feat(finance): configure global numeric parser and await SQL update in handleEditSave

This commit is contained in:
Sidney 2026-05-21 09:07:37 -03:00
parent 27fcbada35
commit 024ef1f088
3 changed files with 80 additions and 14 deletions

View File

@ -103,7 +103,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
}; };
const checkInstallmentsForStudent = (studentId: string) => { 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<string, any>; const grouped = {} as Record<string, any>;
studentPayments.forEach(p => { studentPayments.forEach(p => {
const iid = p.asaasInstallmentId || p.installmentId || (typeof p.installment === 'object' ? p.installment.id : p.installment); const iid = p.asaasInstallmentId || p.installmentId || (typeof p.installment === 'object' ? p.installment.id : p.installment);
@ -173,11 +173,63 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
} }
}; };
const dataPaymentsRef = React.useRef(data.payments); const [postgresPayments, setPostgresPayments] = useState<any[]>([]);
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(() => { React.useEffect(() => {
dataPaymentsRef.current = data.payments; fetchPostgresPayments();
}, [data.payments]); }, [data.payments]);
const currentPayments = useMemo(() => {
return loadedFromDb ? postgresPayments : data.payments;
}, [loadedFromDb, postgresPayments, data.payments]);
const syncAsaasPayments = async () => { const syncAsaasPayments = async () => {
if (isSyncing) return; if (isSyncing) return;
@ -327,21 +379,21 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
}; };
const paymentIndexMap = useMemo(() => { const paymentIndexMap = useMemo(() => {
return new Map(data.payments.map((p, i) => [p.id, i])); return new Map(currentPayments.map((p, i) => [p.id, i]));
}, [data.payments]); }, [currentPayments]);
const maxIndexMap = useMemo(() => { const maxIndexMap = useMemo(() => {
const map = new Map<string, number>(); const map = new Map<string, number>();
data.payments.forEach(p => { currentPayments.forEach(p => {
const key = p.installmentId || p.id; const key = p.installmentId || p.id;
const currentIndex = paymentIndexMap.get(p.id) || 0; const currentIndex = paymentIndexMap.get(p.id) || 0;
const maxSoFar = map.get(key) || -1; const maxSoFar = map.get(key) || -1;
if (currentIndex > maxSoFar) map.set(key, currentIndex); if (currentIndex > maxSoFar) map.set(key, currentIndex);
}); });
return map; return map;
}, [data.payments, paymentIndexMap]); }, [currentPayments, paymentIndexMap]);
const filteredPayments = data.payments const filteredPayments = currentPayments
.filter(p => { .filter(p => {
const statusMatch = filterStatus === 'all' || p.status === filterStatus; const statusMatch = filterStatus === 'all' || p.status === filterStatus;
const studentMatch = filterStudent === 'all' || p.studentId === filterStudent; const studentMatch = filterStudent === 'all' || p.studentId === filterStudent;
@ -757,11 +809,18 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
// 2. Escrita dupla: Atualizar no SQL (Fase 2) // 2. Escrita dupla: Atualizar no SQL (Fase 2)
fetch(`/api/admin/cobrancas/${targetId}`, { try {
const sqlResp = await fetch(`/api/admin/cobrancas/${targetId}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ valor: newValor, vencimento: editDate, amount_original: newValor }) body: JSON.stringify({ valor: newValor, vencimento: editDate, amount_original: newValor })
}).catch(err => console.warn('[Fase2:SQL] Erro ao sincronizar edição:', err)); });
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) // 3. Atualizar no JSON (manter compatibilidade)
updateData({ updateData({
@ -1427,7 +1486,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-100 text-xs"> <tbody className="divide-y divide-slate-100 text-xs">
{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 => (
<tr key={p.id} className="hover:bg-slate-50"> <tr key={p.id} className="hover:bg-slate-50">
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="font-bold text-slate-700">{p.description || (p.type === 'monthly' ? 'Mensalidade' : 'Taxa')}</div> <div className="font-bold text-slate-700">{p.description || (p.type === 'monthly' ? 'Mensalidade' : 'Taxa')}</div>

View File

@ -6,6 +6,9 @@
*/ */
import pg from 'pg'; 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 DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager';
const pool = new pg.Pool({ const pool = new pg.Pool({

View File

@ -14,6 +14,10 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import pg from 'pg'; 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 path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import multer from 'multer'; import multer from 'multer';