feat(finance): configure global numeric parser and await SQL update in handleEditSave
This commit is contained in:
parent
27fcbada35
commit
024ef1f088
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue