testes/src/features/financeiro-v2/hooks/useFluxoCaixa.js

351 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useMemo, useCallback } from 'react';
import { extratoService } from '@/services/extratoService';
/**
* Retorna o último saldo armazenado (mais recente por data).
* Usado como "último saldo do mês passado" no cruzamento do fluxo de caixa.
* @param {Array<{ data?, data_saldo?, saldo }>} saldosArmazenados
* @returns {number}
*/
function getUltimoSaldoArmazenado(saldosArmazenados) {
if (!Array.isArray(saldosArmazenados) || saldosArmazenados.length === 0) return 0;
const comData = saldosArmazenados
.map((item) => ({
...item,
data: item.data ?? item.data_saldo ?? item.dataSaldo,
saldo: Number(item.saldo) || 0
}))
.filter((item) => item.data);
if (comData.length === 0) return Number(saldosArmazenados[0]?.saldo) || 0;
comData.sort((a, b) => new Date(b.data) - new Date(a.data));
return comData[0].saldo;
}
/**
* Hook para Fluxo de Caixa (financeiro_v2).
* Cruzamento: Último saldo (armazenado) + Receitas (extrato C) Despesas (extrato D) = Resultado
* O resultado deve coincidir com o Saldo disponível (/saldo).
*/
export function useFluxoCaixa() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [extrato, setExtrato] = useState([]);
const [saldo, setSaldo] = useState({ disponivel: 0 });
const [saldoArmazenado, setSaldoArmazenado] = useState([]);
const [fluxoData, setFluxoData] = useState({ mensal: [] });
const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] });
const [saldoConsolidado, setSaldoConsolidado] = useState(null);
const [porCaixinha, setPorCaixinha] = useState([]);
const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1));
const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString());
const [filtroTipo, setFiltroTipo] = useState('mes'); // 'mes' | 'ano' | 'todos'
const loadData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const mesAtu = filtroMes;
const anoAtu = filtroAno;
const tipo = filtroTipo;
// Monta os parâmetros de forma dinâmica conforme o tipo de filtro
const params = {};
if (tipo === 'mes') {
params.mes = mesAtu;
params.ano = anoAtu;
} else if (tipo === 'ano') {
params.ano = anoAtu;
}
// Se for 'todos', params fica vazio
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData, caixinhasList, categoriasList] = await Promise.all([
extratoService.fetchExtrato(params),
extratoService.fetchSaldo(),
extratoService.fetchSaldoArmazenado(),
extratoService.fetchFluxo(params),
extratoService.getSomaPorCategoria(params),
extratoService.fetchSaldoConsolidado(params),
extratoService.fetchCaixinhas(),
extratoService.fetchCategorias()
]);
// --- CLIENT-SIDE FILTERING FALLBACK ---
const matchesPeriod = (itemDate) => {
if (!itemDate || tipo === 'todos') return true;
// formats: YYYY-MM-DD or DD/MM/YYYY or ISO or "Wed, 11 Feb 2026 00:00:00 GMT"
let d;
if (typeof itemDate === 'string') {
if (itemDate.includes('/')) {
const [day, month, year] = itemDate.split('/');
d = new Date(year, month - 1, day);
} else {
d = new Date(itemDate);
}
} else {
d = new Date(itemDate);
}
if (Number.isNaN(d.getTime())) return true;
const itemMonth = String(d.getMonth() + 1);
const itemYear = String(d.getFullYear());
if (tipo === 'mes') {
return itemMonth === mesAtu && itemYear === anoAtu;
} else if (tipo === 'ano') {
return itemYear === anoAtu;
}
return true;
};
// 0. Build Comprehensive Categories Map
const categoriesMap = {};
// Source A: /categorias/apresentar
if (Array.isArray(categoriasList)) {
categoriasList.forEach(c => {
const cId = String(c.idcategoria ?? c.idcategorias ?? c.id ?? '');
if (cId) {
categoriesMap[cId] = c.categoria || c.name || c.nome || c.descricao;
}
});
}
// Source B: Labels already in /extrato/soma_por_categoria
const rawPorCategoria = somaCategoriaData?.por_categoria || [];
rawPorCategoria.forEach(cat => {
const cId = String(cat.idcategoria ?? '');
if (cId && cat.categoria && isNaN(cat.categoria)) {
if (!categoriesMap[cId] || categoriesMap[cId].startsWith('Categoria ')) {
categoriesMap[cId] = cat.categoria;
}
}
});
// 1. Process Extrato (Table Data) - Consolidate labels
const rawExtrato = Array.isArray(extratoData) ? extratoData : [];
const filteredExtrato = rawExtrato.filter(item =>
matchesPeriod(item.dataEntrada || item.data || item.data_entrada)
);
setExtrato(filteredExtrato.map(item => {
// ID can be in 'categoria' or 'idcategoria'
let idVal = item.idcategoria || '';
if (!idVal && item.categoria && !isNaN(item.categoria)) idVal = item.categoria;
const id = String(idVal || '0');
const catName = categoriesMap[id] ||
item.categoria_nome ||
(isNaN(item.categoria) ? item.categoria : null) ||
(id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
return {
...item,
categoria_nome: catName
};
}));
// 2. Aggregate Categories (Chart Data) - Unified C and D for the same category
const aggregatedSummary = {};
rawPorCategoria.forEach(cat => {
const id = String(cat.idcategoria ?? '0');
const name = categoriesMap[id] || cat.categoria || (id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
if (!aggregatedSummary[id]) {
aggregatedSummary[id] = {
idcategoria: id,
categoria: name,
total_entradas: 0,
total_saidas: 0,
total: 0,
total_transacoes: 0
};
}
const value = Math.abs(Number(cat.total) || 0);
if (cat.tipoOperacao === 'C') {
aggregatedSummary[id].total_entradas += value;
} else {
aggregatedSummary[id].total_saidas += value;
}
aggregatedSummary[id].total += value;
aggregatedSummary[id].total_transacoes += Number(cat.total_transacoes || 0);
});
setSomaCategoria({
por_categoria: Object.values(aggregatedSummary).sort((a, b) => b.total - a.total)
});
// 3. Process Caixinhas
const caixinhasMap = {};
if (Array.isArray(caixinhasList)) {
caixinhasList.forEach(c => {
caixinhasMap[String(c.idcaixinhas_financeiro || c.id)] = c.caixinha || c.name;
});
}
const groupedCaixinha = {};
filteredExtrato.forEach(item => {
const id = String(item.caixinha || '0');
if (!groupedCaixinha[id]) {
groupedCaixinha[id] = {
id: id,
caixinha: caixinhasMap[id] || (id === '0' ? '(sem caixinha)' : `Caixinha ${id}`),
total_entradas: 0,
total_saidas: 0,
total: 0,
diferenca: 0
};
}
const value = Math.abs(Number(item.valor) || 0);
if (item.tipoOperacao === 'C') {
groupedCaixinha[id].total_entradas += value;
} else {
groupedCaixinha[id].total_saidas += value;
}
groupedCaixinha[id].total = groupedCaixinha[id].total_entradas + groupedCaixinha[id].total_saidas;
groupedCaixinha[id].diferenca = groupedCaixinha[id].total_entradas - groupedCaixinha[id].total_saidas;
});
setPorCaixinha(Object.values(groupedCaixinha).sort((a, b) => b.total - a.total));
setSaldo(saldoData || { disponivel: 0 });
setSaldoArmazenado(Array.isArray(armazenadoData) ? armazenadoData : []);
const filteredFluxo = {
mensal: (fluxoResponse?.mensal || []).filter(item => {
if (tipo === 'ano') return String(item.ano) === anoAtu;
return true;
}),
diario: (fluxoResponse?.diario || []).filter(item => {
if (tipo === 'mes') return String(item.mes) === mesAtu && String(item.ano) === anoAtu;
return true;
}),
anual: fluxoResponse?.anual || []
};
setFluxoData(filteredFluxo);
setSaldoConsolidado(consolidadoData || null);
} catch (err) {
console.error('[useFluxoCaixa] Erro ao carregar dados:', err);
setError(err.message || 'Erro ao carregar fluxo de caixa');
setExtrato([]);
setSaldo({ disponivel: 0 });
setSaldoArmazenado([]);
setFluxoData({ mensal: [] });
setSomaCategoria({ por_categoria: [] });
setPorCaixinha([]);
setSaldoConsolidado(null);
} finally {
setLoading(false);
}
}, [filtroMes, filtroAno, filtroTipo]);
useEffect(() => {
loadData();
}, [loadData]);
const receitas = useMemo(() => {
return extrato
.filter((item) => item.tipoOperacao === 'C')
.reduce((acc, item) => acc + Math.abs(Number(item.valor) || 0), 0);
}, [extrato]);
const despesas = useMemo(() => {
return extrato
.filter((item) => item.tipoOperacao === 'D')
.reduce((acc, item) => acc + Math.abs(Number(item.valor) || 0), 0);
}, [extrato]);
const ultimoSaldo = useMemo(
() => getUltimoSaldoArmazenado(saldoArmazenado),
[saldoArmazenado]
);
const resultadoCruzamento = useMemo(() => {
return ultimoSaldo + receitas - despesas;
}, [ultimoSaldo, receitas, despesas]);
/** Card Receitas: total receitas (extrato C) + último saldo do mês passado. Reflete "entradas" para o saldo atual. */
const receitasCard = useMemo(() => ultimoSaldo + receitas, [ultimoSaldo, receitas]);
const saldoDisponivel = useMemo(() => {
const d = saldo?.disponivel;
return typeof d === 'number' ? d : Number(d) || 0;
}, [saldo]);
const bateComSaldo = useMemo(() => {
const diff = Math.abs(resultadoCruzamento - saldoDisponivel);
return diff < 0.01;
}, [resultadoCruzamento, saldoDisponivel]);
/**
* Transforma dados de fluxo para o formato Recharts
* Processa tanto mensal quanto anual se disponível
*/
const getChartData = useCallback((type = 'mensal') => {
const data = fluxoData[type] || [];
// Agrupa por período (mês, ano ou dia) e tipo de operação
const periodGroups = data.reduce((acc, item) => {
let key = '';
let label = '';
if (type === 'diario') {
key = `${item.ano}-${String(item.mes).padStart(2, '0')}-${String(item.dia).padStart(2, '0')}`;
label = `${String(item.dia).padStart(2, '0')}/${String(item.mes).padStart(2, '0')}`;
} else if (type === 'mensal') {
key = `${item.ano}-${String(item.mes).padStart(2, '0')}`;
label = `${item.mes}/${item.ano}`;
} else {
key = `${item.ano}`;
label = `${item.ano}`;
}
if (!acc[key]) {
acc[key] = {
name: label,
periodo: key,
receitas: 0,
despesas: 0
};
}
const valor = Math.abs(Number(item.total) || 0);
if (item.tipoOperacao === 'C') acc[key].receitas += valor;
if (item.tipoOperacao === 'D') acc[key].despesas += valor;
return acc;
}, {});
return Object.values(periodGroups).sort((a, b) => a.periodo.localeCompare(b.periodo));
}, [fluxoData]);
return {
loading,
error,
extrato,
receitas,
receitasCard,
despesas,
ultimoSaldo,
resultadoCruzamento,
saldoDisponivel,
bateComSaldo,
getChartData,
somaCategoria,
porCaixinha,
saldoConsolidado,
filtroMes,
setFiltroMes,
filtroAno,
setFiltroAno,
filtroTipo,
setFiltroTipo,
reload: loadData
};
}