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 }; }